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:
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).
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"
}
}
}