├── 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 | [](https://badge.fury.io/gh/RafalWilinski%2Faws-lambda-golang-cdk)
2 | [](https://badge.fury.io/js/aws-lambda-golang)
3 | [](https://badge.fury.io/nu/rwilinski.GolangFunction)
4 | [](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 |
--------------------------------------------------------------------------------