Search code examples
aws-cdkaws-pipeline

How to deploy React to AWS using AWS CDK?


I have the following React project structure

project/
  - cdk/
    - bin/
      - cdk.ts
    - lib/
      - pipeline.stack.ts
    - etc.
  - src/
    - all files related to React app
  package.json (of the React)

NOTE: In other words the cdk was initialized in existing React app.

The cdk.ts file looks like this

#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { PipelineStack } from "../lib/pipeline.stack";

const app = new cdk.App();

new PipelineStack(app, "PipelineStack", {
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_DEFAULT_REGION,
  },
});

app.synth();

The pipeline.stack.ts file looks like this

import * as cdk from "aws-cdk-lib";
import { CodePipeline, CodePipelineSource, ShellStep } from "aws-cdk-lib/pipelines";
import { Construct } from "constructs";

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

    // Define the pipeline
    const pipeline = new CodePipeline(this, "Pipeline", {
      pipelineName: "MyPipeline",
      synth: new ShellStep("Synth", {
        input: CodePipelineSource.gitHub("<OWNER>/<PROJECT>", "dev"),
        commands: ["cd cdk", "npm ci", "npm run build", "npx cdk synth"],
        primaryOutputDirectory: "cdk/cdk.out",
      }),
    });
  }
}

In order to create this pipeline on AWS, I had to manually run cdk deploy --profile my-profile. After that, I was able to trigger the pipeline by committing code to the dev branch of my GitHub repository.

Now, I need to add the missing steps:

  • Build the React App.
  • Deploy artifacts to S3 and CloudFront.

I'm not sure where to find the information on how to do this using aws-cdk-lib/pipelines. Could you help me understand how to add the missing code to automatically build the React app and host it on S3?

P.S. Any links to videos or guides would be welcome (AWS CDK v2).


