Initial version (#1)
* infra: python, bootstrap * feat: local sample * infra: invocations and CFN * docs: stub * wip: clean up, add sceptre * wip: add app stack teardown * refactor: template rejigging * chore: ignore outfiles * wip: cleanup post upload to s3 * wip: teardown infra task * docs: links, contrib * docs: missing command * docs: formatting
This commit is contained in:
parent
a20a479882
commit
d2a6cfe378
17 changed files with 336 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
*.sw*
|
||||
*.pyc
|
||||
*.zip
|
||||
*_out.json
|
1
.python-version
Normal file
1
.python-version
Normal file
|
@ -0,0 +1 @@
|
|||
3.8.2
|
7
Dockerfile
Normal file
7
Dockerfile
Normal file
|
@ -0,0 +1,7 @@
|
|||
FROM lambci/lambda:python3.8
|
||||
|
||||
USER root
|
||||
|
||||
ENV APP_DIR /var/task
|
||||
|
||||
WORKDIR $APP_DIR
|
39
README.md
39
README.md
|
@ -1,2 +1,41 @@
|
|||
# lambda-boilerplate
|
||||
🛠 Skip the boilerplate and start building fun λ things 🛠
|
||||
|
||||
## Overview
|
||||
|
||||
AWS Lambdas are fun, but often the amount of boilerplate involved in getting a project off the ground hinders the fun. From setting up a local environment to writing out a Cloudformation template, the overhead of Lambda-based greenfield projects can be daunting. No more. Just use this repository as a template or clone it and jump straight into the action! This repository offers a quick template your can use, full with a Docker setup for local development and invocation commands that you can use to package and deploy small Lambdas.
|
||||
|
||||
Use this as a foundation and tweak it to your use case!
|
||||
|
||||
## Local development
|
||||
|
||||
To get started, hit the bootstrap script with `. script/bootstrap`. This will set up a Python 3.8 virtualenv set up with some basic tools that will make your life easier.
|
||||
|
||||
The base Lambda handler is at `src/base.py` and all the infrastructure templates and Sceptre configuration are in `infrastructure`.
|
||||
|
||||
[Read more about Sceptre](https://sceptre.cloudreach.com/latest/index.html)
|
||||
|
||||
[Read more about AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/lambda-python.html)
|
||||
|
||||
### Invocations
|
||||
|
||||
This template uses PyInvoke, all commands are of the format `inv <command> <parameters>`.
|
||||
|
||||
|Command|Description|
|
||||
|---|---|
|
||||
|`app.start`|Start your Lambda in Docker.|
|
||||
|`app.stop`|Stop and remove the container.|
|
||||
|`app.invoke-function <function name> <Serialized JSON payload>`|Invokes the given local Lambda by container name|
|
||||
|`stack.deploy`|Packages your code and deploys the stack|
|
||||
|`stack.teardown-app`|Tears down the application stack|
|
||||
|`stack.teardown-bootstrap`|Tears down the bootstrap stack|
|
||||
|
||||
## Deployment
|
||||
|
||||
The base setup assumes that your Lambda handler is located in `src.base`. Doing `inv stack.deploy` will zip up your `src` directory, create an S3 bucket for your development artifacts, uploads the source doe archive to S3 and kickstarts the Cloudformation deployment of your stack.
|
||||
|
||||
## Contributing
|
||||
|
||||
Got suggestions or improvements you'd like to make? Open a PR or [an issue](https://github.com/mcataford/lambda-boilerplate/issues)!
|
||||
|
||||
Feature requests should keep in mind that the goal of this boilerplate is to be general enough so that the cost of tailoring it to specific use cases is low.
|
||||
|
|
30
dev_requirements.txt
Normal file
30
dev_requirements.txt
Normal file
|
@ -0,0 +1,30 @@
|
|||
appdirs==1.4.4
|
||||
attrs==20.1.0
|
||||
awscli==1.18.124
|
||||
black==19.10b0
|
||||
boto3==1.14.55
|
||||
botocore==1.17.55
|
||||
click==7.1.2
|
||||
colorama==0.3.9
|
||||
decorator==4.4.2
|
||||
docutils==0.15.2
|
||||
invoke==1.4.1
|
||||
Jinja2==2.11.2
|
||||
jmespath==0.10.0
|
||||
MarkupSafe==1.1.1
|
||||
networkx==2.1
|
||||
packaging==16.8
|
||||
pathspec==0.8.0
|
||||
pyasn1==0.4.8
|
||||
pyparsing==2.4.7
|
||||
python-dateutil==2.8.1
|
||||
PyYAML==5.3.1
|
||||
regex==2020.7.14
|
||||
rsa==4.5
|
||||
s3transfer==0.3.3
|
||||
sceptre==2.3.0
|
||||
six==1.15.0
|
||||
toml==0.10.1
|
||||
typed-ast==1.4.1
|
||||
typing==3.7.4.3
|
||||
urllib3==1.25.10
|
13
docker-compose.yml
Normal file
13
docker-compose.yml
Normal file
|
@ -0,0 +1,13 @@
|
|||
version: '3.2'
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
volumes:
|
||||
- ./src/:/var/task/
|
||||
command: "base.handler"
|
||||
ports:
|
||||
- "9001:9001"
|
||||
environment:
|
||||
PYTHONPATH: /var/task/src:/var/task/lib
|
||||
DOCKER_LAMBDA_STAY_OPEN: 1
|
||||
DOCKER_LAMBDA_WATCH: 1
|
8
infrastructure/config/app/app.yaml
Normal file
8
infrastructure/config/app/app.yaml
Normal file
|
@ -0,0 +1,8 @@
|
|||
template_path: app.yaml
|
||||
parameters:
|
||||
# Lambda source artifacts bucket name
|
||||
ArtifactsBucketName: !stack_output bootstrap/bootstrap.yaml::ArtifactsBucketNameTest
|
||||
# Lambda zip key
|
||||
SourceKey: {{ var.source_key | default("") }}
|
||||
ApiStageName: "v0"
|
||||
|
2
infrastructure/config/app/config.yaml
Normal file
2
infrastructure/config/app/config.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
project_code: app
|
||||
region: us-east-1
|
4
infrastructure/config/bootstrap/bootstrap.yaml
Normal file
4
infrastructure/config/bootstrap/bootstrap.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
template_path: bootstrap.yaml
|
||||
parameters:
|
||||
# Bucket name for the artifacts S# bucket. Used solely to store versioned lambda zips.
|
||||
ArtifactsBucketName: my-cool-bucket-name
|
2
infrastructure/config/bootstrap/config.yaml
Normal file
2
infrastructure/config/bootstrap/config.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
project_code: app
|
||||
region: us-east-1
|
2
infrastructure/config/config.yaml
Normal file
2
infrastructure/config/config.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
project_code: app
|
||||
region: us-east-1
|
99
infrastructure/templates/app.yaml
Normal file
99
infrastructure/templates/app.yaml
Normal file
|
@ -0,0 +1,99 @@
|
|||
AWSTemplateFormatVersion: 2010-09-09
|
||||
Parameters:
|
||||
ArtifactsBucketName:
|
||||
Type: String
|
||||
Description: Bucket storing the function source.
|
||||
SourceKey:
|
||||
Type: String
|
||||
ApiStageName:
|
||||
Type: String
|
||||
Resources:
|
||||
# Lambda function
|
||||
Function:
|
||||
Type: AWS::Lambda::Function
|
||||
Properties:
|
||||
Code:
|
||||
S3Bucket: !Ref ArtifactsBucketName
|
||||
S3Key: !Ref SourceKey
|
||||
FunctionName: sampleFunction
|
||||
Handler: src.base.handler
|
||||
Role: !GetAtt LambdaExecutionRole.Arn
|
||||
Runtime: python3.8
|
||||
# Roles
|
||||
LambdaExecutionRole:
|
||||
Type: AWS::IAM::Role
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: 2012-10-17
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- lambda.amazonaws.com
|
||||
Action:
|
||||
- sts:AssumeRole
|
||||
ApiGatewayRole:
|
||||
Type: AWS::IAM::Role
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: 2012-10-17
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- apigateway.amazonaws.com
|
||||
Action:
|
||||
- sts:AssumeRole
|
||||
Path: '/'
|
||||
Policies:
|
||||
- PolicyName: LambdaAccess
|
||||
PolicyDocument:
|
||||
Version: 2012-10-17
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action: 'lambda:*'
|
||||
Resource: !GetAtt Function.Arn
|
||||
|
||||
|
||||
RestApi:
|
||||
Type: AWS::ApiGateway::RestApi
|
||||
Properties:
|
||||
Name: lambda-api
|
||||
GatewayResource:
|
||||
Type: AWS::ApiGateway::Resource
|
||||
Properties:
|
||||
ParentId: !GetAtt RestApi.RootResourceId
|
||||
PathPart: lambda
|
||||
RestApiId: !Ref RestApi
|
||||
# This template only defines a POST method, but others can easily be defined
|
||||
# by duping this resource and tweaking it for other resources, opnames or URIs.
|
||||
PostMethod:
|
||||
Type: AWS::ApiGateway::Method
|
||||
Properties:
|
||||
HttpMethod: POST
|
||||
AuthorizationType: NONE
|
||||
Integration:
|
||||
Credentials: !GetAtt ApiGatewayRole.Arn
|
||||
IntegrationHttpMethod: POST
|
||||
Type: AWS_PROXY
|
||||
Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Function.Arn}/invocations'
|
||||
OperationName: lambda
|
||||
ResourceId: !Ref GatewayResource
|
||||
RestApiId: !Ref RestApi
|
||||
ApiStage:
|
||||
Type: AWS::ApiGateway::Stage
|
||||
Properties:
|
||||
DeploymentId: !Ref ApiDeployment
|
||||
RestApiId: !Ref RestApi
|
||||
StageName: !Ref ApiStageName
|
||||
ApiDeployment:
|
||||
Type: AWS::ApiGateway::Deployment
|
||||
DependsOn: PostMethod
|
||||
Properties:
|
||||
RestApiId: !Ref RestApi
|
||||
|
||||
Outputs:
|
||||
ApiGatewayId:
|
||||
Value: !Ref RestApi
|
||||
Export:
|
||||
Name: RestApiId
|
18
infrastructure/templates/bootstrap.yaml
Normal file
18
infrastructure/templates/bootstrap.yaml
Normal file
|
@ -0,0 +1,18 @@
|
|||
AWSTemplateFormatVersion: 2010-09-09
|
||||
Parameters:
|
||||
ArtifactsBucketName:
|
||||
Type: String
|
||||
Description: Bucket storing the function source
|
||||
Resources:
|
||||
DevBucket:
|
||||
Type: AWS::S3::Bucket
|
||||
Properties:
|
||||
BucketName: !Ref ArtifactsBucketName
|
||||
AccessControl: Private
|
||||
VersioningConfiguration:
|
||||
Status: Enabled
|
||||
Outputs:
|
||||
ArtifactsBucketNameTest:
|
||||
Value: !Ref ArtifactsBucketName
|
||||
Export:
|
||||
Name: ArtifactsBucketNameTest
|
15
script/bootstrap
Normal file
15
script/bootstrap
Normal file
|
@ -0,0 +1,15 @@
|
|||
VENV=lambda-boilerplate.venv
|
||||
|
||||
#################################################################
|
||||
# Bootstrapping sets up the Python 3.8 venv that allows the use #
|
||||
# of the invoke commands. #
|
||||
#################################################################
|
||||
|
||||
{
|
||||
pyenv virtualenv-delete -f $VENV
|
||||
pyenv virtualenv $VENV &&
|
||||
pyenv activate $VENV &&
|
||||
python -m pip install -U pip &&
|
||||
pip install -r dev_requirements.txt &&
|
||||
echo "✨ Good to go! ✨"
|
||||
}
|
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
5
src/base.py
Normal file
5
src/base.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
|
||||
def handler(event, context):
|
||||
return {"statusCode": 200}
|
87
tasks.py
Normal file
87
tasks.py
Normal file
|
@ -0,0 +1,87 @@
|
|||
from invoke import task, Collection
|
||||
import boto3
|
||||
import os
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
|
||||
#####################
|
||||
# Stack invocations #
|
||||
#####################
|
||||
|
||||
|
||||
@task(name="teardown-app")
|
||||
def teardown_app(ctx):
|
||||
with ctx.cd("infrastructure"):
|
||||
ctx.run("sceptre delete app/app.yaml -y")
|
||||
|
||||
@task(name="teardown-bootstrap")
|
||||
def teardown_bootstrap(ctx):
|
||||
with ctx.cd("infrastructure"):
|
||||
ctx.run("sceptre delete bootstrap/bootstrap.yaml -y")
|
||||
|
||||
|
||||
|
||||
@task(name="deploy")
|
||||
def stack_deploy(ctx):
|
||||
path = Path(__file__).parent
|
||||
with ctx.cd("infrastructure"):
|
||||
ctx.run("sceptre launch bootstrap/bootstrap.yaml -y")
|
||||
|
||||
ctx.run("zip lambda_function.zip src/*")
|
||||
with open("lambda_function.zip", "rb") as src:
|
||||
srchash = hashlib.md5(src.read()).hexdigest()
|
||||
new_archive_name = f"lambda_function_{srchash}.zip"
|
||||
ctx.run(f"mv lambda_function.zip {new_archive_name}")
|
||||
|
||||
ctx.run(f"aws s3 cp {new_archive_name} s3://mcat-dev-test-bucket-artifacts-2 && rm {new_archive_name}")
|
||||
|
||||
with ctx.cd("infrastructure"):
|
||||
ctx.run(f"sceptre --var source_key={new_archive_name} launch app/app.yaml -y")
|
||||
|
||||
|
||||
#####################
|
||||
# Local invocations #
|
||||
#####################
|
||||
|
||||
|
||||
@task(name="start")
|
||||
def app_start(ctx):
|
||||
ctx.run("docker-compose up -d --build")
|
||||
|
||||
|
||||
@task(name="stop")
|
||||
def app_stop(ctx):
|
||||
ctx.run("docker-compose down")
|
||||
|
||||
|
||||
@task(
|
||||
name="invoke-function",
|
||||
help={
|
||||
"function_name": "Name of the Lambda to invoke locally (as defined in the Cloudformation template)",
|
||||
"payload": "JSON payload to include in the trigger event",
|
||||
},
|
||||
)
|
||||
def app_invoke_function(ctx, function_name, payload):
|
||||
ctx.run(
|
||||
f"aws lambda invoke --endpoint http://localhost:9001 --no-sign-request --function-name {function_name} --log-type Tail --payload {payload} {function_name}_out.json"
|
||||
)
|
||||
|
||||
|
||||
ns = Collection()
|
||||
|
||||
# Stack invocations manage the Clouformation flows
|
||||
|
||||
stack = Collection("stack")
|
||||
stack.add_task(stack_deploy)
|
||||
stack.add_task(teardown_app)
|
||||
stack.add_task(teardown_bootstrap)
|
||||
|
||||
# App invocations manage local containers
|
||||
|
||||
app = Collection("app")
|
||||
app.add_task(app_start)
|
||||
app.add_task(app_stop)
|
||||
app.add_task(app_invoke_function)
|
||||
|
||||
ns.add_collection(stack)
|
||||
ns.add_collection(app)
|
Reference in a new issue