Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to deploy CDK app via Lambda #2637

Closed
kadishmal opened this issue May 24, 2019 · 11 comments
Closed

How to deploy CDK app via Lambda #2637

kadishmal opened this issue May 24, 2019 · 11 comments

Comments

@kadishmal
Copy link

kadishmal commented May 24, 2019

Hi,

I think this is an unsupported use case for CDK. I am trying to deploy the CDK app via Lambda. The goal is for the Lambda function to call cdk deploy and get the application, included together with the Lambda code, deployed.

Currently, calling cdk deploy via the Node's exec command fails due to missing AWS credentials. Ideally, the same role that is used to execute the Lambda function should be reused. In my case this function has all the permissions to deploy a CFN template that the underlying CDK generates.

I tried to extract the deployment related code out of the aws-cdk package and call it directly, but found out that it depends on the credential provider which tries to find the credentials either in the env variables or config files.

Is there a way to bypass this credentials check and let it just call the APIs to do the job?

Thanks.

@rix0rrr
Copy link
Contributor

rix0rrr commented May 27, 2019

Since Lambda provides SDK credentials via environment variables, I'm not entirely sure why it would fail. Furthermore:

We'd be happy to help you out, but GitHub issues is not a support forum. There are websites much better suited for that, such as StackOverflow. If you could please ask that same question again on StackOverflow and paste the link to it here, we will answer it for you over there.

I will now close this issue, feel free to reopen when pasting the StackOverflow link.

@rix0rrr rix0rrr closed this as completed May 27, 2019
@kadishmal
Copy link
Author

kadishmal commented May 27, 2019

AFAIK, Lambda doesn't provide SDK credentials. The recommended way is to add proper permissions to the Lambda IAM role https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-lambda.html. Setting credentials via env variables is a security concern.

Considering the above, this issue is more of a limitation of CDK, not a question. So closing at will seems not caring about your customer which is one of your leadership principles.

Having said that, I figured out how to deploy a CDK app via Lambda. And, no, CDK will not work out of the box in Lambda. The following are the changes I had made locally and deployed to Lambda.

Overrode SDK class so that it doesn't require credentials for CloudFormation and S3 in https://github.com/awslabs/aws-cdk/blob/master/packages/aws-cdk/lib/api/util/sdk.ts#L95.

// LambdaSDK.ts
import { Environment } from '@aws-cdk/cx-api';
import { SDK } from 'aws-cdk/lib/api/util/sdk';
import * as CloudFormation from 'aws-sdk/clients/cloudformation';
import * as S3 from 'aws-sdk/clients/s3';

export default class LambdaSDK extends SDK {
  async cloudFormation(environment: Environment) {
    return new CloudFormation({
      region: environment.region,
    });
  }
  async s3(environment: Environment) {
    return new S3({
      region: environment.region
    });
  }
}

Here is how I used LambdaSDK in the Lambda handler:

import { Context } from 'aws-lambda';
import { config } from 'aws-sdk';
import { CloudFormationDeploymentTarget, DEFAULT_TOOLKIT_STACK_NAME } from 'aws-cdk/lib/api/deployment-target';
import { CdkToolkit } from 'aws-cdk/lib/cdk-toolkit';
import { AppStacks } from 'aws-cdk/lib/api/cxapp/stacks';
import { Configuration } from 'aws-cdk/lib/settings';
import { execProgram } from "aws-cdk/lib/api/cxapp/exec";
import { parseRenames } from "aws-cdk/lib/renames";
import * as yargs from 'yargs';
import { RequireApproval } from "aws-cdk/lib/diff";
import { DISPLAY_VERSION } from "aws-cdk/lib/version";

import LambdaSDK from './LambdaSDK';

module.exports.handler = async (event: any, context: Context) => {
  const aws = new LambdaSDK();
  const argv = await parseCommandLineArguments();

  const configuration = new Configuration(argv);
  await configuration.load();
  const appStacks = new AppStacks({
    configuration,
    aws,
    synthesizer: execProgram,
    renames: parseRenames(argv.rename)
  });

  const provisioner = new CloudFormationDeploymentTarget({ aws });
  const cli = new CdkToolkit({ appStacks, provisioner });
  const toolkitStackName = configuration.settings.get(['toolkitStackName']) || DEFAULT_TOOLKIT_STACK_NAME;

  await cli.deploy({
    stackNames: [],
    exclusively: argv.exclusively as boolean,
    toolkitStackName,
    roleArn: argv.roleArn as string,
    requireApproval: configuration.settings.get(['requireApproval']),
    ci: argv.ci,
    reuseAssets: argv['build-exclude'] as string[]
  });
};