Solution

  • I was able to deploy my React application using AWS CDK Pipelines

    This is my project structure

    bin/
      cdk.ts
    lib/
      pipeline.stack.ts
      app.stage.ts
      web-site.stack.ts
    src/
      ...all React source code
    package.json (Shared file for AWS CDK and React)
    cdk.json
    cdk.context.json
    

    These are the internals of the files. I tried to make them clean, but they may include some strange parts.

    P.S. Hashtag (#) is a native way to make class fields private. See Private Properties MDN

    bin/cdk.ts

    #!/usr/bin/env node
    import "source-map-support/register";
    import * as cdk from "aws-cdk-lib";
    import { Environment } from "../lib/_constants/environment.constant";
    import { PipelineStack } from "../lib/pipeline/pipeline.stack";
    import { AppStack } from "../lib/app.stack";
    
    const app = new cdk.App();
    
    const env = app.node.getContext("env")
    const context = app.node.getContext(env) as Context;
    
    const config: Config = {
      environment: env,
      github: context.github,
      domain: context.domain
    }
    
    new PipelineStack(app, "Pipeline", {
      env: {
        account: context.account,
        region: context.region
      },
      config,
    })
    
    app.synth();
    
    export interface Config {
      environment: Environment
      github: {
        branch: string
      },
      domain: {
        name: string
        alternativeNames: string[]
        certificateArn: string
      }
    }
    
    interface Context {
      account: string;
      region: string;
      github: {
        branch: string
      }
      domain: {
        name: string
        alternativeNames: string[]
        certificateArn: string
      }
    }
    
    

    pipeline.stack.ts

    import { SecretValue, Stack, StackProps, pipelines } from "aws-cdk-lib";
    import { Construct } from "constructs";
    import { Config } from "../../bin/cdk";
    import { AppStage } from "./app.stage";
    
    interface PipelineStackProps extends StackProps {
      config: Config
    }
    
    export class PipelineStack extends Stack {
      #props: PipelineStackProps
    
      constructor(scope: Construct, id: string, props: PipelineStackProps) {
        super(scope, id, props)
    
        this.#props = props
    
        this.#init()
      }
    
      #init() {
        const pipeline = new pipelines.CodePipeline(this, "Pipeline", {
          synth: new pipelines.ShellStep('Synth', {
            input: pipelines.CodePipelineSource.gitHub("<OWNER>/<PROJECT_NAME>", this.#props.config.github.branch, {
              authentication: SecretValue.secretsManager("github-token")
            }),
            commands: [
              "npm ci",
              "npm run build",
              `npx cdk synth --context env=${this.#props.config.environment}`
            ]
          }),
          publishAssetsInParallel: false, // or true, if your AWS account has enough parallel capacity
          selfMutation: true // or false
        })
    
        const appStage = new AppStage(this, 'App', { env: this.#props.env, config: this.#props.config })
    
        pipeline.addStage(appStage)
      }
    }
    
    
    

    app.stage.ts

    import { Stage, StageProps } from "aws-cdk-lib"
    import { Construct } from "constructs"
    import { AppStack } from "../app.stack"
    import { Config } from "../../bin/cdk"
    
    interface AppStageProps extends StageProps {
      config: Config
    }
    
    export class AppStage extends Stage {
      #props: AppStageProps
    
      constructor(scope: Construct, id: string, props: AppStageProps) {
        super(scope, id, props)
    
        this.#props = props
    
        this.#init()
      }
    
      #init() {
        new AppStack(this, 'App', { env: this.#props.env, config: this.#props.config })
      }
    }
    
    

    web-site.stack.ts

    import { RemovalPolicy, Stack, StackProps, aws_certificatemanager, aws_cloudfront, aws_cloudfront_origins, aws_s3, aws_s3_deployment } from "aws-cdk-lib";
    import { Construct } from "constructs";
    import path, { dirname } from "path";
    import { Config } from "../../bin/cdk";
    
    interface WebSiteStackProps extends StackProps {
      config: Config
    }
    
    export class WebSiteStack extends Stack {
      #props: WebSiteStackProps;
    
      #originAccessIdentity: aws_cloudfront.OriginAccessIdentity;
      #distribution: aws_cloudfront.Distribution;
      #S3Origin: aws_cloudfront_origins.S3Origin
      #bucket: aws_s3.Bucket;
    
      constructor(scope: Construct, id: string, props: WebSiteStackProps) {
        super(scope, id, props)
    
        this.#props = props;
    
        this.#initBucket()
        this.#initOriginAccessIdentity()
        this.#initOrigin()
        this.#initDistribution()
        this.#initBucketDeployment()
        this.#configureS3Integration()
      }
    
      // TIP: Initialize bucket where React sources will be stored
      #initBucket() {
        this.#bucket = new aws_s3.Bucket(this, "Bucket", {
          removalPolicy: RemovalPolicy.RETAIN, // or RemovalPolicy.DESTROY
          autoDeleteObjects: false // or true
        });
      }
    
      // TIP: It automates the copying of the React artifacts from your pipeline CodeBuild container to a S3 bucket
      #initBucketDeployment() {
        new aws_s3_deployment.BucketDeployment(this, "WebDeployment", {
          sources: [aws_s3_deployment.Source.asset(path.join(__dirname, '../../dist'))], // relative to the Stack dir
          destinationBucket: this.#bucket,
          distribution: this.#distribution // Automatic cache invalidation. See https://docs.aws.amazon.com/cdk/api/v1/docs/aws-s3-deployment-readme.html#cloudfront-invalidation (might not automatically move you to the chapter, scroll on your own)
        })
      }
    
      // TIP: Create OriginAccessIdentity that will be used to access S3 bucket
      #initOriginAccessIdentity() {
        this.#originAccessIdentity = new aws_cloudfront.OriginAccessIdentity(this, "OriginAccessIdentity");
      }
    
      // TIP: I'm not sure what it does :(
      #initOrigin() {
        this.#S3Origin = new aws_cloudfront_origins.S3Origin(this.#bucket, {
          originAccessIdentity: this.#originAccessIdentity,
        })
      }
      
      // TIP: Create CloudFront distribution
      #initDistribution() {
        this.#distribution = new aws_cloudfront.Distribution(this, "Distribution", {
          defaultBehavior: {
            origin: this.#S3Origin,
            viewerProtocolPolicy: aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
          },
          defaultRootObject: "index.html",
          errorResponses: [ // TIP: TIP: Always return index.html even if the user presses the enter key in the browser's URL bar when they are on a non-existent page.
            {
              httpStatus: 404,
              responseHttpStatus: 200,
              responsePagePath: "/index.html",
            },
            {
              httpStatus: 403,
              responseHttpStatus: 200,
              responsePagePath: "/index.html",
            },
          ],
          domainNames: [this.#props.config.domain.name, ...this.#props.config.domain.alternativeNames], // TIP: Provide custom domains that will be used to access website (e.g. https://example.com)
          certificate: aws_certificatemanager.Certificate.fromCertificateArn(this, "Certificate", this.#props.config.domain.certificateArn),
        })
      }
    
      #configureS3Integration() {
        this.#bucket.grantRead(this.#originAccessIdentity) // TIP: Allow originAccessIdentity to have read access to the private bucket
      }
    }
    

    cdk.context.json

    {
      "dev": {
        "account": "111111111111",
        "region": "eu-central-1",
        "github": {
          "branch": "dev"
        },
        "domain": {
          "name": "dev.example.com",
          "alternativeNames": [],
          "certificateArn": "arn:aws:acm:us-east-1:111111111111:certificate/4568ace7-aff8-4849-8b3f-17d8a4ae9653"
        }
      },
      "staging": {
        "account": "222222222222",
        "region": "eu-central-1",
        "github": {
          "branch": "staging"
        },
        "domain": {
          "name": "staging.example.com",
          "alternativeNames": [],
          "certificateArn": "arn:aws:acm:us-east-1:222222222222:certificate/4568ace7-aff8-4849-8b3f-17d8a4ae9653"
        }
      },
      "prod": {
        "account": "333333333333",
        "region": "eu-central-1",
        "github": {
          "branch": "main"
        },
        "domain": {
          "name": "example.com",
          "alternativeNames": [
            "www.example.com"
          ],
          "certificateArn": "arn:aws:acm:us-east-1:333333333333:certificate/4568ace7-aff8-4849-8b3f-17d8a4ae9653"
        }
      }
    }