Pablo Fernandez
Pablo Fernandez

Reputation: 287500

When I add this BucketDeployment to my CDK CodePipeline, cdk synth never finishes

I'm trying to use CDK and CodePipeline to build and deploy a React application to S3. After the CodePipeline phase, in my own stack, I defined the S3 bucket like this:

const bucket = new Bucket(this, "Bucket", {
    websiteIndexDocument: "index.html",
    websiteErrorDocument: "error.html",
})

which worked. And then I defined the deployment of my built React app like this:

new BucketDeployment(this, "WebsiteDeployment", {
    sources: [Source.asset("./")],
    destinationBucket: bucket
})

which doesn't seem to work. Is that use of BucketDeployment correct?

Something odd that happens when I add the BucketDeployment lines is that cdk synth or cdk deploy, they never finish and they seem to generate an infinite recursive tree in cdk.out, so something definitely seems wrong there.

And if I change to Source.asset("./build") I get the error:

> cdk synth
C:\Users\pupeno\Code\ww3fe\node_modules\aws-cdk-lib\core\lib\asset-staging.ts:109
      throw new Error(`Cannot find asset at ${this.sourcePath}`);
            ^
Error: Cannot find asset at C:\Users\pupeno\Code\ww3fe\build
    at new AssetStaging (C:\Users\pupeno\Code\ww3fe\node_modules\aws-cdk-lib\core\lib\asset-staging.ts:109:13)
    at new Asset (C:\Users\pupeno\Code\ww3fe\node_modules\aws-cdk-lib\aws-s3-assets\lib\asset.ts:72:21)
    at Object.bind (C:\Users\pupeno\Code\ww3fe\node_modules\aws-cdk-lib\aws-s3-deployment\lib\source.ts:55:23)
    at C:\Users\pupeno\Code\ww3fe\node_modules\aws-cdk-lib\aws-s3-deployment\lib\bucket-deployment.ts:170:83
    at Array.map (<anonymous>)
    at new BucketDeployment (C:\Users\pupeno\Code\ww3fe\node_modules\aws-cdk-lib\aws-s3-deployment\lib\bucket-deployment.ts:170:51)
    at new MainStack (C:\Users\pupeno\Code\ww3fe\infra\pipeline-stack.ts:16:9)
    at new DeployStage (C:\Users\pupeno\Code\ww3fe\infra\pipeline-stack.ts:28:26)
    at new PipelineStack (C:\Users\pupeno\Code\ww3fe\infra\pipeline-stack.ts:56:24)
    at Object.<anonymous> (C:\Users\pupeno\Code\ww3fe\infra\pipeline.ts:6:1)
Subprocess exited with error 1

which would indicate this is very wrong. Why is it searching for the build directory on my machine? It's supposed to search for it in CodePipeline, after building.

My whole pipeline is:

import {Construct} from "constructs"
import {CodeBuildStep, CodePipeline, CodePipelineSource} from "aws-cdk-lib/pipelines"
import {Stage, CfnOutput, StageProps, Stack, StackProps} from "aws-cdk-lib"
import {Bucket} from "aws-cdk-lib/aws-s3"
import {BucketDeployment, Source} from "aws-cdk-lib/aws-s3-deployment"

export class MainStack extends Stack {
    constructor(scope: Construct, id: string, props?: StageProps) {
        super(scope, id, props)

        const bucket = new Bucket(this, "Bucket", {
            websiteIndexDocument: "index.html",
            websiteErrorDocument: "error.html",
        })

        new BucketDeployment(this, "WebsiteDeployment", {
            sources: [Source.asset("./")],
            destinationBucket: bucket
        })

        new CfnOutput(this, "BucketOutput", {value: bucket.bucketArn})
    }
}

export class DeployStage extends Stage {
    public readonly mainStack: MainStack

    constructor(scope: Construct, id: string, props?: StageProps) {
        super(scope, id, props)
        this.mainStack = new MainStack(this, "example")
    }
}

export class PipelineStack extends Stack {
    constructor(scope: Construct, id: string, props?: StackProps) {
        super(scope, id, props)

        const pipeline = new CodePipeline(this, id, {
            pipelineName: id,
            synth: new CodeBuildStep("Synth", {
                    input: CodePipelineSource.connection("user/example", "main", {
                        connectionArn: "arn:aws:codestar-connections:....",
                    }),
                    installCommands: [
                        "npm install -g aws-cdk"
                    ],
                    commands: [
                        "npm ci",
                        "npm run build",
                        "npx cdk synth"
                    ]
                }
            ),
        })

        const deploy = new DeployStage(this, "Staging")
        const deployStage = pipeline.addStage(deploy)
    }
}

The actual error I'm experiencing in AWS is this:

[Container] 2022/01/25 20:28:27 Phase complete: BUILD State: SUCCEEDED
[Container] 2022/01/25 20:28:27 Phase context status code:  Message: 
[Container] 2022/01/25 20:28:27 Entering phase POST_BUILD
[Container] 2022/01/25 20:28:27 Phase complete: POST_BUILD State: SUCCEEDED
[Container] 2022/01/25 20:28:27 Phase context status code:  Message: 
[Container] 2022/01/25 20:28:27 Expanding base directory path: cdk.out
[Container] 2022/01/25 20:28:27 Assembling file list
[Container] 2022/01/25 20:28:27 Expanding cdk.out
[Container] 2022/01/25 20:28:27 Skipping invalid file path cdk.out
[Container] 2022/01/25 20:28:27 Phase complete: UPLOAD_ARTIFACTS State: FAILED
[Container] 2022/01/25 20:28:27 Phase context status code: CLIENT_ERROR Message: no matching base directory path found for cdk.out