Copied over the same command line parsing function from cdk into the lambda handler script and removed all commands except deploy.

async function parseCommandLineArguments() {
  return yargs
    .env('CDK')
    .usage('Usage: cdk -a <cdk-app> COMMAND')
    .option('app', { type: 'string', alias: 'a', desc: 'REQUIRED: Command-line for executing your CDK app (e.g. "node bin/my-app.js")', requiresArg: true })
    .option('context', { type: 'array', alias: 'c', desc: 'Add contextual string parameter (KEY=VALUE)', nargs: 1, requiresArg: true })
    .option('plugin', { type: 'array', alias: 'p', desc: 'Name or path of a node package that extend the CDK features. Can be specified multiple times', nargs: 1 })
    .option('rename', { type: 'string', desc: 'Rename stack name if different from the one defined in the cloud executable ([ORIGINAL:]RENAMED)', requiresArg: true })
    .option('trace', { type: 'boolean', desc: 'Print trace for stack warnings' })
    .option('strict', { type: 'boolean', desc: 'Do not construct stacks with warnings' })
    .option('ignore-errors', { type: 'boolean', default: false, desc: 'Ignores synthesis errors, which will likely produce an invalid output' })
    .option('json', { type: 'boolean', alias: 'j', desc: 'Use JSON output instead of YAML', default: false })
    .option('verbose', { type: 'boolean', alias: 'v', desc: 'Show debug logs', default: false })
    .option('profile', { type: 'string', desc: 'Use the indicated AWS profile as the default environment', requiresArg: true })
    .option('proxy', { type: 'string', desc: 'Use the indicated proxy. Will read from HTTPS_PROXY environment variable if not specified.', requiresArg: true })
    .option('ec2creds', { type: 'boolean', alias: 'i', default: undefined, desc: 'Force trying to fetch EC2 instance credentials. Default: guess EC2 instance status.' })
    .option('version-reporting', { type: 'boolean', desc: 'Include the "AWS::CDK::Metadata" resource in synthesized templates (enabled by default)', default: undefined })
    .option('path-metadata', { type: 'boolean', desc: 'Include "aws:cdk:path" CloudFormation metadata for each resource (enabled by default)', default: true })
    .option('asset-metadata', { type: 'boolean', desc: 'Include "aws:asset:*" CloudFormation metadata for resources that user assets (enabled by default)', default: true })
    .option('role-arn', { type: 'string', alias: 'r', desc: 'ARN of Role to use when invoking CloudFormation', default: undefined, requiresArg: true })
    .option('toolkit-stack-name', { type: 'string', desc: 'The name of the CDK toolkit stack', requiresArg: true })
    .option('staging', { type: 'string', desc: 'directory name for staging assets (use --no-asset-staging to disable)', default: '.cdk.staging' })
    .command('deploy [STACKS..]', 'Deploys the stack(s) named STACKS into your AWS account', yargs => yargs
      .option('build-exclude', { type: 'array', alias: 'E', nargs: 1, desc: 'do not rebuild asset with the given ID. Can be specified multiple times.', default: [] })
      .option('exclusively', { type: 'boolean', alias: 'e', desc: 'only deploy requested stacks, don\'t include dependencies' })
      .option('require-approval', { type: 'string', choices: [RequireApproval.Never, RequireApproval.AnyChange, RequireApproval.Broadening], desc: 'what security-sensitive changes need manual approval' }))
    .option('ci', { type: 'boolean', desc: 'Force CI detection. Use --no-ci to disable CI autodetection.', default: process.env.CI !== undefined })
    .version(DISPLAY_VERSION)
    .demandCommand(1, '') // just print help
    .help()
    .alias('h', 'help')
    .epilogue([
      'If your app has a single stack, there is no need to specify the stack name',
      'If one of cdk.json or ~/.cdk.json exists, options specified there will be used as defaults. Settings in cdk.json take precedence.'
    ].join('\n\n'))
    .parse(['deploy', '--app', 'bin/LambdaStacksApp.js', '--staging', '/tmp', '--verbose', '--require-approval', 'never']);
}

Note the last parse() command I added:

    .parse(['deploy', '--app', 'bin/LambdaStacksApp.js', '--staging', '/tmp', '--verbose', '--require-approval', 'never']);

