question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

How to deploy SPA to S3 using CDK in a pipeline

See original GitHub issue

I have a single page react application that I want to host in S3. The source is in git and I would like to set up a Pipeline such that when I push to master it fetches the source, builds it and deploys to S3. Pretty straight forward. I have created a CDK stack for deploying to S3, and it looks like this:

import { Construct, StackProps, Stack, RemovalPolicy, CfnParameter } from '@aws-cdk/core';
import { Bucket, HttpMethods } from "@aws-cdk/aws-s3";
import { BucketDeployment, Source, ISource } from "@aws-cdk/aws-s3-deployment";
import { CloudFrontWebDistribution } from '@aws-cdk/aws-cloudfront';

export default function createReactStack(scope: Construct, name: string, props?: StackProps) {
  const stack = new Stack(scope, `${name}-React`, props);

  // This works locally...
  const source = Source.asset('../react/build');
  // ... but in the pipeline I need the following line
  const source = createSourceAsset(stack);

  const bucket = createBucket(stack);

  createDeploySourceToBucket(stack, source, bucket);

  createCloudFront(stack, bucket);

  return stack;
}

function createSourceAsset(stack: Stack) {
  const bucketName = new CfnParameter(stack, `${stack.stackName}-bucketName`).valueAsString;
  const zipFile = new CfnParameter(stack, `${stack.stackName}-zipFile`).valueAsString;
  return Source.bucket(Bucket.fromBucketName(stack, 'bucketName', bucketName), zipFile);
}

function createBucket(stack: Stack): Bucket {
  return new Bucket(stack, `${stack.stackName}.Bucket`, {
    removalPolicy: RemovalPolicy.DESTROY,
    websiteIndexDocument: 'index.html',
    websiteErrorDocument: 'index.html',
    publicReadAccess: true,
    cors: [
      {
        allowedOrigins: ['*'],
        allowedMethods: [HttpMethods.GET],
      }
    ]
  });
}

function createDeploySourceToBucket(stack: Stack, source: ISource, bucket: Bucket) {
  return new BucketDeployment(stack, `${stack.stackName}.BucketDeployment`, {
    sources: [source],
    destinationBucket: bucket
  });
}

function createCloudFront(stack: Stack, bucket: Bucket) {
  new CloudFrontWebDistribution(stack, `${stack.stackName}.CloudFront`, {
    originConfigs: [
      {
        s3OriginSource: {
          s3BucketSource: bucket
        },
        behaviors: [
          { isDefaultBehavior: true }
        ]
      }
    ]
  });
}

This works well from a local environment, I’m able to run cdk synth and cdk deploy to deploy this stack.

But I don’t want to manually deploy it, I want to set up a pipeline to deploy it. My pipeline has 4 stages:

  • Get the source (using S3SourceAction)
  • Build the react SPA app (using CodeBuildAction)
  • Synth the CloudFormation templates (using CodeBuildAction)
  • Deploy the stack based on the template (using CloudFormationCreateUpdateStackAction)

The first three steps are straight forward, but the last step seems impossible to achieve.

I’ve tried using CloudFormationCreateUpdateStackAction, like this:

new CloudFormationCreateUpdateStackAction({
  actionName: `Deploy-Cdk`,
  templatePath: input.atPath(
    `${stackName}.template.json`
  ),
  stackName,
  adminPermissions: true,
  extraInputs: [source],
  parameterOverrides: {
    [`${stackName}-bucketName`]: source.s3Location.bucketName,
    [`${stackName}-zipFile`]: source.s3Location.objectKey
  }
})

But when I do I get an error in the pipeline like this:

image

These seem to be parameters needed by BucketDeployment, and their values are magically set by cdk deploy but not by CloudFormationCreateUpdateStackAction. How can I make this work in Pipeline? The assetParameters seem to have two hashes as part of them, how can I generate them correctly?

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Comments:10 (6 by maintainers)

github_iconTop GitHub Comments

3reactions
skinny85commented, Nov 11, 2019

It’s all doable - from your description, I thought the Bucket and CloudFrontWebDistribution were all that the SPA needed, so I didn’t see much value in adding a CloudFormation Stack to the Pipeline. But it’s certainly doable:

import { App, Stack, StackProps, Construct, RemovalPolicy } from '@aws-cdk/core';
import s3 = require('@aws-cdk/aws-s3');
import s3deploy = require('@aws-cdk/aws-s3-deployment');
import cloudfront = require('@aws-cdk/aws-cloudfront');
import codebuild = require('@aws-cdk/aws-codebuild');
import codepipeline = require('@aws-cdk/aws-codepipeline');
import cpactions = require('@aws-cdk/aws-codepipeline-actions');

/* This class contains all AWS resources needed by the Single-Page Application. */
class SpaResources extends Construct {
    constructor(scope: Construct, id: string) {
        super(scope, id);

        // add all SPA-required resources here...
    }
}

interface MyStackProps extends StackProps {
    readonly local: boolean;
}