In case it matters, by cdk.json, in the root of my repo, contains:

{
  "app": "npx ts-node --project infra/tsconfig.json --prefer-ts-exts infra/pipeline.ts",
  "watch": {
    "include": [
      "**"
    ],
    "exclude": [
      "README.md",
      "cdk*.json",
      "**/*.d.ts",
      "**/*.js",
      "tsconfig.json",
      "package*.json",
      "yarn.lock",
      "node_modules",
      "test"
    ]
  },
  "context": {
    "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true,
    "@aws-cdk/core:stackRelativeExports": true,
    "@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
    "@aws-cdk/aws-lambda:recognizeVersionProps": true,
    "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true
  }
}

Upvotes: 4

Views: 4504

Answers (3)

Yahya
Yahya

Reputation: 51

Take a look in your gitignore file if some files like json or html files are not commited to be pushed into Repo.

Upvotes: 0

fedonev
fedonev

Reputation: 25669

TL;DR With two changes, the pipeline successfully deploys the React app: (1) Source.asset needs the full path to the build directory and (2) the React build commands need to be added to the synth step.

Give Source.asset the full path to the React build dir:

new BucketDeployment(this, "WebsiteDeployment", {
    sources: [Source.asset(path.join(__dirname, './build'))], // relative to the Stack dir
    destinationBucket: bucket
})

React build artifacts are typically .gitignored, so CodePipeline needs to build the React app. My version has a separate package.json for the React app, so the build step 1 needs a few more commands:

synth: new pipelines.CodeBuildStep('Synth', {
  commands: [
    // build react (new)
    'cd react-app',  // path from project root to React app package.json
    'npm ci',
    'npm run build',
    'cd ..',
    // synth cdk (as in OP)
    "npm ci",
    "npm run build",
    "npx cdk synth"  // synth must run AFTER the React build step
  ],

The React app deploys to a S3 URL:

// MainStack
new cdk.CfnOutput(this, 'WebsiteUrl', {
  value: `http://${this.websiteBucket.bucketName}.s3-website-${this.region}.amazonaws.com`,
});

(1) pipelines.CodePipeline is an opinionated construct for deploying Stacks. The lower-level codepipeline.Pipeline construct has features many apps will need, such as separating build steps and the passing build-time env vars between steps (e.g. injecting the API URL into the client bundle using a REACT_APP_API_URL env var).

Upvotes: 5

8888
8888

Reputation: 2064

For the first question:

And if I change to Source.asset("./build") I get the error: ... Why is it searching for the build directory on my machine?

This is happening when you run cdk synth locally. Remember, cdk synth will always reference the file system where this command is run. Locally it will be your machine, in the pipeline it will be in the container or environment that is being used by AWS CodePipeline.

Dig a little deeper into BucketDeployment
But also, there is some interesting things that happen here that could be helpful. BucketDeployment doesn't just pull from the source you reference in BucketDeployment.sources and upload it to the bucket you specify in BucketDeployment.destinationBucket. According to the BucketDeployment docs the assets are uploaded to an intermediary bucket and then later merged to your bucket. This matters because it will explain your error received Error: Cannot find asset at C:\Users\pupeno\Code\ww3fe\build because when you run cdk synth it will expect the dir ./build as stated in Source.asset("./build") to exist.

This gets really interesting when trying to use a CodePipeline to build and deploy a single page app like React in your case. By default, CodePipeline will execute a Source step, followed a Synth step, then any of the waves or stages you add after. Adding a wave that builds your react app won't work right away because we now see that the output directory of building you react app is needed during the Synth step because of how BucketDeployment works. We need to be able to have the order be Source -> Build -> Synth -> Deploy. As found in this question, we can control the order of the steps by using inputs and outputs. CodePipeline will order the steps to ensure input/output dependencies are met. So we need the have our Synth step use the Build's output as its input.

Concerns with the currently defined pipeline
I believe that your current pipeline is missing a CodeBuildStep that would bundle your react app and output it to the directory that you specified in BucketDeployment.sources. We also need to set the inputs to order these actions correctly. Below are some updates to the pipeline definition, though some changes may need to be made to have the correct file paths. Also, set BucketDeployment.sources to the dir where your app bundle is written to.

export class PipelineStack extends Stack {
    constructor(scope: Construct, id: string, props?: StackProps) {
        super(scope, id, props)

        const sourceAction = CodePipelineSource.connection("user/example", "main", {
            connectionArn: "arn:aws:codestar-connections:....",
        });

        const buildAction = new CodeBuildStep("Build", {
            input: sourceAction, // This places Source first
            installCommands: [ "npm ci" ],
            commands: [ "npm run build" ], // Change this to your build command for your react app
            // We need to output the entire contents of our file structure
            // this is both the CDK code needed for synth and the bundled app
            primaryOutputDirectory: "./",
        });

        const synthAction = new ShellStep("Synth", {
            input: buildAction, // This places Synth after Build
            installCommands: [
                "npm install -g aws-cdk"
            ],
            commands: [
                "npm ci",
                "npm run build",
                "npx cdk synth"
            ],
            // Synth step must output to cdk.out
            // if your CDK code is nested, this will have to match that structure
            primaryOutputDirectory: "./cdk.out",
          });

        const pipeline = new CodePipeline(this, id, {
            pipelineName: id,
            synth: synthAction,
        });

        const deploy = new DeployStage(this, "Staging");
        const deployStage = pipeline.addStage(deploy);
    }
}

Upvotes: 2

Related Questions