Context
I have a react web application which I'm able to deploy to AWS with CodePipeline. My codepipeline is hooked with my React GitHub repository so that whenever I push a change to the GitHub, my codepipeline will re-build the artifact and deploy it to S3 bucket.
Problem
Now I created different .env
files to store environment variables. What I did is quite similar to these:
-
My CDK Code
import * as CDK from "aws-cdk-lib"; import * as YAML from "yaml"; import * as FS from "fs"; import * as CodeBuild from "aws-cdk-lib/aws-codebuild"; import * as S3 from "aws-cdk-lib/aws-s3"; import * as CloudFront from "aws-cdk-lib/aws-cloudfront"; import * as ACM from "aws-cdk-lib/aws-certificatemanager"; import * as Route53 from "aws-cdk-lib/aws-route53"; import * as Route53Targets from "aws-cdk-lib/aws-route53-targets"; import * as CloudFrontOrigins from "aws-cdk-lib/aws-cloudfront-origins"; import * as IAM from "aws-cdk-lib/aws-iam"; import * as codepipeline from "aws-cdk-lib/aws-codepipeline"; import * as codepipeline_actions from "aws-cdk-lib/aws-codepipeline-actions"; export interface CodePipelineStackProps extends CDK.StackProps { // Built in Stack props readonly env: CDK.Environment; readonly description: string; readonly websiteDomain: string; } export class CodePipelineStack extends CDK.Stack { constructor(scope: CDK.App, id: string, props: CodePipelineStackProps) { super(scope, id, props); // AWS CodeBuild artifacts const outputSources = new codepipeline.Artifact(); const outputWebsite = new codepipeline.Artifact(); // AWS CodePipeline pipeline const pipeline = new codepipeline.Pipeline(this, "Pipeline", { pipelineName: "PandaWebsite", restartExecutionOnUpdate: true, }); this.addSourceStage(pipeline, outputSources); this.addBuildStage(pipeline, outputSources, outputWebsite); // Amazon S3 bucket to host the store website artifact const websiteBucket = new S3.Bucket(this, "PandaWebsite", { bucketName: `${props.websiteDomain}-${props.env.account}-${props.env.region}`, websiteIndexDocument: "index.html", websiteErrorDocument: "error.html", removalPolicy: CDK.RemovalPolicy.DESTROY, autoDeleteObjects: true, accessControl: S3.BucketAccessControl.PRIVATE, encryption: S3.BucketEncryption.S3_MANAGED, publicReadAccess: false, blockPublicAccess: S3.BlockPublicAccess.BLOCK_ALL, }); const hostedZone: Route53.IHostedZone = Route53.HostedZone.fromLookup( this, "HostedZoneId", { domainName: props.websiteDomain, } ); const cloudFrontDistribution: CloudFront.Distribution = this.createCloudFrontDistribution( props.websiteDomain, websiteBucket, hostedZone ); new Route53.ARecord(this, "Route53RecordSet", { recordName: props.websiteDomain, zone: hostedZone, target: Route53.RecordTarget.fromAlias( new Route53Targets.CloudFrontTarget(cloudFrontDistribution) ), }); // AWS CodePipeline stage to deployt website pipeline.addStage({ stageName: "Deploy", actions: [ // AWS CodePipeline action to deploy website to S3 bucket new codepipeline_actions.S3DeployAction({ actionName: "PandaWebsite", input: outputWebsite, bucket: websiteBucket, }), ], }); new CDK.CfnOutput(this, "DeployURL", { value: `https://${props.websiteDomain}`, description: "Website URL", }); } private addSourceStage( pipeline: codepipeline.Pipeline, outputSources: codepipeline.Artifact ) { // AWS CodePipeline stage to clone sources from GitHub repository pipeline.addStage({ stageName: "Source", actions: [ new codepipeline_actions.GitHubSourceAction({ actionName: "Checkout", owner: "yangliu", repo: "PandaWebsite", branch: "main", oauthToken: CDK.SecretValue.secretsManager( "PandaWebsite-GitHubToken" ), output: outputSources, trigger: codepipeline_actions.GitHubTrigger.WEBHOOK, }), ], }); } private addBuildStage( pipeline: codepipeline.Pipeline, outputSources: codepipeline.Artifact, outputWebsite: codepipeline.Artifact ) { const buildspecFile = FS.readFileSync("./config/buildspec.yml", "utf-8"); const buildspecFileYaml = YAML.parse(buildspecFile, { prettyErrors: true, }); pipeline.addStage({ stageName: "Build", actions: [ new codepipeline_actions.CodeBuildAction({ actionName: "BuildeWebsite", project: new CodeBuild.PipelineProject(this, "BuildWebsite", { projectName: "BuildeWebsite", environment: { buildImage: CodeBuild.LinuxBuildImage.STANDARD_5_0, }, buildSpec: CodeBuild.BuildSpec.fromObjectToYaml(buildspecFileYaml), }), input: outputSources, outputs: [outputWebsite], }), ], }); } private createCloudFrontDistribution( websiteDomain: string, websiteBucket: S3.Bucket, hostedZone: Route53.IHostedZone ) { const certificateManagerCertificate = new ACM.Certificate( this, "CertificateManagerCertificate", { domainName: websiteDomain, validation: ACM.CertificateValidation.fromDns(hostedZone), } ); // Create a special CloudFront user called an origin access identity (OAI) // and associate it with the CloudFront distribution. const cloudFrontOAI = new CloudFront.OriginAccessIdentity( this, "PandaWebsiteOriginAccessIdentityID", { comment: "OriginAccessIdentityID for PandaWebsite" } ); const cloudfrontUserAccessPolicy = new IAM.PolicyStatement(); cloudfrontUserAccessPolicy.addActions("s3:GetObject"); cloudfrontUserAccessPolicy.addPrincipals(cloudFrontOAI.grantPrincipal); cloudfrontUserAccessPolicy.addResources(websiteBucket.arnForObjects("*")); websiteBucket.addToResourcePolicy(cloudfrontUserAccessPolicy); return new CloudFront.Distribution(this, "CloudFrontDistribution", { domainNames: [websiteDomain], defaultBehavior: { origin: new CloudFrontOrigins.S3Origin(websiteBucket, { // CloudFront can use the OAI to access the files in the S3 bucket // and serve them to users. Users can’t use a direct URL to the // S3 bucket to access a file there. originAccessIdentity: cloudFrontOAI, }), compress: true, allowedMethods: CloudFront.AllowedMethods.ALLOW_GET_HEAD, cachedMethods: CloudFront.CachedMethods.CACHE_GET_HEAD, viewerProtocolPolicy: CloudFront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, cachePolicy: CloudFront.CachePolicy.CACHING_OPTIMIZED, }, errorResponses: [ { httpStatus: 403, responsePagePath: "/index.html", responseHttpStatus: 200, ttl: CDK.Duration.minutes(0), }, { httpStatus: 404, responsePagePath: "/index.html", responseHttpStatus: 200, ttl: CDK.Duration.minutes(0), }, ], priceClass: CloudFront.PriceClass.PRICE_CLASS_ALL, enabled: true, certificate: certificateManagerCertificate, minimumProtocolVersion: CloudFront.SecurityPolicyProtocol.TLS_V1_2_2021, httpVersion: CloudFront.HttpVersion.HTTP2, defaultRootObject: "index.html", enableIpv6: true, }); } }
CodePudding user response:
Follow Sándor Bakos's comment, I'm able to address the issue.
I update my buildspec.yml with:
version: 0.2 env: variables: REACT_APP_DOMAIN: https://<DomainName> REACT_APP_BACKEND_SERVICE_API: https://<DomainName>/api secrets-manager: REACT_APP_GOOGLE_MAP_API_KEY: "REACT_APP_GOOGLE_MAP_API_KEY" phases: install: runtime-versions: nodejs: 14 commands: - echo Performing yarn install - yarn install build: commands: - yarn build artifacts: base-directory: ./build files: - "**/*" cache: paths: - "./node_modules/**/*"
CDK Code for pipeline:
import * as CDK from "aws-cdk-lib"; import * as YAML from "yaml"; import * as FS from "fs"; import * as CodeBuild from "aws-cdk-lib/aws-codebuild"; import * as S3 from "aws-cdk-lib/aws-s3"; import * as CloudFront from "aws-cdk-lib/aws-cloudfront"; import * as ACM from "aws-cdk-lib/aws-certificatemanager"; import * as Route53 from "aws-cdk-lib/aws-route53"; import * as Route53Targets from "aws-cdk-lib/aws-route53-targets"; import * as CloudFrontOrigins from "aws-cdk-lib/aws-cloudfront-origins"; import * as IAM from "aws-cdk-lib/aws-iam"; import * as codepipeline from "aws-cdk-lib/aws-codepipeline"; import * as codepipeline_actions from "aws-cdk-lib/aws-codepipeline-actions"; import * as SecretsManager from "aws-cdk-lib/aws-secretsmanager"; export interface CodePipelineStackProps extends CDK.StackProps { // Built in Stack props readonly env: CDK.Environment; readonly description: string; readonly websiteDomain: string; } export class CodePipelineStack extends CDK.Stack { constructor(scope: CDK.App, id: string, props: CodePipelineStackProps) { super(scope, id, props); // AWS CodeBuild artifacts const outputSources = new codepipeline.Artifact(); const outputWebsite = new codepipeline.Artifact(); // AWS CodePipeline pipeline const pipeline = new codepipeline.Pipeline(this, "Pipeline", { pipelineName: "pandaWebsite", restartExecutionOnUpdate: true, }); this.addSourceStage(pipeline, outputSources); this.addBuildStage(pipeline, outputSources, outputWebsite); // Amazon S3 bucket to host the store website artifact const websiteBucket = new S3.Bucket(this, "pandaWebsite", { bucketName: `${props.websiteDomain}-${props.env.account}-${props.env.region}`, websiteIndexDocument: "index.html", websiteErrorDocument: "error.html", removalPolicy: CDK.RemovalPolicy.DESTROY, autoDeleteObjects: true, accessControl: S3.BucketAccessControl.PRIVATE, encryption: S3.BucketEncryption.S3_MANAGED, publicReadAccess: false, blockPublicAccess: S3.BlockPublicAccess.BLOCK_ALL, }); const hostedZone: Route53.IHostedZone = Route53.HostedZone.fromLookup( this, "HostedZoneId", { domainName: props.websiteDomain, } ); const cloudFrontDistribution: CloudFront.Distribution = this.createCloudFrontDistribution( props.websiteDomain, websiteBucket, hostedZone ); new Route53.ARecord(this, "Route53RecordSet", { recordName: props.websiteDomain, zone: hostedZone, target: Route53.RecordTarget.fromAlias( new Route53Targets.CloudFrontTarget(cloudFrontDistribution) ), }); // AWS CodePipeline stage to deploy website pipeline.addStage({ stageName: "Deploy", actions: [ // AWS CodePipeline action to deploy website to S3 bucket new codepipeline_actions.S3DeployAction({ actionName: "pandaWebsite", input: outputWebsite, bucket: websiteBucket, }), ], }); new CDK.CfnOutput(this, "DeployURL", { value: `https://${props.websiteDomain}`, description: "Website URL", }); } private addSourceStage( pipeline: codepipeline.Pipeline, outputSources: codepipeline.Artifact ) { // AWS CodePipeline stage to clone sources from GitHub repository pipeline.addStage({ stageName: "Source", actions: [ new codepipeline_actions.GitHubSourceAction({ actionName: "Checkout", owner: "yangliunewyork", repo: "pandaWebsite", branch: "main", oauthToken: CDK.SecretValue.secretsManager( "pandaWebsite-GitHubToken" ), output: outputSources, trigger: codepipeline_actions.GitHubTrigger.WEBHOOK, }), ], }); } private addBuildStage( pipeline: codepipeline.Pipeline, outputSources: codepipeline.Artifact, outputWebsite: codepipeline.Artifact ) { const buildspecFile = FS.readFileSync("./config/buildspec.yml", "utf-8"); const buildspecFileYaml = YAML.parse(buildspecFile, { prettyErrors: true, }); const pipelineProject = new CodeBuild.PipelineProject( this, "BuildWebsite", { projectName: "BuildeWebsite", environment: { buildImage: CodeBuild.LinuxBuildImage.STANDARD_5_0, }, buildSpec: CodeBuild.BuildSpec.fromObjectToYaml(buildspecFileYaml), } ); // Below doesn't work yet https://github.com/aws/aws-cdk/issues/18555 const googleMapApiKey = SecretsManager.Secret.fromSecretNameV2(this, "GoogleMapApiKey", "REACT_APP_GOOGLE_MAP_API_KEY"); // add policy to allow fetching from secrets manager pipelineProject.addToRolePolicy( new IAM.PolicyStatement({ effect: IAM.Effect.ALLOW, actions: [ "secretsmanager:GetRandomPassword", "secretsmanager:GetResourcePolicy", "secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret", "secretsmanager:ListSecretVersionIds", ], //resources: [googleMapApiKey.secretArn], resources: ["arn:aws:secretsmanager:us-east-1:587395118549:secret:REACT_APP_GOOGLE_MAP_API_KEY-arSAPR"], }) ); pipeline.addStage({ stageName: "Build", actions: [ new codepipeline_actions.CodeBuildAction({ actionName: "BuildeWebsite", project: pipelineProject, input: outputSources, outputs: [outputWebsite], }), ], }); } private createCloudFrontDistribution( websiteDomain: string, websiteBucket: S3.Bucket, hostedZone: Route53.IHostedZone ) { const certificateManagerCertificate = new ACM.Certificate( this, "CertificateManagerCertificate", { domainName: websiteDomain, validation: ACM.CertificateValidation.fromDns(hostedZone), } ); // Create a special CloudFront user called an origin access identity (OAI) // and associate it with the CloudFront distribution. const cloudFrontOAI = CloudFront.OriginAccessIdentity.fromOriginAccessIdentityName( this, "websiteOriginAccessIdentityID", "ABABX0123X0" ); const cloudfrontUserAccessPolicy = new IAM.PolicyStatement(); cloudfrontUserAccessPolicy.addActions("s3:GetObject"); cloudfrontUserAccessPolicy.addPrincipals(cloudFrontOAI.grantPrincipal); cloudfrontUserAccessPolicy.addResources(websiteBucket.arnForObjects("*")); websiteBucket.addToResourcePolicy(cloudfrontUserAccessPolicy); return new CloudFront.Distribution(this, "CloudFrontDistribution", { domainNames: [websiteDomain], defaultBehavior: { origin: new CloudFrontOrigins.S3Origin(websiteBucket, { // CloudFront can use the OAI to access the files in the S3 bucket // and serve them to users. Users can’t use a direct URL to the // S3 bucket to access a file there. originAccessIdentity: cloudFrontOAI, }), compress: true, allowedMethods: CloudFront.AllowedMethods.ALLOW_GET_HEAD, cachedMethods: CloudFront.CachedMethods.CACHE_GET_HEAD, viewerProtocolPolicy: CloudFront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, cachePolicy: CloudFront.CachePolicy.CACHING_OPTIMIZED, }, errorResponses: [ { httpStatus: 403, responsePagePath: "/index.html", responseHttpStatus: 200, ttl: CDK.Duration.minutes(0), }, { httpStatus: 404, responsePagePath: "/index.html", responseHttpStatus: 200, ttl: CDK.Duration.minutes(0), }, ], priceClass: CloudFront.PriceClass.PRICE_CLASS_ALL, enabled: true, certificate: certificateManagerCertificate, minimumProtocolVersion: CloudFront.SecurityPolicyProtocol.TLS_V1_2_2021, httpVersion: CloudFront.HttpVersion.HTTP2, defaultRootObject: "index.html", enableIpv6: true, }); } }