├── tests ├── __init__.py └── test_bake_project.py ├── cookiecutter.json ├── {{cookiecutter.project_name}} ├── Makefile ├── hello-world │ ├── go.mod │ ├── main_test.go │ └── main.go ├── template.yaml └── README.md ├── requirements-dev.txt ├── .gitignore ├── LICENSE ├── README.md └── CONTRIBUTING.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_name": "Name of the project" 3 | } -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | 3 | build: 4 | sam build 5 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | cookiecutter==2.1.1 2 | flake8==3.5.0 3 | pytest==3.3.2 4 | pytest-cookies==0.3.0 5 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/hello-world/go.mod: -------------------------------------------------------------------------------- 1 | require github.com/aws/aws-lambda-go v1.13.3 2 | 3 | module hello-world 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/hello-world/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aws/aws-lambda-go/events" 7 | "github.com/aws/aws-sdk-go/service/dynamodb" 8 | "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface" 9 | ) 10 | 11 | type mockedPutItem struct { 12 | dynamodbiface.DynamoDBAPI 13 | Response dynamodb.PutItemOutput 14 | } 15 | 16 | func (d mockedPutItem) PutItem(in *dynamodb.PutItemInput) (*dynamodb.PutItemOutput, error) { 17 | return &d.Response, nil 18 | } 19 | 20 | func TestLambdaHandler(t *testing.T) { 21 | t.Run("Successful Request", func(t *testing.T) { 22 | 23 | m := mockedPutItem{ 24 | Response: dynamodb.PutItemOutput{}, 25 | } 26 | 27 | d := dependency{ 28 | ddb: m, 29 | table: "test_table", 30 | } 31 | 32 | _, err := d.LambdaHandler(events.APIGatewayProxyRequest{}) 33 | if err != nil { 34 | t.Fatal("Everything should be ok") 35 | } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Rob Sutter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS SAM CLI better application templates for Go 2 | 3 | This repository contains a better "Hello, world!" template for AWS SAM CLI applications that use the [Go][go] runtime. This template includes the necessary setup for dependency injection to support mocks for testing. 4 | 5 | ## How to use this template 6 | 7 | 1. Install the [AWS SAM CLI][aws-sam-cli] 8 | 1. From a terminal, run `sam init` 9 | 1. Select option **2 - Custom Template Location** 10 | 1. Enter _gh:rts-rob/aws-sam-better-golang_ 11 | 1. Provide a project name, e.g., _hello-world_ 12 | 1. Change into the created directory 13 | 1. Build with `sam build` 14 | 1. Deploy with `sam deploy --guided` 15 | 16 | Now you have a sample application with [Amazon API Gateway][api-gateway], [Amazon DynamoDB][dynamodb], and [AWS Lambda][lambda] deployed and running in the region you specified. 17 | 18 | ## Contributing 19 | 20 | Issue reports and pull requests to help improve these application templates are always welcome. Please see [CONTRIBUTING.md](CONTRIBUTING.md). 21 | 22 | ## License 23 | 24 | This project is licensed under the [MIT License][mit-license]. 25 | 26 | [api-gateway]: https://aws.amazon.com/api-gateway/ 27 | [aws-sam-cli]: https://rbsttr.tv/samcli 28 | [dynamodb]: https://aws.amazon.com/dynamodb/ 29 | [go]: https://golang.org 30 | [lambda]: https://aws.amazon.com/lambda/ 31 | [mit-license]: https://choosealicense.com/licenses/mit/ 32 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | {{ cookiecutter.project_name }} 5 | 6 | AWS SAM Template for {{ cookiecutter.project_name }} 7 | 8 | # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst 9 | Globals: 10 | Function: 11 | Timeout: 5 12 | 13 | Resources: 14 | HelloWorldFunction: 15 | Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction 16 | Properties: 17 | CodeUri: hello-world/ 18 | Handler: hello-world 19 | Runtime: go1.x 20 | Tracing: Active # https://docs.aws.amazon.com/lambda/latest/dg/lambda-x-ray.html 21 | Events: 22 | CatchAll: 23 | Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api 24 | Properties: 25 | Path: /hello 26 | Method: POST 27 | Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object 28 | Variables: 29 | DYNAMODB_TABLE: !Ref VoteTable 30 | Policies: 31 | - DynamoDBWritePolicy: 32 | TableName: !Ref VoteTable 33 | 34 | VoteTable: 35 | Type: AWS::Serverless::SimpleTable 36 | 37 | Outputs: 38 | # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function 39 | # Find out more about other implicit resources you can reference within SAM 40 | # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api 41 | HelloWorldAPI: 42 | Description: "API Gateway endpoint URL for Prod environment for First Function" 43 | Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" 44 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/hello-world/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/aws/aws-lambda-go/events" 7 | "github.com/aws/aws-lambda-go/lambda" 8 | "github.com/aws/aws-sdk-go/aws" 9 | "github.com/aws/aws-sdk-go/aws/session" 10 | "github.com/aws/aws-sdk-go/service/dynamodb" 11 | "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" 12 | "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface" 13 | ) 14 | 15 | type dependency struct { 16 | // For dependency injection, you want to import {service}/{service}iface 17 | // and create members of type {service}iface/{Service}API 18 | // Examples: 19 | // ddb dynamodbiface.DynamoDBAPI 20 | // s3svc s3iface.S3API 21 | // ssmsvc ssmiface.SSMAPI 22 | ddb dynamodbiface.DynamoDBAPI 23 | table string 24 | } 25 | 26 | // Record represents one record in the DynamoDB table 27 | type Record struct { 28 | ID string `dynamodbav:"id"` 29 | Body string 30 | } 31 | 32 | func (d *dependency) LambdaHandler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 33 | // Check for dependencies, e.g., test injections. 34 | // If not present, create them with a live session. 35 | // 36 | // NOTE: Performance is the same *or better* when initializing dependencies 37 | // inside the handler compared to inside main(). 38 | // For testing, see https://rbsttr.tv/godi 39 | if d.ddb == nil { 40 | sess := session.Must(session.NewSession()) 41 | svc := dynamodb.New(sess) 42 | 43 | d = &dependency{ 44 | ddb: svc, 45 | table: os.Getenv("DYNAMODB_TABLE"), 46 | } 47 | } 48 | 49 | // Create a new record from the request 50 | r := Record{ 51 | ID: request.RequestContext.RequestID, 52 | Body: request.Body, 53 | } 54 | 55 | // Marshal that record into a DynamoDB AttributeMap 56 | av, err := dynamodbattribute.MarshalMap(r) 57 | if err != nil { 58 | return events.APIGatewayProxyResponse{}, err 59 | } 60 | 61 | // Save the AttributeMap in the given table 62 | _, err = d.ddb.PutItem(&dynamodb.PutItemInput{ 63 | TableName: aws.String(d.table), 64 | Item: av, 65 | }) 66 | 67 | if err != nil { 68 | return events.APIGatewayProxyResponse{}, err 69 | } 70 | 71 | return events.APIGatewayProxyResponse{ 72 | StatusCode: 200, 73 | }, nil 74 | } 75 | 76 | func main() { 77 | d := dependency{} 78 | 79 | lambda.Start(d.LambdaHandler) 80 | } 81 | -------------------------------------------------------------------------------- /tests/test_bake_project.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | 3 | import os 4 | import subprocess 5 | 6 | 7 | @contextmanager 8 | def inside_dir(dirpath): 9 | """ 10 | Execute code from inside the given directory 11 | :param dirpath: String, path of the directory the command is being run. 12 | """ 13 | old_path = os.getcwd() 14 | try: 15 | os.chdir(dirpath) 16 | yield 17 | finally: 18 | os.chdir(old_path) 19 | 20 | 21 | def test_project_tree(cookies): 22 | result = cookies.bake(extra_context={'project_name': 'test_project'}) 23 | assert result.exit_code == 0 24 | assert result.exception is None 25 | assert result.project.basename == 'test_project' 26 | 27 | assert result.project.isdir() 28 | assert result.project.join('README.md').isfile() 29 | assert result.project.join('template.yaml').isfile() 30 | assert result.project.join('hello-world').isdir() 31 | assert result.project.join('hello-world', 'main.go').isfile() 32 | assert result.project.join('hello-world', 'main_test.go').isfile() 33 | 34 | 35 | def test_app_content(cookies): 36 | result = cookies.bake(extra_context={'project_name': 'test_project'}) 37 | app_file = result.project.join('hello-world', 'main.go') 38 | app_content = app_file.readlines() 39 | app_content = ''.join(app_content) 40 | 41 | contents = ( 42 | "github.com/aws/aws-lambda-go/events", 43 | "resp, err := http.Get(DefaultHTTPGetAddress)", 44 | "lambda.Start(handler)" 45 | ) 46 | 47 | for content in contents: 48 | assert content in app_content 49 | 50 | 51 | def test_app_test_content(cookies): 52 | result = cookies.bake(extra_context={'project_name': 'test_project'}) 53 | app_file = result.project.join('hello-world', 'main_test.go') 54 | app_content = app_file.readlines() 55 | app_content = ''.join(app_content) 56 | 57 | contents = ( 58 | "DefaultHTTPGetAddress = \"http://127.0.0.1:12345\"", 59 | "DefaultHTTPGetAddress = ts.URL", 60 | "Successful Request" 61 | ) 62 | 63 | for content in contents: 64 | assert content in app_content 65 | 66 | 67 | def test_app_template_content(cookies): 68 | result = cookies.bake(extra_context={'project_name': 'test_project'}) 69 | app_file = result.project.join('template.yaml') 70 | app_content = app_file.readlines() 71 | app_content = ''.join(app_content) 72 | 73 | contents = ( 74 | "Runtime: go1.x", 75 | "HelloWorldFunction", 76 | ) 77 | 78 | for content in contents: 79 | assert content in app_content 80 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to this project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | ## Reporting Bugs/Feature Requests 10 | 11 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 12 | 13 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 14 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 15 | 16 | * A reproducible test case or series of steps 17 | * The version of our code being used 18 | * Any modifications you've made relevant to the bug 19 | * Anything unusual about your environment or deployment 20 | 21 | ## Contributing via Pull Requests 22 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 23 | 24 | 1. You are working against the latest source on the *mainline* branch. 25 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 26 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 27 | 28 | To send us a pull request, please: 29 | 30 | 1. Fork the repository. 31 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 32 | 3. Ensure local tests pass. 33 | 4. Commit to your fork using clear commit messages. 34 | 5. Send us a pull request, answering any default questions in the pull request interface. 35 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 36 | 37 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 38 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 39 | 40 | ## Finding contributions to work on 41 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 42 | 43 | ## Licensing 44 | 45 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 46 | 47 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 48 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/README.md: -------------------------------------------------------------------------------- 1 | # {{ cookiecutter.project_name }} 2 | 3 | This is a sample template for {{ cookiecutter.project_name }} - Below is a brief explanation of what we have generated for you: 4 | 5 | ```bash 6 | . 7 | ├── Makefile <-- Make to automate build 8 | ├── README.md <-- This instructions file 9 | ├── hello-world <-- Source code for a lambda function 10 | │ ├── main.go <-- Lambda function code 11 | │ └── main_test.go <-- Unit tests 12 | └── template.yaml 13 | ``` 14 | 15 | ## Requirements 16 | 17 | * AWS CLI already configured with Administrator permission 18 | * [Docker installed](https://www.docker.com/community-edition) 19 | * [Golang](https://golang.org) 20 | * SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) 21 | 22 | ## Setup process 23 | 24 | ### Installing dependencies & building the target 25 | 26 | In this example we use the built-in `sam build` to automatically download all the dependencies and package our build target. 27 | Read more about [SAM Build here](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-build.html) 28 | 29 | The `sam build` command is wrapped inside of the `Makefile`. To execute this simply run 30 | 31 | ```shell 32 | make 33 | ``` 34 | 35 | ### Local development 36 | 37 | **Invoking function locally through local API Gateway** 38 | 39 | ```bash 40 | sam local start-api 41 | ``` 42 | 43 | If the previous command ran successfully you should now be able to hit the following local endpoint to invoke your function `http://localhost:3000/hello` 44 | 45 | **SAM CLI** is used to emulate both Lambda and API Gateway locally and uses our `template.yaml` to understand how to bootstrap this environment (runtime, where the source code is, etc.) - The following excerpt is what the CLI will read in order to initialize an API and its routes: 46 | 47 | ```yaml 48 | ... 49 | Events: 50 | HelloWorld: 51 | Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api 52 | Properties: 53 | Path: /hello 54 | Method: get 55 | ``` 56 | 57 | ## Packaging and deployment 58 | 59 | AWS Lambda Golang runtime requires a flat folder with the executable generated on build step. SAM will use `CodeUri` property to know where to look up for the application: 60 | 61 | ```yaml 62 | ... 63 | FirstFunction: 64 | Type: AWS::Serverless::Function 65 | Properties: 66 | CodeUri: hello_world/ 67 | ... 68 | ``` 69 | 70 | To deploy your application for the first time, run the following in your shell: 71 | 72 | ```bash 73 | sam deploy --guided 74 | ``` 75 | 76 | The command will package and deploy your application to AWS, with a series of prompts: 77 | 78 | * **Stack Name**: The name of the stack to deploy to CloudFormation. This should be unique to your account and region, and a good starting point would be something matching your project name. 79 | * **AWS Region**: The AWS region you want to deploy your app to. 80 | * **Confirm changes before deploy**: If set to yes, any change sets will be shown to you before execution for manual review. If set to no, the AWS SAM CLI will automatically deploy application changes. 81 | * **Allow SAM CLI IAM role creation**: Many AWS SAM templates, including this example, create AWS IAM roles required for the AWS Lambda function(s) included to access AWS services. By default, these are scoped down to minimum required permissions. To deploy an AWS CloudFormation stack which creates or modified IAM roles, the `CAPABILITY_IAM` value for `capabilities` must be provided. If permission isn't provided through this prompt, to deploy this example you must explicitly pass `--capabilities CAPABILITY_IAM` to the `sam deploy` command. 82 | * **Save arguments to samconfig.toml**: If set to yes, your choices will be saved to a configuration file inside the project, so that in the future you can just re-run `sam deploy` without parameters to deploy changes to your application. 83 | 84 | You can find your API Gateway Endpoint URL in the output values displayed after deployment. 85 | 86 | ### Testing 87 | 88 | We use `testing` package that is built-in in Golang and you can simply run the following command to run our tests: 89 | 90 | ```shell 91 | go test -v ./hello-world/ 92 | ``` 93 | # Appendix 94 | 95 | ### Golang installation 96 | 97 | Please ensure Go 1.x (where 'x' is the latest version) is installed as per the instructions on the official golang website: https://golang.org/doc/install 98 | 99 | A quickstart way would be to use Homebrew, chocolatey or your linux package manager. 100 | 101 | #### Homebrew (Mac) 102 | 103 | Issue the following command from the terminal: 104 | 105 | ```shell 106 | brew install golang 107 | ``` 108 | 109 | If it's already installed, run the following command to ensure it's the latest version: 110 | 111 | ```shell 112 | brew update 113 | brew upgrade golang 114 | ``` 115 | 116 | #### Chocolatey (Windows) 117 | 118 | Issue the following command from the powershell: 119 | 120 | ```shell 121 | choco install golang 122 | ``` 123 | 124 | If it's already installed, run the following command to ensure it's the latest version: 125 | 126 | ```shell 127 | choco upgrade golang 128 | ``` 129 | 130 | ## Bringing to the next level 131 | 132 | Here are a few ideas that you can use to get more acquainted as to how this overall process works: 133 | 134 | * Create an additional API resource (e.g. /hello/{proxy+}) and return the name requested through this new path 135 | * Update unit test to capture that 136 | * Package & Deploy 137 | 138 | Next, you can use the following resources to know more about beyond hello world samples and how others structure their Serverless applications: 139 | 140 | * [AWS Serverless Application Repository](https://aws.amazon.com/serverless/serverlessrepo/) 141 | --------------------------------------------------------------------------------