From 3b719fab76ebe61b45e2ea94a5f700e877a55408 Mon Sep 17 00:00:00 2001 From: Gus El Khoury Seoane Date: Wed, 30 Oct 2019 19:24:20 +0000 Subject: [PATCH] feat: Add Lambda+ApiGateway+Codepipeline example. Fixes #267 This commit adds an example that deploys a Lambda+ApiGateway setup using CodePipeline. Although there's plenty of examples of Lambda already, deploying them via CodePipeline had some non-trivial challenges to sort out. This could mean teams start using CDK and like it, but drop it because they can't adopt Full CI/CD easily when using it --- .gitignore | 1 - scripts/build-npm.sh | 2 +- typescript/lambda-api-ci/.gitignore | 9 ++ typescript/lambda-api-ci/.npmignore | 6 + typescript/lambda-api-ci/.prettierrc | 5 + typescript/lambda-api-ci/README.md | 15 +++ typescript/lambda-api-ci/bin/ci.ts | 9 ++ typescript/lambda-api-ci/bin/lambda.ts | 12 ++ typescript/lambda-api-ci/buildspec.yml | 17 +++ typescript/lambda-api-ci/cdk.json | 3 + typescript/lambda-api-ci/jest.config.js | 9 ++ typescript/lambda-api-ci/lib/ci-stack.ts | 118 ++++++++++++++++++ .../lambda-api-ci/lib/lambda-api-stack.ts | 49 ++++++++ typescript/lambda-api-ci/package.json | 46 +++++++ typescript/lambda-api-ci/src/handler.ts | 44 +++++++ typescript/lambda-api-ci/src/package.json | 27 ++++ typescript/lambda-api-ci/tsconfig.json | 24 ++++ 17 files changed, 394 insertions(+), 2 deletions(-) create mode 100644 typescript/lambda-api-ci/.gitignore create mode 100644 typescript/lambda-api-ci/.npmignore create mode 100644 typescript/lambda-api-ci/.prettierrc create mode 100644 typescript/lambda-api-ci/README.md create mode 100644 typescript/lambda-api-ci/bin/ci.ts create mode 100644 typescript/lambda-api-ci/bin/lambda.ts create mode 100644 typescript/lambda-api-ci/buildspec.yml create mode 100644 typescript/lambda-api-ci/cdk.json create mode 100644 typescript/lambda-api-ci/jest.config.js create mode 100644 typescript/lambda-api-ci/lib/ci-stack.ts create mode 100644 typescript/lambda-api-ci/lib/lambda-api-stack.ts create mode 100644 typescript/lambda-api-ci/package.json create mode 100644 typescript/lambda-api-ci/src/handler.ts create mode 100644 typescript/lambda-api-ci/src/package.json create mode 100644 typescript/lambda-api-ci/tsconfig.json diff --git a/.gitignore b/.gitignore index 64514846e..ac383d313 100644 --- a/.gitignore +++ b/.gitignore @@ -233,7 +233,6 @@ dist/ downloads/ eggs/ .eggs/ -lib/ lib64/ parts/ sdist/ diff --git a/scripts/build-npm.sh b/scripts/build-npm.sh index 3c619d38b..0c85aa9ea 100755 --- a/scripts/build-npm.sh +++ b/scripts/build-npm.sh @@ -20,7 +20,7 @@ verify_star_dependencies() { } # Find and build all NPM projects -for pkgJson in $(find typescript -name package.json | grep -v node_modules); do +for pkgJson in $(find typescript -name cdk.json | grep -v node_modules); do ( echo "==============================" echo "building project: $(dirname $pkgJson)" diff --git a/typescript/lambda-api-ci/.gitignore b/typescript/lambda-api-ci/.gitignore new file mode 100644 index 000000000..feb324cb0 --- /dev/null +++ b/typescript/lambda-api-ci/.gitignore @@ -0,0 +1,9 @@ +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out +*.iml diff --git a/typescript/lambda-api-ci/.npmignore b/typescript/lambda-api-ci/.npmignore new file mode 100644 index 000000000..c1d6d45dc --- /dev/null +++ b/typescript/lambda-api-ci/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/typescript/lambda-api-ci/.prettierrc b/typescript/lambda-api-ci/.prettierrc new file mode 100644 index 000000000..0811454b0 --- /dev/null +++ b/typescript/lambda-api-ci/.prettierrc @@ -0,0 +1,5 @@ +{ + "printWidth": 100, + "semi": false, + "tabWidth": 4 +} diff --git a/typescript/lambda-api-ci/README.md b/typescript/lambda-api-ci/README.md new file mode 100644 index 000000000..2c35fe401 --- /dev/null +++ b/typescript/lambda-api-ci/README.md @@ -0,0 +1,15 @@ +# What's this? +This is code sample that uses CDK to: +* Create a Lambda function that can be invoked using API Gateway +* Create a CI using CodeSuite that deploys the Lambda+ApiGateway resources using `cdk deploy` + +# How do I start using it? +* Ensure you've followed the [guide to Getting Started to AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html), and you have CDK installed, and the AWS SDK installed and credentials configured. +* [Bootstrap your AWS environment](https://docs.aws.amazon.com/cdk/latest/guide/serverless_example.html#serverless_example_deploy_and_test) +* Create a CodeCommit repository. See [this documentation](https://docs.aws.amazon.com/codecommit/latest/userguide/how-to-create-repository.html) for help. +* Place the contents of this folder inside it +* Set the repository name in the `repositoryName` prop in `bin/ci.ts`. +* Build the stack with `npm run build` +* Deploy the CI stack with `cdk deploy` +* `Todo` summarize permissions + * If you'd like to deploy just the Lambda+ApiGateway stack, you can do so with `cdk deploy -a "npx ts-node bin/lambda.ts"` \ No newline at end of file diff --git a/typescript/lambda-api-ci/bin/ci.ts b/typescript/lambda-api-ci/bin/ci.ts new file mode 100644 index 000000000..2b4ea382c --- /dev/null +++ b/typescript/lambda-api-ci/bin/ci.ts @@ -0,0 +1,9 @@ +#!/usr/bin/env node +import "source-map-support/register" +import cdk = require("@aws-cdk/core") +import { CIStack } from "../lib/ci-stack" + +const app = new cdk.App() +new CIStack(app, "CDKExampleLambdaApiCIStack", { + repositoryName: "lambda-api-ci", +}) diff --git a/typescript/lambda-api-ci/bin/lambda.ts b/typescript/lambda-api-ci/bin/lambda.ts new file mode 100644 index 000000000..8b55632ff --- /dev/null +++ b/typescript/lambda-api-ci/bin/lambda.ts @@ -0,0 +1,12 @@ +#!/usr/bin/env node +import "source-map-support/register" +import cdk = require("@aws-cdk/core") +import { CDKExampleLambdaApiStack } from "../lib/lambda-api-stack" + +export const lambdaApiStackName = "CDKExampleLambdaApiStack" +export const lambdaFunctionName = "CDKExampleWidgetStoreFunction" + +const app = new cdk.App() +new CDKExampleLambdaApiStack(app, lambdaApiStackName, { + functionName: lambdaFunctionName, +}) diff --git a/typescript/lambda-api-ci/buildspec.yml b/typescript/lambda-api-ci/buildspec.yml new file mode 100644 index 000000000..3fee1642e --- /dev/null +++ b/typescript/lambda-api-ci/buildspec.yml @@ -0,0 +1,17 @@ + +version: 0.2 + +phases: + install: + runtime-versions: + nodejs: 12 + commands: + - npm install + + build: + commands: + - npm run build + - npm run -- cdk deploy --ci --require-approval never -a "npx ts-node bin/lambda.ts" +artifacts: + files: + - "cdk.out/**/*" diff --git a/typescript/lambda-api-ci/cdk.json b/typescript/lambda-api-ci/cdk.json new file mode 100644 index 000000000..14e564c88 --- /dev/null +++ b/typescript/lambda-api-ci/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "npx ts-node bin/ci.ts" +} diff --git a/typescript/lambda-api-ci/jest.config.js b/typescript/lambda-api-ci/jest.config.js new file mode 100644 index 000000000..9aa0e8d42 --- /dev/null +++ b/typescript/lambda-api-ci/jest.config.js @@ -0,0 +1,9 @@ +module.exports = { + "roots": [ + "/test" + ], + testMatch: [ '**/*.test.ts'], + "transform": { + "^.+\\.tsx?$": "ts-jest" + }, + } diff --git a/typescript/lambda-api-ci/lib/ci-stack.ts b/typescript/lambda-api-ci/lib/ci-stack.ts new file mode 100644 index 000000000..740562fe7 --- /dev/null +++ b/typescript/lambda-api-ci/lib/ci-stack.ts @@ -0,0 +1,118 @@ +import { CodeCommitSourceAction, CodeBuildAction } from "@aws-cdk/aws-codepipeline-actions" +import { PolicyStatement } from "@aws-cdk/aws-iam" +import { Construct, Stack, StackProps } from "@aws-cdk/core" +import { PipelineProject, LinuxBuildImage } from "@aws-cdk/aws-codebuild" +import { Artifact, Pipeline } from "@aws-cdk/aws-codepipeline" +import { Repository } from "@aws-cdk/aws-codecommit" +import { lambdaApiStackName, lambdaFunctionName } from "../bin/lambda" + +interface CIStackProps extends StackProps { + repositoryName: string +} + +export class CIStack extends Stack { + constructor(scope: Construct, name: string, props: CIStackProps) { + super(scope, name, props) + + const pipeline = new Pipeline(this, "Pipeline", {}) + + const repo = Repository.fromRepositoryName( + this, + "WidgetsServiceRepository", + props.repositoryName + ) + const sourceOutput = new Artifact("SourceOutput") + const sourceAction = new CodeCommitSourceAction({ + actionName: "CodeCommit", + repository: repo, + output: sourceOutput, + }) + pipeline.addStage({ + stageName: "Source", + actions: [sourceAction], + }) + + this.createBuildStage(pipeline, sourceOutput) + } + + private createBuildStage(pipeline: Pipeline, sourceOutput: Artifact) { + const project = new PipelineProject(this, `BuildProject`, { + environment: { + buildImage: LinuxBuildImage.STANDARD_3_0, + }, + }) + + const cdkDeployPolicy = new PolicyStatement() + cdkDeployPolicy.addActions( + "cloudformation:GetTemplate", + "cloudformation:CreateChangeSet", + "cloudformation:DescribeChangeSet", + "cloudformation:ExecuteChangeSet", + "cloudformation:DescribeStackEvents", + "cloudformation:DeleteChangeSet", + "cloudformation:DescribeStacks", + "s3:*Object", + "s3:ListBucket", + "s3:getBucketLocation", + "lambda:UpdateFunctionCode", + "lambda:GetFunction", + "lambda:CreateFunction", + "lambda:DeleteFunction", + "lambda:GetFunctionConfiguration", + "lambda:AddPermission", + "lambda:RemovePermission" + ) + cdkDeployPolicy.addResources( + this.formatArn({ + service: "cloudformation", + resource: "stack", + resourceName: "CDKToolkit/*", + }), + this.formatArn({ + service: "cloudformation", + resource: "stack", + resourceName: `${lambdaApiStackName}/*`, + }), + this.formatArn({ + service: "lambda", + resource: "function", + sep: ":", + resourceName: lambdaFunctionName, + }), + "arn:aws:s3:::cdktoolkit-stagingbucket-*" + ) + const editOrCreateLambdaDependencies = new PolicyStatement() + editOrCreateLambdaDependencies.addActions( + "iam:GetRole", + "iam:PassRole", + "iam:CreateRole", + "iam:AttachRolePolicy", + "iam:PutRolePolicy", + "apigateway:GET", + "apigateway:DELETE", + "apigateway:PUT", + "apigateway:POST", + "apigateway:PATCH", + "s3:CreateBucket", + "s3:PutBucketTagging" + ) + editOrCreateLambdaDependencies.addResources("*") + project.addToRolePolicy(cdkDeployPolicy) + project.addToRolePolicy(editOrCreateLambdaDependencies) + + const buildOutput = new Artifact(`BuildOutput`) + const buildAction = new CodeBuildAction({ + actionName: `Build`, + project, + input: sourceOutput, + outputs: [buildOutput], + }) + + pipeline.addStage({ + stageName: "build", + actions: [buildAction], + }) + + return buildOutput + } +} diff --git a/typescript/lambda-api-ci/lib/lambda-api-stack.ts b/typescript/lambda-api-ci/lib/lambda-api-stack.ts new file mode 100644 index 000000000..8c240090f --- /dev/null +++ b/typescript/lambda-api-ci/lib/lambda-api-stack.ts @@ -0,0 +1,49 @@ +import { LambdaIntegration, MethodLoggingLevel, RestApi } from "@aws-cdk/aws-apigateway" +import { PolicyStatement } from "@aws-cdk/aws-iam" +import { Function, Runtime, AssetCode, Code } from "@aws-cdk/aws-lambda" +import { Construct, Duration, Stack, StackProps } from "@aws-cdk/core" +import s3 = require("@aws-cdk/aws-s3") + +interface LambdaApiStackProps extends StackProps { + functionName: string +} + +export class CDKExampleLambdaApiStack extends Stack { + private restApi: RestApi + private lambdaFunction: Function + private bucket: s3.Bucket + + constructor(scope: Construct, id: string, props: LambdaApiStackProps) { + super(scope, id, props) + + this.bucket = new s3.Bucket(this, "WidgetStore") + + this.restApi = new RestApi(this, this.stackName + "RestApi", { + deployOptions: { + stageName: "beta", + metricsEnabled: true, + loggingLevel: MethodLoggingLevel.INFO, + dataTraceEnabled: true, + }, + }) + + const lambdaPolicy = new PolicyStatement() + lambdaPolicy.addActions("s3:ListBucket") + lambdaPolicy.addResources(this.bucket.bucketArn) + + this.lambdaFunction = new Function(this, props.functionName, { + functionName: props.functionName, + handler: "handler.handler", + runtime: Runtime.NODEJS_10_X, + code: new AssetCode(`./src`), + memorySize: 512, + timeout: Duration.seconds(10), + environment: { + BUCKET: this.bucket.bucketName, + }, + initialPolicy: [lambdaPolicy], + }) + + this.restApi.root.addMethod("GET", new LambdaIntegration(this.lambdaFunction, {})) + } +} diff --git a/typescript/lambda-api-ci/package.json b/typescript/lambda-api-ci/package.json new file mode 100644 index 000000000..decb22a7d --- /dev/null +++ b/typescript/lambda-api-ci/package.json @@ -0,0 +1,46 @@ +{ + "name": "lambda-api-ci", + "version": "0.1.0", + "bin": { + "lambda-api-ci": "bin/lambda-api-ci.js" + }, + "scripts": { + "build": "npm run prettier && tsc && npm run build-lambda", + "build-lambda": "cd src && npm run build", + "watch": "tsc -w", + "test": "jest", + "cdk": "cdk", + "prettier": "prettier --write '**/{bin,lib,src,tst}/*.ts'" + }, + "devDependencies": { + "aws-cdk": "*", + "@aws-cdk/core": "*", + "@aws-cdk/assert": "*", + "@aws-cdk/aws-apigateway": "*", + "@aws-cdk/aws-codebuild": "*", + "@aws-cdk/aws-codecommit": "*", + "@aws-cdk/aws-codepipeline": "*", + "@aws-cdk/aws-codepipeline-actions": "*", + "@aws-cdk/aws-cloudformation": "*", + "@types/jest": "^24.0.18", + "@types/node": "^13.7.0", + "jest": "^24.9.0", + "ts-jest": "^24.0.2", + "ts-node": "^8.1.0", + "typescript": "^3.8.3", + "prettier": "^2.0.4" + }, + "dependencies": { + "aws-sdk": "^2.617.0", + "source-map-support": "^0.5.9" + }, + "description": "* `npm run build` compile typescript to js * `npm run watch` watch for changes and compile * `npm run test` perform the jest unit tests * `cdk deploy` deploy this stack to your default AWS account/region * `cdk diff` compare deployed stack with current state * `cdk synth` emits the synthesized CloudFormation template", + "main": "jest.config.js", + "directories": { + "lib": "lib", + "test": "test" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/typescript/lambda-api-ci/src/handler.ts b/typescript/lambda-api-ci/src/handler.ts new file mode 100644 index 000000000..57428bcfe --- /dev/null +++ b/typescript/lambda-api-ci/src/handler.ts @@ -0,0 +1,44 @@ +import { S3 } from "aws-sdk" + +const bucketName = process.env.BUCKET! + +// From https://docs.aws.amazon.com/cdk/latest/guide/serverless_example.html +const handler = async function (event: any, context: any) { + const S3Client = new S3() + + try { + var method = event.httpMethod + + if (method === "GET") { + if (event.path === "/") { + const data = await S3Client.listObjectsV2({ Bucket: bucketName }).promise() + var body = { + widgets: data.Contents!.map(function (e) { + return e.Key + }), + } + return { + statusCode: 200, + headers: {}, + body: JSON.stringify(body), + } + } + } + + // We only accept GET for now + return { + statusCode: 400, + headers: {}, + body: "We only accept GET /", + } + } catch (error) { + const body = error.stack || JSON.stringify(error, null, 2) + return { + statusCode: 400, + headers: {}, + body: JSON.stringify(body), + } + } +} + +export { handler } diff --git a/typescript/lambda-api-ci/src/package.json b/typescript/lambda-api-ci/src/package.json new file mode 100644 index 000000000..6ba2209f0 --- /dev/null +++ b/typescript/lambda-api-ci/src/package.json @@ -0,0 +1,27 @@ +{ + "name": "widgets-lambda", + "version": "1.0.0", + "description": "A simple Lambda function to fetch widgets. See https://docs.aws.amazon.com/cdk/latest/guide/serverless_example.html", + "main": "handler.js", + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest" + }, + "devDependencies": { + "@types/node": "^13.7.0", + "jest": "^24.9.0", + "ts-jest": "^24.0.2", + "ts-node": "^8.1.0", + "typescript": "~3.8.3" + }, + "dependencies": { + "aws-sdk": "^2.617.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/awsdocs/aws-cdk-guide/blob/master/doc_source/serverless_example.md" + }, + "author": "", + "license": "ISC" +} diff --git a/typescript/lambda-api-ci/tsconfig.json b/typescript/lambda-api-ci/tsconfig.json new file mode 100644 index 000000000..63df946e4 --- /dev/null +++ b/typescript/lambda-api-ci/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target":"ES2018", + "module": "commonjs", + "lib": ["es2016", "es2017.object", "es2017.string"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization":false, + "typeRoots": ["./node_modules/@types"], + "allowSyntheticDefaultImports": true + }, + "exclude": ["cdk.out"] +}