Skip to content

Commit

Permalink
feat: allow to download .env file from S3 to a local dev environment (#…
Browse files Browse the repository at this point in the history
…17)

* feat: allow to download .env file with build environment variables from S3 to a local dev environment

* integ test

* readme
  • Loading branch information
tmokmss authored May 1, 2024
1 parent 6a519e0 commit 6065025
Show file tree
Hide file tree
Showing 19 changed files with 251 additions and 106 deletions.
16 changes: 16 additions & 0 deletions API.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ Then, the extracted directories will be located as the following:

You can also override the path where assets are extracted by `extractPath` property for each asset.

With `outputEnvFile` property enabled, a `.env` file is automatically generated and uploaded to your S3 bucket. This file can be used running you frontend project locally. You can download the file to your local machine by running the command added in the stack output.

Please also check [the example directory](./example/) for a complete example.

#### Allowing access from the build environment to other AWS resources
Expand Down
18 changes: 11 additions & 7 deletions example/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Stack, StackProps, App, RemovalPolicy, CfnOutput } from 'aws-cdk-lib';
import { Stack, StackProps, App, CfnOutput } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { MockIntegration, RestApi } from 'aws-cdk-lib/aws-apigateway';
import { ContainerImageBuild, NodejsBuild, SociIndexBuild } from '../src/';
import { BlockPublicAccess, Bucket, BucketEncryption } from 'aws-cdk-lib/aws-s3';
import { OriginAccessIdentity, CloudFrontWebDistribution } from 'aws-cdk-lib/aws-cloudfront';
import { DockerImageAsset, Platform } from 'aws-cdk-lib/aws-ecr-assets';
import { DockerImageFunction } from 'aws-cdk-lib/aws-lambda';
import { AwsLogDriver, Cluster, CpuArchitecture, FargateTaskDefinition } from 'aws-cdk-lib/aws-ecs';
import { SubnetType, Vpc } from 'aws-cdk-lib/aws-ec2';
import { OriginAccessIdentity, CloudFrontWebDistribution } from 'aws-cdk-lib/aws-cloudfront';

class NodejsTestStack extends Stack {
constructor(scope: Construct, id: string, props: StackProps = {}) {
Expand All @@ -28,8 +28,8 @@ class NodejsTestStack extends Stack {
);

const dstBucket = new Bucket(this, 'DstBucket', {
autoDeleteObjects: true,
removalPolicy: RemovalPolicy.DESTROY,
// autoDeleteObjects: true,
// removalPolicy: RemovalPolicy.DESTROY,
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
encryption: BucketEncryption.S3_MANAGED,
});
Expand All @@ -54,6 +54,10 @@ class NodejsTestStack extends Stack {
],
});

new CfnOutput(this, 'DistributionUrl', {
value: `https://${distribution.distributionDomainName}`,
});

new NodejsBuild(this, 'ExampleBuild', {
assets: [
{
Expand All @@ -68,13 +72,13 @@ class NodejsTestStack extends Stack {
buildCommands: ['npm ci', 'npm run build'],
buildEnvironment: {
VITE_API_ENDPOINT: api.url,
AAA: 'asdf',
BBB: dstBucket.bucketName,
},
nodejsVersion: 20,
outputEnvFile: true,
});

new CfnOutput(this, 'DistributionUrl', {
value: `https://${distribution.distributionDomainName}`,
});
}
}

Expand Down
15 changes: 14 additions & 1 deletion lambda/trigger-codebuild/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const handler = async (event: Event, context: any) => {
value: event.LogicalResourceId,
},
];
const newPhysicalId = Crypto.randomBytes(16).toString('hex');

