Aws-cdk: (core): BundlingDockerImage.cp() needs to be explained more in the README

Created on 7 Dec 2020  路  13Comments  路  Source: aws/aws-cdk

When using lambda.Code.fromAsset and cdk.BundlingDockerImage.fromAsset together, synth fails to find anything in \asset-output

Reproduction Steps

  1. Create a Dockerfile that compiles and copies files to an /asset-output directory
FROM python:3.7-slim
COPY . /asset-input
COPY . /asset-output
WORKDIR /asset-input
RUN apt-get update && apt-get -y install curl make automake gcc g++ subversion python3-dev
RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python3 -
ENV PATH "/root/.poetry/bin:/opt/venv/bin:${PATH}"
RUN poetry export -f requirements.txt -o requirements.txt
RUN pip3 install -r requirements.txt -t /asset-output
  1. Use the following snippet when creating the lambda using cdk:
code: lambda.Code.fromAsset(PROJECT_DIR, {
        bundling: {
          image: cdk.BundlingDockerImage.fromAsset(PROJECT_DIR)
        }
      }),
  1. Run tsc && cdk synth -o cdk.out

What did you expect to happen?

Docker should find the compiled assets in /asset-output

What actually happened?

Error: Bundling did not produce any output. Check that content is written to /asset-output.

Environment

  • CDK CLI Version : 1.75.0
  • Framework Version: 1.75.0
  • Node.js Version: v15.3.0
  • OS : Mac Catalina 10.15.7
  • Language (Version): TypeScript 4.1.2

Other

If I use an implementation of ILocalBundling that is mostly copied from asset-staging.ts but calls both run and cp the synth works but I don't believe that should be necessary:

class LocalBundling implements ILocalBundling {
  tryBundle(outputDir: string, options: BundlingOptions): boolean {

    let user: string;
    if (options.user) {
      user = options.user;
    } else {
      // Default to current user
      const userInfo = os.userInfo();
      user =
        userInfo.uid !== -1 // uid is -1 on Windows
          ? `${userInfo.uid}:${userInfo.gid}`
          : "1000:1000";
    }

    // Always mount input and output dir
    const volumes = [
      {
        hostPath: PROJECT_DIR, // this.sourcePath
        containerPath: AssetStaging.BUNDLING_INPUT_DIR,
      },
      {
        hostPath: outputDir, // bundleDir
        containerPath: AssetStaging.BUNDLING_OUTPUT_DIR ?? outputDir,
      },
      ...(options.volumes ?? []),
    ];

    options.image.run({
      command: options.command,
      user,
      volumes,
      environment: options.environment,
      workingDirectory:
        options.workingDirectory ?? AssetStaging.BUNDLING_INPUT_DIR,
    });

    options.image.cp(AssetStaging.BUNDLING_OUTPUT_DIR ?? outputDir, outputDir);

    return true;
  }
}

This is :bug: Bug Report

@aws-cdcore bug docinline documentation efforsmall good first issue p1

All 13 comments

I think that what you are looking for is BundlingDockerImage.cp() (introduced in https://github.com/aws/aws-cdk/pull/9728).

After building their own Docker images, users can more easily run the image or copy files out of the image to create their own assets without using the bundling mechanism.

https://github.com/aws/aws-cdk/blob/cbe7a10053ce0e4e766f360cf8792f0b46c565f0/packages/%40aws-cdk/core/lib/bundling.ts#L178-L181

/asset-input and /asset-output (with mounted volumes) works when running the container not when building it.

So what is the appropriate use of the default bundling behavior? I would assume the snippet in step 2 above would work without needing to implement ILocalBundling to use the cp method.

So what is the appropriate use of the default bundling behavior?

The default behavior is to run a command in a container where the source path is mounted at /asset-input and during the execution it should put content at /asset-output. You can see examples here:

Gotcha, so anything put in /asset-output by the Dockerfile will be cleared out and it is assumed that the command will be the one to transfer the required bundling files?

Gotcha, so anything put in /asset-output by the Dockerfile will be cleared out

anything put in /asset-output when the container runs will be used as the final CDK asset.

and it is assumed that the command will be the one to transfer the required bundling files?

yes, you can do whatever you want as long as you put content in /asset-output at some point, you have access to the original asset in /asset-input. A trivial example would be cp -R /asset-input/* /asset-output.

I also ran into a situation where I just wanted to use some content from the built image as the asset output. I think our APIs can probably offer a better experience for this.

  1. In this case the asset input is meaningless.
  2. Ideally docker cp will be much faster to extract files from the built image as oppose to running a command inside the image.

@jogold what do you think?

You can already do this:

const assetPath = '/path/to/my/asset';

const image = cdk.BundlingDockerImage.fromAsset('/path/to/docker');

image.cp('/path/in/the/image', assetPath);

new lambda.Function(this, 'Fn', {
  code: lambda.Code.fromAsset(assetPath),
  runtime: lambda.Runtime.NODEJS_12_X,
  handler: 'index.handler',
});

Is it this that you want to improve?

docker cp is of course faster but has different use cases.

My issue was that the Dockerfile put everything needed in /asset-output but when the container ran that folder was empty. I am now putting everything in a folder named /asset-stage and passing cp -r ../asset-stage ../asset-output to copy everything from stage to output.

What I would like to see improved is either better documentation around the behavior of the asset-output folder or just simply taking what's already in there instead of wiping it before bundling.

const image = BundlingDockerImage.fromAsset('/path/to/docker');

new lambda.Function(this, 'Fn', {
  code: lambda.Code.fromAsset(image.fetch('/path/in/the/image')),
  runtime: lambda.Runtime.NODEJS_12_X,
  handler: 'index.handler',
});

I think the API for BundlingDockerImage can be improved:

const image = Docker.build('/path/to/docker');
const tmpdir = image.cp('/path/in/the/image');
// alternatively, users can specify the destination for "cp"
image.cp('/path/in/the/image', tmpdir);

And then, we can also add something like:

new lambda.Function(this, 'Fn', {
  code: lambda.Code.fromDockerBuildAsset('/path/in/the/image'),
  runtime: lambda.Runtime.NODEJS_12_X,
  handler: 'index.handler',
});

@eladb

error JSII5016: Members cannot be named "build" as it conflicts with synthetic declarations in some languages.

Haha... so perhaps Docker.fromBuild()?

Haha... so perhaps Docker.fromBuild()?

yes

And then, we can also add something like:

new lambda.Function(this, 'Fn', {
  code: lambda.Code.fromDockerBuildAsset('/path/in/the/image'),
  runtime: lambda.Runtime.NODEJS_12_X,
  handler: 'index.handler',
});

shouldn't this be lambda.Code.fromDockerBuildAsset('/path/to/docker', buildOptions) and the asset is supposed to be located at /asset in the image?

/**
 * Loads the function code from an asset created by a Docker build.
 *
 * The asset is expected to be located at `/asset` in the image.
 *
 * @param path The path to the directory containing the Docker file
 * @param options Docker build options
 */
public static fromDockerBuildAsset(path: string, options: cdk.DockerBuildOptions = {}): AssetCode {
  const assetPath = cdk.DockerImage.fromBuild(path, options).cp('/asset');
  return new AssetCode(assetPath);
}
Was this page helpful?
0 / 5 - 0 ratings