class MyStack extends Stack {
    constructor(scope: Construct, id: string, props: MyStackProps) {
        super(scope, id, props);

        const deployBucket = new s3.Bucket(this, 'Bucket', {
            removalPolicy: RemovalPolicy.DESTROY,
            websiteIndexDocument: 'index.html',
            websiteErrorDocument: 'index.html',
            publicReadAccess: true,
            cors: [
                {
                    allowedOrigins: ['*'],
                    allowedMethods: [s3.HttpMethods.GET],
                }
            ]
        });

        new cloudfront.CloudFrontWebDistribution(this, 'CloudFront', {
            originConfigs: [
                {
                    s3OriginSource: {
                        s3BucketSource: deployBucket,
                    },
                    behaviors: [
                        { isDefaultBehavior: true }
                    ]
                }
            ]
        });

        if (props.local) {
            // add the BucketDeployment part...
            const source = s3deploy.Source.asset('../react/build');
            new s3deploy.BucketDeployment(this, 'BucketDeployment', {
                sources: [source],
                destinationBucket: deployBucket,
            });

            // add the SPA-required resources to this Stack...
            new SpaResources(this, 'SpaResources');
        } else {
            // CodePipeline
            const sourceOutput = new codepipeline.Artifact();
            const spaBuildOutput = new codepipeline.Artifact();
            const cdkBuildOutput = new codepipeline.Artifact();
            new codepipeline.Pipeline(this, 'Pipeline', {
                stages: [
                    {
                        stageName: 'Source',
                        actions: [
                            // add your source from Git here...
                        ],
                    },
                    {
                        stageName: 'Build',
                        actions: [
                            new cpactions.CodeBuildAction({
                                actionName: 'Build_SPA',
                                input: sourceOutput,
                                project: new codebuild.PipelineProject(this, 'SpaBuild', {
                                    buildSpec: codebuild.BuildSpec.fromObject({
                                        version: '0.2',
                                        phases: {
                                            pre_build: {
                                                commands: [
                                                    'cd react',
                                                    'npm install',
                                                ],
                                            },
                                            build: {
                                                commands: [
                                                    'npm run build', // or whatever other process you use for the SPA build...
                                                ],
                                            },
                                        },
                                        artifacts: {
                                            'base-directory': 'react/build',
                                            files: '**/*',
                                        },
                                    }),
                                }),
                                outputs: [spaBuildOutput],
                            }),
                            new cpactions.CodeBuildAction({
                                actionName: 'Build_CDK',
                                input: sourceOutput,
                                project: new codebuild.PipelineProject(this, 'SpaBuild', {
                                    buildSpec: codebuild.BuildSpec.fromObject({
                                        version: '0.2',
                                        phases: {
                                            pre_build: {
                                                commands: [
                                                    'cd cdk',
                                                    'npm install',
                                                ],
                                            },
                                            build: {
                                                commands: [
                                                    'npm run build',
                                                    'npm run cdk synth ProdStack',
                                                ],
                                            },
                                        },
                                        artifacts: {
                                            'base-directory': 'cdk/cdk.out',
                                            files: '*',
                                        },
                                    }),
                                }),
                                outputs: [cdkBuildOutput],
                            }),
                        ],
                    },
                    {
                        stageName: 'Deploy',
                        actions: [
                            new cpactions.CloudFormationCreateUpdateStackAction({
                                actionName: 'CFN_Deploy',
                                adminPermissions: true,
                                stackName: 'YourStackName',
                                templatePath: cdkBuildOutput.atPath('ProdStack.template.json'),
                            }),
                            new cpactions.S3DeployAction({
                                actionName: 'S3_Deploy',
                                bucket: deployBucket,
                                input: spaBuildOutput,
                                runOrder: 2,
                            }),
                        ],
                    },
                ],
            });
        }
    }
}

const app = new App();

// for local testing
new MyStack(app, 'LocalStack', { local: true });

// stack containing the Pipeline, deployed manually
new MyStack(app, 'PipelineStack', { local: false });

// this Stack will only be deployed through the Pipeline
const prodStack = new Stack(app, 'ProdStack');
new SpaResources(prodStack, 'SpaResources');

app.synth();
0reactions
SomayaBcommented, Dec 16, 2019

Closing this issue for now until #3437 is merged. Feel free to reopen

Read more comments on GitHub >

github_iconTop Results From Across the Web

aws-cdk/aws-s3-deployment module - AWS Documentation
This library allows populating an S3 bucket with the contents of .zip files from other S3 buckets or from local disk. The following...
Read more >
Hosting a static Single Page Application on AWS using the CDK
A complete guide on hosting a SPA on AWS including automatic CI/CD ... A one time S3 deployment to put the static files...
Read more >
Deploying a SPA using aws-cdk (TypeScript)
An S3 bucket for hosting our static website. A Cloudfront Distribution that will act as a CDN. A Codebuild project that will trigger...
Read more >
Deploy React SPA with CodePipeline and CodeBuild using ...
Deploy React SPA with CodePipeline and CodeBuild using AWS CDK ... deploy a React SPA written in TypeScript to an Amazon S3 bucket...
Read more >
DEPLOY YOUR APPLICATION TO AWS USING CDK ...
For deploying your application to the cloud you will be using AWS CDK Pipelines. CDK Pipelines are a high level construct from CDK...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found