0

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:

Thus yarn build:prod will build the website artifact with .env.production file.

As we should not add .env files to GitHub for security reasons. How should I setup environment variables so that in the build stage of my pipeline it can get the .env files from somewhere? enter image description here

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,
    });
  }
}



jellycsc
  • 10,904
  • 2
  • 15
  • 32
Yang Liu
  • 541
  • 9
  • 26
  • 1
    There are multiple way you can add env variables to the build stage. You can save the env variables to parameter store or secret manager and reference to them in the buildspac.yaml. https://blog.shikisoft.com/define-environment-vars-aws-codebuild-buildspec/ – Sándor Bakos Jun 14 '22 at 19:21

1 Answers1

0

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,
    });
  }

}
Yang Liu
  • 541
  • 9
  • 26