Localstack / AWS Services / Go
Pre-requisites
Before following the article, you should have
python
&pip
docker.io
docker-compose
go
- AWS CLI or
awslocal
Installed on your system.
The reason why
awslocal
makes it a lot easier to play with AWS commands is, you don’t have to specify--endpoint-url
each and everytime you need to access, invoke or create a AWS resource.Thanks to localstack.cloud for creating this utility for us.
pip install awscli-local
Example Project
Lambda Function
Let’s write Lambda function that responds with a JSON response.
package main
import (
"context"
"encoding/json"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"log"
"time"
)
type response struct {
UTC time.Time `json:"utc"`
}
func handleRequest(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
now := time.Now()
resp := &response{
UTC: now.UTC(),
}
body, err := json.Marshal(resp)
if err != nil {
return events.APIGatewayProxyResponse{}, err
}
return events.APIGatewayProxyResponse{Body: string(body), StatusCode: 200}, nil
}
func main() {
lambda.Start(handleRequest)
}
You can probably put this code into a main.go
and then build your project.
With Go modules, it is even simpler i.e; you don’t have to manage GOPATH
separately.
mkdir mylambda/
cd mylambda/
go mod init mylambda
touch main.go # and put the code above, in that file
go mod download # will fetch all dependencies
Localstack
Install Localstack
pip install localstack
Run Localstack
localstack start
If you need services like AWS Lambda, AWS API Gateway, or may be S3 - you may pass an environment variable
SERVICES=lambda,apigateway,s3 localstack start
Run Localstack Using Docker Compose
You can define your own docker-compose file to run localstack
version: "3.3"
services:
mylocalstack:
container_name: mylocalstack
image: localstack/localstack
network_mode: bridge
ports:
- "4566:4566"
- "4571:4571"
- "${PORT_WEB_UI-8080}:${PORT_WEB_UI-8080}"
environment:
- DATA_DIR=${DATA_DIR- }
- PORT_WEB_UI=${PORT_WEB_UI- }
- LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR- }
- KINESIS_ERROR_PROBABILITY=${KINESIS_ERROR_PROBABILITY- }
- DOCKER_HOST=unix:///var/run/docker.sock
- HOST_TMP_FOLDER=${TMPDIR}
- LOCALSTACK_API_KEY=${LOCALSTACK_API_KEY}
- DEBUG=${DEBUG- }
- SERVICES=${SERVICES- }
volumes:
- "${TMPDIR:-/tmp/localstack}:/tmp/localstack"
- "/var/run/docker.sock:/var/run/docker.sock"
You can now run the container
docker-compose up -d
In macOS, you will have to run it using
TMPDIR=/private$TMPDIR docker-compose up -d
Otherwise it won’t work.
Installing AWS CLI
pip install awscli-local
Setup AWS Infrastructure Locally
#!/usr/bin/env bash
GRACE_TIME="20s"
REGION="us-east-1"
STAGE="test"
API_NAME=api_$(date +"%Y%m%d_%H%M%S")
DOCKER_FILE="docker-compose.yml"
ENV_FILE="./.env"
LOCALSTACK_ENDPOINT="http://localhost:4566"
function fail() {
echo $2
exit $1
}
if [[ -f $ENV_FILE ]]; then
echo "Sourcing environment variables..."
source $ENV_FILE
else
fail 9 "$ENV_FILE not present..."
fi
echo "Building lambda..."
GOOS=linux go build -o api ./ && \
zip api.zip api && \
rm -rf api
echo "Removing old containers..."
docker-compose -f ${DOCKER_FILE} down --remove-orphans
echo "Building new localstack environment..."
docker-compose -f ${DOCKER_FILE} up -d
echo "Grace time for $GRACE_TIME..."
sleep $GRACE_TIME && echo "Grace time ended..."
echo "Generated API name: $API_NAME"
awslocal elasticache create-cache-cluster \
--cache-cluster-id "testcenters_cache" \
--engine redis \
--cache-node-type cache.m5.large \
--num-cache-nodes 1
echo "Creating lambda function..."
awslocal lambda create-function \
--region ${REGION} \
--function-name ${API_NAME} \
--runtime go1.x \
--handler api \
--timeout 30 \
--memory-size 512 \
--zip-file fileb://api.zip \
--role arn:aws:iam::123456:role/irrelevant
[ $? == 0 ] || fail 1 "Failed: AWS / lambda / create-function"
LAMBDA_ARN=$(awslocal lambda list-functions --query "Functions[?FunctionName==\`${API_NAME}\`].FunctionArn" --output text --region ${REGION})
echo "Creating REST API..."
awslocal apigateway create-rest-api \
--region ${REGION} \
--name ${API_NAME}
[ $? == 0 ] || fail 2 "Failed: AWS / apigateway / create-rest-api"
API_ID=$(awslocal apigateway get-rest-apis --query "items[?name==\`${API_NAME}\`].id" --output text --region ${REGION})
PARENT_RESOURCE_ID=$(awslocal apigateway get-resources --rest-api-id ${API_ID} --query 'items[?path==`/`].id' --output text --region ${REGION})
awslocal apigateway create-resource \
--region ${REGION} \
--rest-api-id ${API_ID} \
--parent-id ${PARENT_RESOURCE_ID} \
--path-part "{somethingId}"
[ $? == 0 ] || fail 3 "Failed: AWS / apigateway / create-resource"
RESOURCE_ID=$(awslocal apigateway get-resources --rest-api-id ${API_ID} --query 'items[?path==`/{somethingId}`].id' --output text --region ${REGION})
awslocal apigateway put-method \
--region ${REGION} \
--rest-api-id ${API_ID} \
--resource-id ${RESOURCE_ID} \
--http-method GET \
--request-parameters "method.request.path.somethingId=true" \
--authorization-type "NONE" \
[ $? == 0 ] || fail 4 "Failed: AWS / apigateway / put-method"
echo "Creating REST API integration..."
awslocal apigateway put-integration \
--region ${REGION} \
--rest-api-id ${API_ID} \
--resource-id ${RESOURCE_ID} \
--http-method GET \
--type AWS_PROXY \
--integration-http-method POST \
--uri arn:aws:apigateway:${REGION}:lambda:path/2015-03-31/functions/${LAMBDA_ARN}/invocations \
--passthrough-behavior WHEN_NO_MATCH \
[ $? == 0 ] || fail 5 "Failed: AWS / apigateway / put-integration"
echo "Creating REST API deployment..."
awslocal apigateway create-deployment \
--region ${REGION} \
--rest-api-id ${API_ID} \
--stage-name ${STAGE} \
[ $? == 0 ] || fail 6 "Failed: AWS / apigateway / create-deployment"
ENDPOINT=$LOCALSTACK_ENDPOINT/restapis/${API_ID}/${STAGE}/_user_request_
echo "API endpoint:"
echo ${ENDPOINT}
echo -e "\nAll good..."
Resources
- Thanks to this amazing gist
- Localstack’s AWS CLI
- AWS documentation