├── lib ├── index.ts ├── util.ts ├── builder.ts └── function.ts ├── .gitignore ├── .npmignore ├── .github └── workflows │ └── publish.yml ├── package.json └── README.md /lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './function'; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | tsconfig.json 3 | tslint.json 4 | *.js.map 5 | *.d.ts 6 | *.generated.ts 7 | dist 8 | lib/generated/resources.ts 9 | .jsii 10 | 11 | .LAST_BUILD 12 | .nyc_output 13 | coverage 14 | nyc.config.js 15 | .LAST_PACKAGE 16 | *.snk 17 | 18 | node_modules -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Don't include original .ts files when doing `npm pack` 2 | *.ts 3 | !*.d.ts 4 | coverage 5 | .nyc_output 6 | *.tgz 7 | 8 | dist 9 | example 10 | .LAST_PACKAGE 11 | .LAST_BUILD 12 | !*.js 13 | 14 | # Include .jsii 15 | !.jsii 16 | 17 | *.snk 18 | 19 | *.tsbuildinfo 20 | 21 | tsconfig.json -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Publish CDK packages 3 | 4 | on: 5 | push: 6 | tags: 7 | - "v*" 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Get the version 15 | id: get_version 16 | run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\/v/} 17 | 18 | - name: Checkout code 19 | uses: actions/checkout@v1 20 | with: 21 | fetch-depth: 1 22 | 23 | - name: Publish packages 24 | uses: udondan/jsii-publish@v0.8.3 25 | with: 26 | VERSION: ${{ steps.get_version.outputs.VERSION }} 27 | BUILD_SOURCE: true 28 | BUILD_PACKAGES: true 29 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 30 | PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} 31 | NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }} 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | GITHUB_REPOSITORY: ${{ github.repository }} -------------------------------------------------------------------------------- /lib/util.ts: -------------------------------------------------------------------------------- 1 | // From https://github.com/errwischt/stacktrace-parser/blob/master/src/stack-trace-parser.js 2 | const STACK_RE = /^\s*at (?:((?:\[object object\])?[^\\/]+(?: \[as \S+\])?) )?\(?(.*?):(\d+)(?::(\d+))?\)?\s*$/i; 3 | 4 | /** 5 | * A parsed stack trace line 6 | */ 7 | export interface StackTrace { 8 | readonly file: string; 9 | readonly methodName?: string; 10 | readonly lineNumber: number; 11 | readonly column: number; 12 | } 13 | 14 | /** 15 | * Parses the stack trace of an error 16 | */ 17 | export function parseStackTrace(error?: Error): StackTrace[] { 18 | const err = error || new Error(); 19 | 20 | if (!err.stack) { 21 | return []; 22 | } 23 | 24 | const lines = err.stack.split('\n'); 25 | 26 | const stackTrace: StackTrace[] = []; 27 | 28 | for (const line of lines) { 29 | const results = STACK_RE.exec(line); 30 | if (results) { 31 | stackTrace.push({ 32 | file: results[2], 33 | methodName: results[1], 34 | lineNumber: parseInt(results[3], 10), 35 | column: parseInt(results[4], 10), 36 | }); 37 | } 38 | } 39 | 40 | return stackTrace; 41 | } 42 | -------------------------------------------------------------------------------- /lib/builder.ts: -------------------------------------------------------------------------------- 1 | import { spawnSync } from 'child_process'; 2 | import * as path from 'path'; 3 | 4 | /** 5 | * Builder options 6 | */ 7 | export interface BuilderOptions { 8 | /** 9 | * Entry file 10 | */ 11 | readonly entry: string; 12 | 13 | /** 14 | * The output directory 15 | */ 16 | readonly outDir: string; 17 | 18 | /** 19 | * The output directory 20 | */ 21 | readonly buildCmd: string; 22 | 23 | /** 24 | * The handler name, also name of compiled file, defaults to `main` 25 | */ 26 | readonly handler: string; 27 | 28 | /** 29 | * Additional env variables 30 | */ 31 | readonly extraEnv: string; 32 | } 33 | 34 | /** 35 | * Builder 36 | */ 37 | export class Builder { 38 | private readonly entry: string; 39 | private readonly outDir: string; 40 | private readonly buildCmd: string; 41 | private readonly handler: string; 42 | private readonly extraEnv: any; 43 | 44 | constructor(options: BuilderOptions) { 45 | this.entry = options.entry; 46 | this.outDir = options.outDir; 47 | this.buildCmd = options.buildCmd; 48 | this.handler = options.handler; 49 | this.extraEnv = options.extraEnv; 50 | } 51 | 52 | public build(): void { 53 | try { 54 | const cmd = `${this.buildCmd} -o ${this.outDir}/${this.handler} ${this.entry}`; 55 | const cwd = path.dirname(this.entry); 56 | 57 | const goBuild = spawnSync(cmd, { 58 | env: { ...process.env, GOOS: 'linux', ...this.extraEnv }, 59 | shell: true, 60 | cwd, 61 | }); 62 | 63 | if (goBuild.error) { 64 | throw goBuild.error; 65 | } 66 | 67 | if (goBuild.status !== 0) { 68 | throw new Error(goBuild.stdout.toString().trim()); 69 | } 70 | } catch (err) { 71 | throw new Error(`Failed to compile Go function at ${this.entry}: ${err}`); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-lambda-golang", 3 | "version": "0.1.1", 4 | "description": "CDK Construct for AWS Lambda in Golang", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "scripts": { 8 | "build": "jsii", 9 | "build:watch": "jsii -w", 10 | "package": "jsii-pacmak", 11 | "clean": "find ./lib -name \"*.d.ts\" -type f -delete && find ./lib -name \"*.js\" -type f -delete && rm -fr ./dist && find ./example -name \"*.d.ts\" -type f -delete && find ./example -name \"*.js\" -type f -delete && rm -fr example/cdk.out" 12 | }, 13 | "keywords": [ 14 | "aws", 15 | "cdk", 16 | "constructs", 17 | "lambda", 18 | "golang" 19 | ], 20 | "author": { 21 | "name": "Rafal Wilinski", 22 | "email": "raf.wilinski@gmail.com", 23 | "url": "https://rwilinski.me" 24 | }, 25 | "jest": { 26 | "moduleFileExtensions": [ 27 | "js" 28 | ], 29 | "coverageThreshold": { 30 | "global": { 31 | "branches": 60, 32 | "statements": 80 33 | } 34 | }, 35 | "collectCoverage": true, 36 | "coverageReporters": [ 37 | "lcov", 38 | "html", 39 | "text-summary" 40 | ] 41 | }, 42 | "license": "Apache-2.0", 43 | "devDependencies": { 44 | "@aws-cdk/assert": "^1.43.0", 45 | "@types/node": "^14.0.10", 46 | "fs-extra": "^8.1.0", 47 | "jsii": "^1.6.0", 48 | "jsii-pacmak": "^1.6.0" 49 | }, 50 | "dependencies": { 51 | "@aws-cdk/aws-lambda": "^1.43.0", 52 | "@aws-cdk/core": "^1.43.0", 53 | "constructs": "^3.0.3" 54 | }, 55 | "homepage": "https://github.com/RafalWilinski/aws-lambda-golang-cdk", 56 | "repository": { 57 | "url": "https://github.com/RafalWilinski/aws-lambda-golang-cdk.git", 58 | "type": "git" 59 | }, 60 | "jsii": { 61 | "outdir": "dist", 62 | "targets": { 63 | "python": { 64 | "distName": "rwilinski.aws-lambda-golang", 65 | "module": "rwilinski.aws-lambda-golang" 66 | }, 67 | "java": { 68 | "package": "com.rwilinski.awslambdagolang", 69 | "maven": { 70 | "groupId": "com.rwilinski", 71 | "artifactId": "aws-lambda-golang" 72 | } 73 | }, 74 | "dotnet": { 75 | "namespace": "rwilinski.AwsLambdaGolang", 76 | "packageId": "rwilinski.GolangFunction" 77 | } 78 | } 79 | }, 80 | "peerDependencies": { 81 | "@aws-cdk/aws-lambda": "^1.43.0", 82 | "@aws-cdk/core": "^1.43.0", 83 | "constructs": "^3.0.3" 84 | }, 85 | "engines": { 86 | "node": ">= 10.3.0" 87 | }, 88 | "stability": "experimental", 89 | "awscdkio": { 90 | "announce": true, 91 | "twitter": "@RafalWilinski" 92 | } 93 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub version](https://badge.fury.io/gh/RafalWilinski%2Faws-lambda-golang-cdk.svg)](https://badge.fury.io/gh/RafalWilinski%2Faws-lambda-golang-cdk) 2 | [![npm version](https://badge.fury.io/js/aws-lambda-golang.svg)](https://badge.fury.io/js/aws-lambda-golang) 3 | [![NuGet version](https://badge.fury.io/nu/rwilinski.GolangFunction.svg)](https://badge.fury.io/nu/rwilinski.GolangFunction) 4 | [![PyPI version](https://badge.fury.io/py/rwilinski.aws-lambda-golang.svg)](https://badge.fury.io/py/rwilinski.aws-lambda-golang) 5 | 6 | ## Amazon Lambda Golang Construct 7 | 8 | This library provides constructs for Golang (Go 1.11 and 1.12 because of go modules) Lambda functions. 9 | 10 | [Why? Read this blogpost](https://www.rwilinski.me/blog/golang-and-cdk/) 11 | 12 | ### Installing 13 | In Typescript: 14 | 15 | ```sh 16 | npm i aws-lambda-golang --save 17 | # or using yarn 18 | yarn add aws-lambda-golang 19 | ``` 20 | 21 | In .NET: 22 | ```sh 23 | dotnet add package rwilinski.GolangFunction --version 0.1.0 24 | ``` 25 | 26 | In Python using Pip: 27 | ```sh 28 | pip install rwilinski.aws-lambda-golang 29 | ``` 30 | 31 | In Java using Maven, add this to `pom.xml`: 32 | ```xml 33 | 34 | com.rwilinski 35 | aws-lambda-golang 36 | 0.1.1 37 | 38 | ``` 39 | 40 | ### Usage 41 | In Typescript: 42 | 43 | ```ts 44 | import * as golang from 'aws-lambda-golang'; // Import aws-lambda-golang module 45 | import * as cdk from '@aws-cdk/core'; 46 | import * as apigateway from '@aws-cdk/aws-apigateway'; 47 | 48 | export class TestStackStack extends cdk.Stack { 49 | constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { 50 | super(scope, id, props); 51 | 52 | // Define function. Source code should be located in ./test-function/main.go 53 | const backend = new golang.GolangFunction(this, 'test-function'); 54 | const api = new apigateway.LambdaRestApi(this, 'myapi', { 55 | handler: backend, 56 | proxy: false, 57 | }); 58 | 59 | const items = api.root.addResource('items'); 60 | items.addMethod('GET'); 61 | } 62 | } 63 | ``` 64 | 65 | By default, the construct will use the name of the defining file and the construct's id to look 66 | up the entry file: 67 | ``` 68 | . 69 | ├── stack.ts # defines a 'GolangFunction' with 'my-handler' as id 70 | ├── stack/my-handler/main.go 71 | ├── stack/my-handler/go.mod 72 | ├── stack/my-handler/go.sum 73 | ``` 74 | 75 | The simplest Golang function (`stack/my-handler/main.go`): 76 | 77 | ```go 78 | package main 79 | 80 | import ( 81 | "fmt" 82 | "net/http" 83 | "github.com/aws/aws-lambda-go/events" 84 | "github.com/aws/aws-lambda-go/lambda" 85 | ) 86 | 87 | func handler(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 88 | fmt.Println("Received body: ", req.Body) 89 | 90 | return events.APIGatewayProxyResponse{ 91 | StatusCode: http.StatusOK, 92 | Body: "Hello from CDK GolangFunction!", 93 | }, nil 94 | } 95 | 96 | func main() { 97 | lambda.Start(handler) 98 | } 99 | ``` 100 | 101 | ### Configuring build 102 | 103 | The `GolangFunction` construct exposes some options via properties: `buildCmd`, `buildDir`, `entry` and `handler`, `extraEnv`. 104 | 105 | By default, your Golang code is compiled using `go build -ldflags="-s -w"` command with `GOOS=linux` env variable. 106 | 107 | ## 🤝 Contributing 108 | 109 | Contributions, issues and feature requests are welcome!
Feel free to check [issues page](https://github.com/RafalWilinski/aws-lambda-golang-cdk/issues). 110 | 111 | Project sponsored by [Dynobase](https://dynobase.dev) 112 | -------------------------------------------------------------------------------- /lib/function.ts: -------------------------------------------------------------------------------- 1 | import * as lambda from '@aws-cdk/aws-lambda'; 2 | import * as cdk from '@aws-cdk/core'; 3 | import * as crypto from 'crypto'; 4 | import * as fs from 'fs'; 5 | import * as path from 'path'; 6 | import { Builder } from './builder'; 7 | import { parseStackTrace } from './util'; 8 | 9 | /** 10 | * Properties for a GolangFunction 11 | */ 12 | export interface GolangFunctionProps extends lambda.FunctionOptions { 13 | /** 14 | * Path to the entry Golang source file. 15 | * 16 | * @default - Derived from the name of the defining file and the construct's id. 17 | * If the `GolangFunction` is defined in `stack.ts` with `my-handler` as id 18 | * (`new GolangFunction(this, 'my-handler')`), the construct will look at `stack/my-handler/main.go` 19 | */ 20 | readonly entry?: string; 21 | 22 | /** 23 | * The name of the exported handler in the entry file. 24 | * 25 | * @default main 26 | */ 27 | readonly handler?: string; 28 | 29 | /** 30 | * The build directory 31 | * 32 | * @default - `.build` in the entry file directory 33 | */ 34 | readonly buildDir?: string; 35 | 36 | /** 37 | * The build command 38 | * 39 | * @default - `go build -ldflags="-s -w"` 40 | */ 41 | readonly buildCmd?: string; 42 | 43 | /** 44 | * Additional environment variables 45 | * 46 | * @default - `{ GOOS: 'linux' }` 47 | */ 48 | readonly extraEnv?: any; 49 | } 50 | 51 | /** 52 | * A Node.js Lambda function bundled using Parcel 53 | */ 54 | export class GolangFunction extends lambda.Function { 55 | constructor(scope: cdk.Construct, id: string, props: GolangFunctionProps = {}) { 56 | const entry = findEntry(id, props.entry); 57 | const handler = props.handler || 'main'; 58 | const buildCmd = props.buildCmd || 'go build -ldflags="-s -w"'; 59 | const buildDir = props.buildDir || path.join(path.dirname(entry), '.build'); 60 | const handlerDir = path.join(buildDir, crypto.createHash('sha256').update(entry).digest('hex')); 61 | const runtime = lambda.Runtime.GO_1_X; 62 | 63 | // Build with go 64 | const builder = new Builder({ 65 | entry, 66 | outDir: handlerDir, 67 | handler, 68 | buildCmd, 69 | extraEnv: props.extraEnv || {}, 70 | }); 71 | builder.build(); 72 | 73 | super(scope, id, { 74 | ...props, 75 | runtime, 76 | code: lambda.Code.fromAsset(handlerDir), 77 | handler: handler, 78 | }); 79 | } 80 | } 81 | 82 | /** 83 | * Searches for an entry file. Preference order is the following: 84 | * 1. Given entry file 85 | * 2. A .go file named as the defining file with id as suffix (defining-file.id.go) 86 | */ 87 | function findEntry(id: string, entry?: string): string { 88 | if (entry) { 89 | if (!/\.(go)$/.test(entry)) { 90 | throw new Error('Only Golang entry files are supported.'); 91 | } 92 | 93 | entry = path.join(process.cwd(), path.sep, entry) 94 | 95 | if (!fs.existsSync(entry)) { 96 | throw new Error(`Cannot find entry file at ${entry}`); 97 | } 98 | return entry; 99 | } 100 | 101 | const definingFile = findDefiningFile(); 102 | const libDir = path.join(definingFile, '..'); 103 | const goHandlerFile = path.join(libDir, `/${id}/main.go`); 104 | 105 | if (fs.existsSync(goHandlerFile)) { 106 | return goHandlerFile; 107 | } 108 | 109 | throw new Error('Cannot find entry file.'); 110 | } 111 | 112 | /** 113 | * Finds the name of the file where the `GolangFunction` is defined 114 | */ 115 | function findDefiningFile(): string { 116 | const stackTrace = parseStackTrace(); 117 | const functionIndex = stackTrace.findIndex((s) => /GolangFunction/.test(s.methodName || '')); 118 | 119 | if (functionIndex === -1 || !stackTrace[functionIndex + 1]) { 120 | throw new Error('Cannot find defining file.'); 121 | } 122 | 123 | return stackTrace[functionIndex + 1].file; 124 | } 125 | --------------------------------------------------------------------------------