let command: StartBuildCommand;
switch (props.type) {
Expand Down Expand Up @@ -67,7 +68,7 @@ export const handler = async (event: Event, context: any) => {
{
name: 'destinationObjectKey',
// This should be random to always trigger a BucketDeployment update process
value: `${Crypto.randomBytes(16).toString('hex')}.zip`,
value: `${newPhysicalId}.zip`,
},
{
name: 'workingDirectory',
Expand All @@ -81,6 +82,18 @@ export const handler = async (event: Event, context: any) => {
name: 'projectName',
value: props.codeBuildProjectName,
},
{
name: 'outputEnvFile',
value: props.outputEnvFile.toString(),
},
{
name: 'envFileKey',
value: `deploy-time-build/${event.StackId.split('/')[1]}/${event.LogicalResourceId}/${newPhysicalId}.env`,
},
{
name: 'envNames',
value: Object.keys(props.environment ?? {}).join(','),
},
...Object.entries(props.environment ?? {}).map(([name, value]) => ({
name,
value,
Expand Down
35 changes: 33 additions & 2 deletions src/nodejs-build.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { posix, join, basename } from 'path';
import { Annotations, CustomResource, Duration } from 'aws-cdk-lib';
import { Annotations, CfnOutput, CustomResource, Duration } from 'aws-cdk-lib';
import { IDistribution } from 'aws-cdk-lib/aws-cloudfront';
import { BuildSpec, LinuxBuildImage, Project } from 'aws-cdk-lib/aws-codebuild';
import { IGrantable, IPrincipal, PolicyStatement } from 'aws-cdk-lib/aws-iam';
Expand Down Expand Up @@ -69,6 +69,12 @@ export interface NodejsBuildProps {
* @default 18
*/
readonly nodejsVersion?: number;
/**
* If true, a .env file is uploaded to an S3 bucket with values of `buildEnvironment` property.
* You can copy it to your local machine by running the command in the stack output.
* @default false
*/
readonly outputEnvFile?: boolean;
}

/**
Expand Down Expand Up @@ -110,11 +116,15 @@ export class NodejsBuild extends Construct implements IGrantable {
}

const destinationObjectKeyOutputKey = 'destinationObjectKey';
const envFileKeyOutputKey = 'envFileKey';

const project = new Project(this, 'Project', {
environment: { buildImage: LinuxBuildImage.fromCodeBuildImageId(buildImage) },
buildSpec: BuildSpec.fromObject({
version: '0.2',
env: {
shell: 'bash',
},
phases: {
install: {
'runtime-versions': {
Expand Down Expand Up @@ -160,6 +170,21 @@ done
'cd "$outputSourceDirectory"',
'zip -r output.zip ./',
'aws s3 cp output.zip "s3://$destinationBucketName/$destinationObjectKey"',
// Upload .env if required
`
if [[ $outputEnvFile == "true" ]]
then
# Split the comma-separated string into an array
for var_name in \${envNames//,/ }
do
echo "Element: $var_name"
var_value="\${!var_name}"
echo "$var_name=$var_value" >> tmp.env
done
aws s3 cp tmp.env "s3://$destinationBucketName/$envFileKey"
fi
`,
],
},
post_build: {
Expand All @@ -182,7 +207,8 @@ cat <<EOF > payload.json
"Status": "$STATUS",
"Reason": "$REASON",
"Data": {
"${destinationObjectKeyOutputKey}": "$destinationObjectKey"
"${destinationObjectKeyOutputKey}": "$destinationObjectKey",
"${envFileKeyOutputKey}": "$envFileKey"
}
}
EOF
Expand Down Expand Up @@ -232,6 +258,7 @@ curl -v -i -X PUT -H 'Content-Type:' -d "@payload.json" "$responseURL"
environment: props.buildEnvironment,
buildCommands: props.buildCommands ?? ['npm run build'],
codeBuildProjectName: project.projectName,
outputEnvFile: props.outputEnvFile ?? false,
};

const custom = new CustomResource(this, 'Resource', {
Expand All @@ -248,5 +275,9 @@ curl -v -i -X PUT -H 'Content-Type:' -d "@payload.json" "$responseURL"
});

deploy.node.addDependency(custom);

if (props.outputEnvFile) {
new CfnOutput(this, 'DownloadEnvFile', { value: `aws s3 cp ${bucket.s3UrlForObject(custom.getAttString(envFileKeyOutputKey))} .env.local` });
}
}
}
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type NodejsBuildResourceProps = {
outputSourceDirectory: string;
buildCommands: string[];
codeBuildProjectName: string;
outputEnvFile: boolean;
};

export type SociIndexBuildResourceProps = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@
"Properties": {
"Code": {
"S3Bucket": {
"Ref": "AssetParameters95e46a828cd9ccdd188870e970e75b59893922504ccc3863966dce2e66fccc49S3Bucket73B303AC"
"Ref": "AssetParametersaa47254059d94cff79a1f8a5a97c4e8a0e14a3f105d2b089464c0beeeb6cfe8dS3BucketA057BD7E"
},
"S3Key": {
"Fn::Join": [
Expand All @@ -253,7 +253,7 @@
"Fn::Split": [
"||",
{
"Ref": "AssetParameters95e46a828cd9ccdd188870e970e75b59893922504ccc3863966dce2e66fccc49S3VersionKey39332242"
"Ref": "AssetParametersaa47254059d94cff79a1f8a5a97c4e8a0e14a3f105d2b089464c0beeeb6cfe8dS3VersionKey4979DC57"
}
]
}
Expand All @@ -266,7 +266,7 @@
"Fn::Split": [
"||",
{
"Ref": "AssetParameters95e46a828cd9ccdd188870e970e75b59893922504ccc3863966dce2e66fccc49S3VersionKey39332242"
"Ref": "AssetParametersaa47254059d94cff79a1f8a5a97c4e8a0e14a3f105d2b089464c0beeeb6cfe8dS3VersionKey4979DC57"
}
]
}
Expand Down Expand Up @@ -1081,17 +1081,17 @@
}
},
"Parameters": {
"AssetParameters95e46a828cd9ccdd188870e970e75b59893922504ccc3863966dce2e66fccc49S3Bucket73B303AC": {
"AssetParametersaa47254059d94cff79a1f8a5a97c4e8a0e14a3f105d2b089464c0beeeb6cfe8dS3BucketA057BD7E": {
"Type": "String",
"Description": "S3 bucket for asset \"95e46a828cd9ccdd188870e970e75b59893922504ccc3863966dce2e66fccc49\""
"Description": "S3 bucket for asset \"aa47254059d94cff79a1f8a5a97c4e8a0e14a3f105d2b089464c0beeeb6cfe8d\""
},
"AssetParameters95e46a828cd9ccdd188870e970e75b59893922504ccc3863966dce2e66fccc49S3VersionKey39332242": {
"AssetParametersaa47254059d94cff79a1f8a5a97c4e8a0e14a3f105d2b089464c0beeeb6cfe8dS3VersionKey4979DC57": {
"Type": "String",
"Description": "S3 key for asset version \"95e46a828cd9ccdd188870e970e75b59893922504ccc3863966dce2e66fccc49\""
"Description": "S3 key for asset version \"aa47254059d94cff79a1f8a5a97c4e8a0e14a3f105d2b089464c0beeeb6cfe8d\""
},
"AssetParameters95e46a828cd9ccdd188870e970e75b59893922504ccc3863966dce2e66fccc49ArtifactHash0DB02EFB": {
"AssetParametersaa47254059d94cff79a1f8a5a97c4e8a0e14a3f105d2b089464c0beeeb6cfe8dArtifactHashC99F560F": {
"Type": "String",
"Description": "Artifact hash for asset \"95e46a828cd9ccdd188870e970e75b59893922504ccc3863966dce2e66fccc49\""
"Description": "Artifact hash for asset \"aa47254059d94cff79a1f8a5a97c4e8a0e14a3f105d2b089464c0beeeb6cfe8d\""
},
"AssetParametersf7fa10e7cd7b9b27f49a4a335f4aa9795fb7e68a665c34ca8bf5711f0aa6aeabS3Bucket1FF22598": {
"Type": "String",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ var handler = async (event, context) => {
value: event.LogicalResourceId
}
];
const newPhysicalId = import_crypto.default.randomBytes(16).toString("hex");
let command;
switch (props.type) {
case "NodejsBuild":
Expand All @@ -76,7 +77,7 @@ var handler = async (event, context) => {
},
{
name: "destinationObjectKey",
value: `${import_crypto.default.randomBytes(16).toString("hex")}.zip`
value: `${newPhysicalId}.zip`
},
{
name: "workingDirectory",
Expand All @@ -90,6 +91,18 @@ var handler = async (event, context) => {
name: "projectName",
value: props.codeBuildProjectName
},
{
name: "outputEnvFile",
value: props.outputEnvFile.toString()
},
{
name: "envFileKey",
value: `deploy-time-build/${event.StackId.split("/")[1]}/${event.LogicalResourceId}/${newPhysicalId}.env`
},
{
name: "envNames",
value: Object.keys(props.environment ?? {}).join(",")
},
...Object.entries(props.environment ?? {}).map(([name, value]) => ({
name,
value
Expand Down
24 changes: 12 additions & 12 deletions test/container-image-build.integ.snapshot/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@
{
"type": "aws:cdk:asset",
"data": {
"path": "asset.95e46a828cd9ccdd188870e970e75b59893922504ccc3863966dce2e66fccc49",
"id": "95e46a828cd9ccdd188870e970e75b59893922504ccc3863966dce2e66fccc49",
"path": "asset.aa47254059d94cff79a1f8a5a97c4e8a0e14a3f105d2b089464c0beeeb6cfe8d",
"id": "aa47254059d94cff79a1f8a5a97c4e8a0e14a3f105d2b089464c0beeeb6cfe8d",
"packaging": "zip",
"sourceHash": "95e46a828cd9ccdd188870e970e75b59893922504ccc3863966dce2e66fccc49",
"s3BucketParameter": "AssetParameters95e46a828cd9ccdd188870e970e75b59893922504ccc3863966dce2e66fccc49S3Bucket73B303AC",
"s3KeyParameter": "AssetParameters95e46a828cd9ccdd188870e970e75b59893922504ccc3863966dce2e66fccc49S3VersionKey39332242",
"artifactHashParameter": "AssetParameters95e46a828cd9ccdd188870e970e75b59893922504ccc3863966dce2e66fccc49ArtifactHash0DB02EFB"
"sourceHash": "aa47254059d94cff79a1f8a5a97c4e8a0e14a3f105d2b089464c0beeeb6cfe8d",
"s3BucketParameter": "AssetParametersaa47254059d94cff79a1f8a5a97c4e8a0e14a3f105d2b089464c0beeeb6cfe8dS3BucketA057BD7E",
"s3KeyParameter": "AssetParametersaa47254059d94cff79a1f8a5a97c4e8a0e14a3f105d2b089464c0beeeb6cfe8dS3VersionKey4979DC57",
"artifactHashParameter": "AssetParametersaa47254059d94cff79a1f8a5a97c4e8a0e14a3f105d2b089464c0beeeb6cfe8dArtifactHashC99F560F"
}
},
{
Expand Down Expand Up @@ -71,22 +71,22 @@
"data": "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f306AEFF37"
}
],
"/ContainerImageBuildIntegTest/AssetParameters/95e46a828cd9ccdd188870e970e75b59893922504ccc3863966dce2e66fccc49/S3Bucket": [
"/ContainerImageBuildIntegTest/AssetParameters/aa47254059d94cff79a1f8a5a97c4e8a0e14a3f105d2b089464c0beeeb6cfe8d/S3Bucket": [
{
"type": "aws:cdk:logicalId",
"data": "AssetParameters95e46a828cd9ccdd188870e970e75b59893922504ccc3863966dce2e66fccc49S3Bucket73B303AC"
"data": "AssetParametersaa47254059d94cff79a1f8a5a97c4e8a0e14a3f105d2b089464c0beeeb6cfe8dS3BucketA057BD7E"
}
],
"/ContainerImageBuildIntegTest/AssetParameters/95e46a828cd9ccdd188870e970e75b59893922504ccc3863966dce2e66fccc49/S3VersionKey": [
"/ContainerImageBuildIntegTest/AssetParameters/aa47254059d94cff79a1f8a5a97c4e8a0e14a3f105d2b089464c0beeeb6cfe8d/S3VersionKey": [
{
"type": "aws:cdk:logicalId",
"data": "AssetParameters95e46a828cd9ccdd188870e970e75b59893922504ccc3863966dce2e66fccc49S3VersionKey39332242"
"data": "AssetParametersaa47254059d94cff79a1f8a5a97c4e8a0e14a3f105d2b089464c0beeeb6cfe8dS3VersionKey4979DC57"
}
],
"/ContainerImageBuildIntegTest/AssetParameters/95e46a828cd9ccdd188870e970e75b59893922504ccc3863966dce2e66fccc49/ArtifactHash": [
"/ContainerImageBuildIntegTest/AssetParameters/aa47254059d94cff79a1f8a5a97c4e8a0e14a3f105d2b089464c0beeeb6cfe8d/ArtifactHash": [
{
"type": "aws:cdk:logicalId",
"data": "AssetParameters95e46a828cd9ccdd188870e970e75b59893922504ccc3863966dce2e66fccc49ArtifactHash0DB02EFB"
"data": "AssetParametersaa47254059d94cff79a1f8a5a97c4e8a0e14a3f105d2b089464c0beeeb6cfe8dArtifactHashC99F560F"
}
],
"/ContainerImageBuildIntegTest/AssetParameters/f7fa10e7cd7b9b27f49a4a335f4aa9795fb7e68a665c34ca8bf5711f0aa6aeab/S3Bucket": [
Expand Down
Loading

0 comments on commit 6065025

Please sign in to comment.