In this example we will show how you can deploy a serverless application written in Golang and deploy it in multiple regions using the AWS CDK with Python as the configuration language.
The stack running our application will be composed out of the following resources:
- API Gateway with custom domain names
- ACM Certificates
- Lambda function (written in Golang)
NOTE: The code used in this post can be found on GitHub
How to install AWS CDK
This is rather straighforward
npm install -g aws-cdk
Bootstrap a new project with AWS CDK
For this example we will bootstrap the project in a folder named
serverless-multi-region
.
cd serverless-multi-region
cdk init --language python
NOTE: if you have Git installed cdk init will also initialize a Git repository.
Since we are using Python it’s a best practice to use a virtualenv so we can install additional Python libraries without impacting our system installed libraries. cdk init already provides everything to make this easy for us and all we need to do is run:
source .env/bin/activate
python -m pip install -r requirements.txt
NOTE: You can also use pipenv
if you want but then you’ll need to perform
some small hacks that might bite you later on so we’ll keep it simple for now.
Project structure
Before we get started lets have a look at how the project is structured after bootstrapping.
.
├── app.py
├── serverless_multi_region
│ ├── serverless_multi_region_stack.py
│ └── __init__.py
├── cdk.json
├── README.md
├── requirements.txt
├── setup.py
└── source.bat
app.py
: This is the entrypoint for the stacks we want to deploy. Here we will
import all the stacks from our module folder . We’ll make some changes later on
by adding more stacks for all the different regions.
serverless_multi_region
: this folder is used as a Python module and contains all the
stacks used to create the AWS resources. The CDK creates a default stack with the following
content.
cdk.json
: CDK configuration file that defines what needs to be executed to generate the CDK construct tree.
README.md
: Bootstrapped readme file describing the project.
requirements
and setup.py
are used to install the necessary Python
libraries in our Virtualenv.
.gitignore
: Initial .gitignore file to prevent committing CDK staging and
local Python data
Serverless app code
We’ll keep the code of the serverless application simple by just printing
Serverless runs on servers
when we issue a GET request to the /test
endpoint
of our API.
To keep everything clean we’ll store the application code in a src
folder
which we create in the root of our project.
CDK
Lambda function
Before adding the Lambda function to our stack we need to build the golang
binary to which we’ll point our handler to. The binary will be put in a dist
folder so we can exclude it from being committed to Git.
go build -o dist/myfunction ./src/main.go
For each AWS Resource Type we want to create using the CDK we need to install
the necessary Python libraries to enable support for these resources. For our
Lambda function we need both the aws-lambda
and aws-logs
libraries.
python -m pip install aws-cdk.aws-logs aws-cdk.aws-lambda
NOTE: Make sure you are running in a virtualenv
Then we can add our Lambda function to the stack by editing the
ServerlessMultiRegionStack
(which can be found in the
serverless_multi_region
folder.
from aws_cdk import (
core,
aws_lambda as _lambda,
aws_logs
)
import os
class ServerlessMultiRegionStack(core.Stack):
def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
super().__init__(scope, id, **kwargs)
# The code that defines your stack goes here
MyFunction = _lambda.Function(
self,
"MyFunction",
code=_lambda.Code.from_asset("dist"),
handler="myfunction",
runtime=_lambda.Runtime.GO_1_X,
log_retention=aws_logs.RetentionDays.ONE_WEEK,
memory_size=128,
events=[],
environment={
"DNS_SUFFIX_PUB": os.getenv("DNS_SUFFIX_PUB")
}
)
Now we are ready to test and deploy. You can test your code by running cdk synth
. This will generate a Cloudformation template and show it in stdout. If
this runs without errors you are ready to deploy with cdk deploy
cdk synth
...
<output ommited>
...
cdk deploy
IAM Statement Changes
┌───┬────────────────────────────┬────────┬────────────────────────────┬──────────────────────────────┬───────────┐
│ │ Resource │ Effect │ Action │ Principal │ Condition │
├───┼────────────────────────────┼────────┼────────────────────────────┼──────────────────────────────┼───────────┤
│ + │ ${AggregatorFunction/Servi │ Allow │ sts:AssumeRole │ Service:lambda.amazonaws.com │ │
│ │ ceRole.Arn} │ │ │ │ │
├───┼────────────────────────────┼────────┼────────────────────────────┼──────────────────────────────┼───────────┤
│ + │ ${LogRetentionaae0aa3c5b4d │ Allow │ sts:AssumeRole │ Service:lambda.amazonaws.com │ │
│ │ 4f87b02d85b201efdd8a/Servi │ │ │ │ │
│ │ ceRole.Arn} │ │ │ │ │
├───┼────────────────────────────┼────────┼────────────────────────────┼──────────────────────────────┼───────────┤
│ + │ * │ Allow │ logs:DeleteRetentionPolicy │ AWS:${LogRetentionaae0aa3c5b │ │
│ │ │ │ logs:PutRetentionPolicy │ 4d4f87b02d85b201efdd8a/Servi │ │
│ │ │ │ │ ceRole} │ │
└───┴────────────────────────────┴────────┴────────────────────────────┴──────────────────────────────┴───────────┘
IAM Policy Changes
┌───┬──────────────────────────────────────────────────────┬──────────────────────────────────────────────────────┐
│ │ Resource │ Managed Policy ARN │
├───┼──────────────────────────────────────────────────────┼──────────────────────────────────────────────────────┤
│ + │ ${AggregatorFunction/ServiceRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/A │
│ │ │ WSLambdaBasicExecutionRole │
├───┼──────────────────────────────────────────────────────┼──────────────────────────────────────────────────────┤
│ + │ ${LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/Servi │ arn:${AWS::Partition}:iam::aws:policy/service-role/A │
│ │ ceRole} │ WSLambdaBasicExecutionRole │
└───┴──────────────────────────────────────────────────────┴──────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)
Do you wish to deploy these changes (y/n)?
serverless-multi-region-eu: deploying...
[0%] start: Publishing 88d538cad4f5f278bb5e7a4e487063ef747abc747bef069f9cf467937c8487e0:current
[50%] success: Published 88d538cad4f5f278bb5e7a4e487063ef747abc747bef069f9cf467937c8487e0:current
[50%] start: Publishing 67b7823b74bc135986aa72f889d6a8da058d0c4a20cbc2dfc6f78995fdd2fc24:current
[100%] success: Published 67b7823b74bc135986aa72f889d6a8da058d0c4a20cbc2dfc6f78995fdd2fc24:current
serverless-multi-region-eu: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (8/8)
API Gateway
Install the necessary Python CDK libraries for api gateway resources
python -m pip install aws-cdk.aws-apigateway
And import the library
from aws_cdk import (
core,
aws_lambda as _lambda,
aws_logs,
aws_apigateway as apigateway
)
Add the resources to our stack.
my_api = apigateway.RestApi(
self,
"MyApi"
)
myfunction_integration = apigateway.LambdaIntegration(my_function)
test_resource = my_api.root.add_resource("test")
test_resource.add_method("GET", myfunction_integration)
That’s it. These few lines are enough to create:
- Rest API
- resource (GET on
/test
) - Lambda integration with all the necessary permissions.
Update the stack by running cdk deploy
IAM Statement Changes
┌───┬────────────────────────┬────────┬────────────────────────┬────────────────────────┬─────────────────────────┐
│ │ Resource │ Effect │ Action │ Principal │ Condition │
├───┼────────────────────────┼────────┼────────────────────────┼────────────────────────┼─────────────────────────┤
│ + │ ${LogRetentionaae0aa3c │ Allow │ sts:AssumeRole │ Service:lambda.amazona │ │
│ │ 5b4d4f87b02d85b201efdd │ │ │ ws.com │ │
│ │ 8a/ServiceRole.Arn} │ │ │ │ │
├───┼────────────────────────┼────────┼────────────────────────┼────────────────────────┼─────────────────────────┤
│ + │ ${MyApi/CloudWatchRole │ Allow │ sts:AssumeRole │ Service:apigateway.ama │ │
│ │ .Arn} │ │ │ zonaws.com │ │
├───┼────────────────────────┼────────┼────────────────────────┼────────────────────────┼─────────────────────────┤
│ + │ ${MyFunction.Arn} │ Allow │ lambda:InvokeFunction │ Service:apigateway.ama │ "ArnLike": { │
│ │ │ │ │ zonaws.com │ "AWS:SourceArn": "arn │
│ │ │ │ │ │ :${AWS::Partition}:exec │
│ │ │ │ │ │ ute-api:${AWS::Region}: │
│ │ │ │ │ │ ${AWS::AccountId}:${MyA │
│ │ │ │ │ │ pi49610EDF}/${MyApi/Dep │
│ │ │ │ │ │ loymentStage.prod}/GET/ │
│ │ │ │ │ │ test" │
│ │ │ │ │ │ } │
│ + │ ${MyFunction.Arn} │ Allow │ lambda:InvokeFunction │ Service:apigateway.ama │ "ArnLike": { │
│ │ │ │ │ zonaws.com │ "AWS:SourceArn": "arn │
│ │ │ │ │ │ :${AWS::Partition}:exec │
│ │ │ │ │ │ ute-api:${AWS::Region}: │
│ │ │ │ │ │ ${AWS::AccountId}:${MyA │
│ │ │ │ │ │ pi49610EDF}/test-invoke │
│ │ │ │ │ │ -stage/GET/test" │
│ │ │ │ │ │ } │
├───┼────────────────────────┼────────┼────────────────────────┼────────────────────────┼─────────────────────────┤
│ + │ ${MyFunction/ServiceRo │ Allow │ sts:AssumeRole │ Service:lambda.amazona │ │
│ │ le.Arn} │ │ │ ws.com │ │
├───┼────────────────────────┼────────┼────────────────────────┼────────────────────────┼─────────────────────────┤
│ + │ * │ Allow │ logs:DeleteRetentionPo │ AWS:${LogRetentionaae0 │ │
│ │ │ │ licy │ aa3c5b4d4f87b02d85b201 │ │
│ │ │ │ logs:PutRetentionPolic │ efdd8a/ServiceRole} │ │
│ │ │ │ y │ │ │
└───┴────────────────────────┴────────┴────────────────────────┴────────────────────────┴─────────────────────────┘
IAM Policy Changes
┌───┬──────────────────────────────────────────────────────┬──────────────────────────────────────────────────────┐
│ │ Resource │ Managed Policy ARN │
├───┼──────────────────────────────────────────────────────┼──────────────────────────────────────────────────────┤
│ + │ ${LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/Servi │ arn:${AWS::Partition}:iam::aws:policy/service-role/A │
│ │ ceRole} │ WSLambdaBasicExecutionRole │
├───┼──────────────────────────────────────────────────────┼──────────────────────────────────────────────────────┤
│ + │ ${MyApi/CloudWatchRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/A │
│ │ │ mazonAPIGatewayPushToCloudWatchLogs │
├───┼──────────────────────────────────────────────────────┼──────────────────────────────────────────────────────┤
│ + │ ${MyFunction/ServiceRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/A │
│ │ │ WSLambdaBasicExecutionRole │
└───┴──────────────────────────────────────────────────────┴──────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)
Do you wish to deploy these changes (y/n)? y
serverless-multi-region-eu: deploying...
[0%] start: Publishing 88d538cad4f5f278bb5e7a4e487063ef747abc747bef069f9cf467937c8487e0:current
[50%] success: Published 88d538cad4f5f278bb5e7a4e487063ef747abc747bef069f9cf467937c8487e0:current
[50%] start: Publishing 67b7823b74bc135986aa72f889d6a8da058d0c4a20cbc2dfc6f78995fdd2fc24:current
[100%] success: Published 67b7823b74bc135986aa72f889d6a8da058d0c4a20cbc2dfc6f78995fdd2fc24:current
serverless-multi-region-eu: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (17/17)
✅ serverless-multi-region-eu
Outputs:
serverless-multi-region-eu.MyApiEndpoint869ABE96 = https://z0nqyt0r1i.execute-api.eu-west-1.amazonaws.com/prod/
Stack ARN:
arn:aws:cloudformation:eu-west-1:440357826049:stack/serverless-multi-region-eu/5827c510-169d-11eb-b147-0ae0d664c880
Notice the link to the base URI of our deployment which can be used to test the API. This URI is mainly for testing purposes and later on we’ll change that to a proper domain name.
❯ curl -s https://z0nqyt0r1i.execute-api.eu-west-1.amazonaws.com/prod/test | jq
{
"message": "Serverless runs on servers."
}
NOTE: By default a prod
stage is created in API Gateway during deployment
as you can see in the URI which ends with /prod
. To test your API, you need to
add the resource. In this case /test
.
Multi region deploy
To have our API deployed in multiple regions we’ll create a second stack in another
region (us-east-1
in this example). This can simply be accomplished by
adding a stack to app.py
using different Environments.
Notice that we are using the same stack as for eu-west-1
but just passing
different Environment options.
#!/usr/bin/env python3
from aws_cdk import core
from serverless_multi_region.serverless_multi_region_stack import ServerlessMultiRegionStack
app = core.App()
env_EU = core.Environment(region="eu-west-1")
env_US = core.Environment(region="us-east-1")
ServerlessMultiRegionStack(app, "serverless-multi-region-eu", env=env_EU)
ServerlessMultiRegionStack(app, "serverless-multi-region-us", env=env_US)
app.synth()
You could now run a cdk deploy
for each stack separately but the CDK also allows the
use of wildcards so we can run:
cdk deploy serverless-multi-region-*`
If that finished successful you should have 1 API deployed in eu-west-1 and onther one in us-east-1 each with their own base URI.
Custom Domain Names
The base URIs generated during the deployment are useful for testing but not really convenient for end users of our service. Besides that, we also want users to connect to the REST API geographically closesd to them. This is where Custom Domains can be used for.
In this example we assume that:
- there is already a Route53 zone created
- an SSL/TLS certificate in AWS Certificate Manager (ACM) in all the regions where we want to deploy the API. This is needed because we use regional custom domain named instead of edge-opimized custom domain names for which the certificate needs to be available in us-east-1
Install the necessary Python libraries:
python pip install aws-cdk.aws-route53-targets aws-cdk.aws-route53 aws-cdk.aws-certificatemanager
And import them.
from aws_cdk import (
core,
aws_lambda as _lambda,
aws_logs,
aws_apigateway as apigateway,
aws_route53 as route53,
aws_route53_targets as route53_targets,
aws_certificatemanager as certificatemanager
)
import os
Then we can add the resources to our stack
# -- DNS Configuration
# Get the Route53 Zone
dns_zone = route53.HostedZone.from_lookup(
self,
"ApiZone",
private_zone=False,
domain_name="zoolite.eu"
)
# Get the certificate
api_domain_cert = certificatemanager.Certificate.from_certificate_arn(
self,
"DomainCertificate",
cert_arn
)
# Create the Custom Domain
api_dns_name = apigateway.DomainName(
self,
"ApiDomainName",
domain_name="my-api.zoolite.eu",
endpoint_type=apigateway.EndpointType.REGIONAL,
certificate=api_domain_cert,
security_policy=apigateway.SecurityPolicy.TLS_1_2
)
#api_dns_name.add_base_path_mapping(my_api, base_path="test")
api_dns_name.add_base_path_mapping(my_api)
# Create the Route53 record
api_dns_record = route53.ARecord(
self,
"MyAPiDNSRecord",
target=route53.RecordTarget.from_alias(
route53_targets.ApiGatewayDomain(api_dns_name)
),
record_name="my-api",
zone=dns_zone
)
# Configure latency based routing on record
recordset = api_dns_record.node.default_child
recordset.region = self.region
recordset.set_identifier = api_dns_record.node.unique_id
ACM Certificate
Because the SSl/TLS certificates are not created in this stack we need to
provide the CDK with the necessary information. This means we need a certificate
resource as a Python ICertificate
class type.
To do this, we can use the from_certificate_arn
method of the Certificate
resource and pass
it the ARN of the certificate as shown here:
# Get the certificate
api_domain_cert = certificatemanager.Certificate.from_certificate_arn(
self,
"DomainCertificate",
cert_arn
)
The only thing left to do is define the cert_arn
parameter. One way this can
be done is by passing it as a parameter to our Stack like this in app.py
:
#!/usr/bin/env python3
from aws_cdk import core
from serverless_multi_region.serverless_multi_region_stack import ServerlessMultiRegionStack
app = core.App()
env_EU = core.Environment(account="123456789012", region="eu-west-1")
env_US = core.Environment(account="123456789012", region="us-east-1")
ServerlessMultiRegionStack(
app,
"serverless-multi-region-eu",
cert_arn="arn:aws:acm:eu-west-1:123456789012:certificate/6c626c39-7573-54bc-b458-efd959e0170a",
env=env_EU,
)
ServerlessMultiRegionStack(
app,
"serverless-multi-region-us",
cert_arn="arn:aws:acm:us-east-1:123456789012:certificate/551e8c78-e6e1-44e7-bf01-bb55cc19fd23",
env=env_US,
)
app.synth()
And then add the parameter to the module itself in serverless_multi_region_stack.py
def __init__(self, scope: core.Construct, id: str, cert_arn, **kwargs) -> None:
Troubleshooting
While building this I encounterd some issues like this one and thought I’d leave it here in case someone encounters the same:
This CDK CLI is not compatible with the CDK library used by your application. Please upgrade the CLI to the latest version.
(Cloud assembly schema version mismatch: Maximum schema version supported is 5.0.0, but found 6.0.0)
This can be fixed by updating the CDK
npm update -g aws-cdk
Conclusion
With the AWS CDK you can define your infrastructure/resources in a “real” programing language which allows you to use all the primitives that langauge provides. You can also leverage all the features available in your your IDE/editor that language (like autocompletion, syntax checks, ..)
The CDK also abstracts away a lot more compared to Cloudformation where every property and resource needs to be specified. This keeps things more condense
As the saying “With great power comes great responsibility” goes, you’ll need to be careful not to make things to complicated. And the fact that there is more abstraction doesn’t mean you don’t need to know these details anymore. On the contrary, I would advise newcomers to start with Cloudformation as it will learn you a lot about how the AWS services work and what their “quirks” are (and there are quite a lot of them 😃.