Home > Enterprise >  How to deploy React to AWS with environment variables
How to deploy React to AWS with environment variables

Time:06-16

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:

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

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