Reputation: 751
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.
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?
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,
});
}
}
Upvotes: 1
Views: 3310
Reputation: 751
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,
});
}
}
Upvotes: 0