That's to make sure which app to use as well as override the staging directory since Lambda allows to write only to /tmp. Also, disable require-approval since the execution is not supervised.

Finally had to fix this line https://github.com/awslabs/aws-cdk/blob/master/packages/aws-cdk/lib/api/util/sdk.ts#L71:

const pkg = (require.main as any).require('../package.json');

When running from Lambda require.main is Lambda, meaning the above code will fail with:

{"errorMessage":"Cannot find module '../package.json'","errorType":"Error","stackTrace":[
"Function.Module._resolveFilename (module.js:547:15)",
"Function.Module._load (module.js:474:25)",
"Module.require (module.js:596:17)",
"new SDK (/var/task/node_modules/aws-cdk/lib/api/util/sdk.ts:71:39)",
"new LambdaSDK (/var/task/LambdaSDK.ts:10:1)",
"module.exports.handler (/var/task/scheduler.ts:24:15)",
"invoke (/var/runtime/node_modules/awslambda/index.js:288:20)",
"InvokeManager.start (/var/runtime/node_modules/awslambda/index.js:151:9)",
"Object.awslambda.waitForInvoke (/var/runtime/node_modules/awslambda/index.js:499:52)"
]}

The fixed code is:

const pkg = require('../../../package.json');

As a reminder, because I didn't appreciate your response, this was not a question but a report that a certain functionality is not supported. If you reopen, I will, perhaps, create a PR to make CDK work from within Lambda.

@john-tipper
Copy link

Hi @rix0rrr, as requested, here is a StackOverflow question relating to this issue. I think there's a valid use-case for wanting to have Lambdas able to call the CDK that is described in that question: https://stackoverflow.com/questions/58781821/is-it-possible-to-deploy-aws-cdk-stacks-from-within-a-lambda. It's to do with working around the limitation of AWS CodePipeline only supporting a single Git branch, so using a Lambda to create Pipelines dynamically in response to GitHub events indicating the creation or repos and branches.

It seems a bit dirty to me to have to get STS credentials via assume-role, somehow create some form of temporary CDK project structure with the appropriate CDK .ts files in it, then drop into exec() and call npm run build && cdk synth && cdk deploy with those STS credentials as env variables.

@kadishmal, please would you mind considering reopening this issue (unless you feel that there is a sensible workaround, in which case please would you mind letting me know what that workaround is)?

If I'm missing something and there is a simple solution then I'd be very happy to see it posted to that SO question, thanks!

@denizhoxha
Copy link

@kadishmal can you please provide an example how to provision CDK App from Lambda?

@MHacker9404
Copy link

MHacker9404 commented Sep 21, 2020

This issue needs to be reopened and resolved - if nothing else with an example. There are numerouse examples of Lambdas deploying CF templates in YAML or JSON. We need that same functionality, but from the CDK perspective - that a Lambda could deploy a CDK Stack construct.

This is a perfect example of what we should be able to achieve: Quickstart example

@vgulkevic
Copy link

@MHacker9404 @denizhoxha I created a repo with an example of how I put CDK into Lambda layer and used it with a Lambda to deploy and destroy a bucket. The size of lambda can become an issue as the maximum is 250mb.
https://github.com/vgulkevic/cdk_layer_in_lambda

@MHacker9404
Copy link

MHacker9404 commented Oct 24, 2020 via email

@mraszplewicz
Copy link

I have written an article about running AWS CDK inside a Lambda function. Maybe it will be useful for you: https://raszpel.medium.com/running-aws-cdk-from-a-lambda-function-9369d3daba57 or https://dev.to/mraszplewicz/running-aws-cdk-from-a-lambda-function-3502

And the Github repo: https://github.com/devopsbox-io/example-cdk-from-lambda/

@MHacker9404
Copy link

MHacker9404 commented Nov 12, 2020 via email

@hom-bahrani
Copy link
Contributor

Another example here https://github.com/imyoungyang/cdk-in-lambda

@wmarcuse
Copy link

I created this repository which uses a 5 minute approach with Gradle & Docker to install CDK in a targeted location and mounts the Lambda Layer zipfile on your local system which you can use directly to manually upload in the AWS console or use i.e. with CDK.

An example Lambda handler function with NodeJS runtime and with the layer attached can look something like:

exports.handler = async (event) => {
    const spawnSync = require('child_process').spawnSync;
    const process = spawnSync('cdk', ['--version'], {
        stdio: 'pipe',
        stderr: 'pipe'
    });
    console.log(process.status);
    console.log(process.stdout.toString());
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants