Skip to content

Commit

Permalink
feat: Add Lambda+ApiGateway+Codepipeline example. Fixes #267
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Gus El Khoury Seoane committed Apr 11, 2020
1 parent 71d87ca commit e158f16
Show file tree
Hide file tree
Showing 16 changed files with 397 additions and 1 deletion.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,6 @@ dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
Expand Down
9 changes: 9 additions & 0 deletions typescript/lambda-api-ci/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
*.js
!jest.config.js
*.d.ts
node_modules

# CDK asset staging directory
.cdk.staging
cdk.out
*.iml
6 changes: 6 additions & 0 deletions typescript/lambda-api-ci/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*.ts
!*.d.ts

# CDK asset staging directory
.cdk.staging
cdk.out
5 changes: 5 additions & 0 deletions typescript/lambda-api-ci/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"printWidth": 100,
"semi": false,
"tabWidth": 4
}
15 changes: 15 additions & 0 deletions typescript/lambda-api-ci/README.md
Original file line number Diff line number Diff line change
@@ -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"`
9 changes: 9 additions & 0 deletions typescript/lambda-api-ci/bin/ci.ts
Original file line number Diff line number Diff line change
@@ -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",
})
12 changes: 12 additions & 0 deletions typescript/lambda-api-ci/bin/lambda.ts
Original file line number Diff line number Diff line change
@@ -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,
})
17 changes: 17 additions & 0 deletions typescript/lambda-api-ci/buildspec.yml
Original file line number Diff line number Diff line change
@@ -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/**/*"
3 changes: 3 additions & 0 deletions typescript/lambda-api-ci/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"app": "npx ts-node bin/ci.ts"
}
9 changes: 9 additions & 0 deletions typescript/lambda-api-ci/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module.exports = {
"roots": [
"<rootDir>/test"
],
testMatch: [ '**/*.test.ts'],
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
}
118 changes: 118 additions & 0 deletions typescript/lambda-api-ci/lib/ci-stack.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
53 changes: 53 additions & 0 deletions typescript/lambda-api-ci/lib/lambda-api-stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
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, {}))
}
}
46 changes: 46 additions & 0 deletions typescript/lambda-api-ci/package.json
Original file line number Diff line number Diff line change
@@ -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": "1.31.0",
"@aws-cdk/core": "1.31.0",
"@aws-cdk/assert": "1.31.0",
"@aws-cdk/aws-apigateway": "1.31.0",
"@aws-cdk/aws-codebuild": "1.31.0",
"@aws-cdk/aws-codecommit": "1.31.0",
"@aws-cdk/aws-codepipeline": "1.31.0",
"@aws-cdk/aws-codepipeline-actions": "1.31.0",
"@aws-cdk/aws-cloudformation": "1.31.0",
"@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"
}
44 changes: 44 additions & 0 deletions typescript/lambda-api-ci/src/handler.ts
Original file line number Diff line number Diff line change
@@ -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 }
27 changes: 27 additions & 0 deletions typescript/lambda-api-ci/src/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
Loading

0 comments on commit e158f16

Please sign in to comment.