Search code examples
amazon-web-servicesaws-cloudformationamazon-route53aws-cdk

How do I make my CloudFormation / CodePipeline update a domain name to point to an S3 bucket when using CDK?


I'm using CDK to deploy a CodePipeline that builds and deploys a React application to S3. All of that is working, but I want the deployment to update a domain name to point that S3 bucket.

I already have the Zone defined in Route53 but it is defined by a different cloud formation stack because there are a lot of details that are not relevant for this app (MX, TXT, etc). What's the right way for my Pipeline/Stacks to set those domain names?

I could think of two solutions:

  • Delegate the domain to another zone, so zone example.com delegates staging.example.com.
  • Have my pipeline inject records into the existing zone.

I didn't try the delegation zone method. I was slightly concerned about manually maintaining the generated nameservers from staging.example.com into my CloudFormation for zone example.com.

I did try injecting the records into the existing zone, but I run into some issues. I'm open to either solving these issues or doing this whichever way is correct.

In my stack (full pipeline at the bottom) I first define and deploy to the bucket:

const bucket = new s3.Bucket(this, "Bucket", {...})
new s3d.BucketDeployment(this, "WebsiteDeployment", {
    sources: [...],
    destinationBucket: bucket
})

then I tried to retrieve the zone and add the CNAME to it:

const dnsZone = route53.HostedZone.fromLookup(this, "DNS zone", {domainName: "example.com"})
new route53.CnameRecord(this, "cname", {
    zone: dnsZone,
    recordName: "staging",
    domainName: bucket.bucketWebsiteDomainName
})

This fails due to lack of permissions to the zone, which is reasonable:

[Container] 2022/01/30 11:35:17 Running command npx cdk synth
current credentials could not be used to assume 'arn:aws:iam::...:role/cdk-hnb659fds-lookup-role-...-us-east-1', but are for the right account. Proceeding anyway.
[Error at /Pipeline/Staging] User: arn:aws:sts::...:assumed-role/Pipeline-PipelineBuildSynthCdkBuildProje-1H5AV7C28FZ3S/AWSCodeBuild-ada5ef88-cc82-4309-9acf-11bcf0bae878 is not authorized to perform: route53:ListHostedZonesByName because no identity-based policy allows the route53:ListHostedZonesByName action
Found errors

To try to solve that, I added rolePolicyStatements to my CodeBuildStep

                    rolePolicyStatements: [
                        new iam.PolicyStatement({
                            actions: ["route53:ListHostedZonesByName"],
                            resources: ["*"],
                            effect: iam.Effect.ALLOW
                        })
                    ]

which might make more sense in the context of the whole file (at the bottom of this question). That had no effect. I'm not sure if the policy statement is wrong or I'm adding it to the wrong role.

After adding that rolePolicyStatements, I run cdk deploy which showed me this output:

> cdk deploy

✨  Synthesis time: 33.43s

This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

IAM Statement Changes
┌───┬──────────┬────────┬───────────────────────────────┬───────────────────────────────────────────────────────────┬───────────┐
│   │ Resource │ Effect │ Action                        │ Principal                                                 │ Condition │
├───┼──────────┼────────┼───────────────────────────────┼───────────────────────────────────────────────────────────┼───────────┤
│ + │ *        │ Allow  │ route53:ListHostedZonesByName │ AWS:${Pipeline/Pipeline/Build/Synth/CdkBuildProject/Role} │           │
└───┴──────────┴────────┴───────────────────────────────┴───────────────────────────────────────────────────────────┴───────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

After deployment finishes, there's a role that I can see in the AWS console that has:

        {
            "Action": "route53:ListHostedZonesByName",
            "Resource": "*",
            "Effect": "Allow"
        }

The ARN of the role is arn:aws:iam::...:role/Pipeline-PipelineBuildSynthCdkBuildProje-1H5AV7C28FZ3S. I'm not 100% if the permissions are being granted to the right thing.

This is my whole CDK pipeline:

import * as path from "path";
import {Construct} from "constructs"
import * as pipelines from "aws-cdk-lib/pipelines"
import * as cdk from "aws-cdk-lib"
import * as s3 from "aws-cdk-lib/aws-s3"
import * as s3d from "aws-cdk-lib/aws-s3-deployment"
import * as iam from "aws-cdk-lib/aws-iam"
import * as route53 from "aws-cdk-lib/aws-route53";

export class MainStack extends cdk.Stack {
    constructor(scope: Construct, id: string, props?: cdk.StageProps) {
        super(scope, id, props)

        const bucket = new s3.Bucket(this, "Bucket", {
            websiteIndexDocument: "index.html",
            websiteErrorDocument: "error.html",
            publicReadAccess: true,
        })

        const dnsZone = route53.HostedZone.fromLookup(this, "DNS zone", {domainName: "example.com"})
        new route53.CnameRecord(this, "cname", {
            zone: dnsZone,
            recordName: "staging",
            domainName: bucket.bucketWebsiteDomainName
        })

        new s3d.BucketDeployment(this, "WebsiteDeployment", {
            sources: [s3d.Source.asset(path.join(process.cwd(), "../build"))],
            destinationBucket: bucket
        })
    }
}

export class DeployStage extends cdk.Stage {
    public readonly mainStack: MainStack

    constructor(scope: Construct, id: string, props?: cdk.StageProps) {
        super(scope, id, props)
        this.mainStack = new MainStack(this, "MainStack", {env: props?.env})
    }
}

export interface PipelineStackProps extends cdk.StackProps {
    readonly githubRepo: string
    readonly repoBranch: string
    readonly repoConnectionArn: string
}

export class PipelineStack extends cdk.Stack {
    constructor(scope: Construct, id: string, props: PipelineStackProps) {
        super(scope, id, props)

        const pipeline = new pipelines.CodePipeline(this, id, {
            pipelineName: id,
            synth: new pipelines.CodeBuildStep("Synth", {
                    input: pipelines.CodePipelineSource.connection(props.githubRepo, props.repoBranch, {connectionArn: props.repoConnectionArn}),
                    installCommands: [
                        "npm install -g aws-cdk"
                    ],
                    commands: [
                        // First build the React app.
                        "npm ci",
                        "npm run build",
                        // Now build the CF stack.
                        "cd infra",
                        "npm ci",
                        "npx cdk synth"
                    ],
                    primaryOutputDirectory: "infra/cdk.out",
                    rolePolicyStatements: [
                        new iam.PolicyStatement({
                            actions: ["route53:ListHostedZonesByName"],
                            resources: ["*"],
                            effect: iam.Effect.ALLOW
                        })
                    ]
                },
            ),
        })


        const deploy = new DeployStage(this, "Staging", {env: props?.env})
        const deployStage = pipeline.addStage(deploy)

    }
}

Solution

  • You cannot depend on CDK pipeline to fix itself if the synth stage is failing, since the Pipeline CloudFormation Stack is changed in the SelfMutate stage which uses the output of the synth stage. You will need to do one of the following options to fix your pipeline:

    1. Run cdk synth and cdk deploy PipelineStack locally (or anywhere outside the pipeline, where you have the required AWS IAM permissions). Edit: You will need to temporarily set selfMutatation to false for this to work (Reference)

    2. Temporarily remove route53.HostedZone.fromLookup and route53.CnameRecord from your MainStack while still keeping the rolePolicyStatements change. Commit and push your code, let CodePipeline run once, making sure that the Pipeline self mutates and the IAM role has the required additional permissions. Add back the route53 constructs, commit, push again and check whether your code works with the new changes.