This post describes the steps needed for setting up automatic SSL certificates creation and renewal, using Let's Encrypt as the automated Certificate Authority, which provides a well-maintained API.

acme-dns-route53 is the tool to obtain SSL certificates from Let’s Encrypt using DNS-01 challenge with Route53 and Amazon Certificate Manager by AWS. acme-dns-route53 also has the built-in functionality for using this tool inside AWS Lambda, and this is what we are going to do.

This post is broken down into 4 sections:

build a self-contained, deployable zip-file

creating an IAM role for lambda function that gives it the necessary permissions to execute

creating a lambda function which executes acme-dns-route53

creating a CloudWatch timer that triggers a lambda function twice a day

Note: before starting, make sure that GoLang 1.9+ and AWS CLI already installed.

Built a self-contained, deployable zip-file

acme-dns-route53 is written in GoLang and supporting version not less than 1.9. We need to create a self-contained, deployable zip-file which contains executable of acme-dns-route53 tool inside.

The first step is to build an executable from remote GitHub repo of acme-dns-route53 tool using go install command:

$ env GOOS=linux GOARCH=amd64 go install github.com/begmaroman/acme-dns-route53

The executable will be installed in $GOPATH/bin directory. Important: as part of this command we're using env to temporarily set two environment variables for the duration for the command ( GOOS=linux and GOARCH=amd64 ). These instruct the Go compiler to create an executable suitable for use with a Linux OS and amd64 architecture — which is what it will be running on when we deploy it to AWS.

AWS requires us to upload our lambda functions in a zip file, so let's make an acme-dns-route53.zip zip file containing the executable we just made:

$ zip -j ~/acme-dns-route53.zip $GOPATH/bin/acme-dns-route53

Note that the executable must be in the root of the zip file — not in a folder within the zip file. To ensure this I've used the -j flag in the snippet above to junk directory names.

Now the zip-file can be deployed, but it still needs permissions to run.

Creating an IAM role for lambda function that gives it the necessary permissions to execute

We need to set up an IAM role which defines the permission that our lambda function will have when it is running.

For now, let's set up a lambda-acme-dns-route53-executor role and attach the AWSLambdaBasicExecutionRole managed policy to it. This will give our lambda function the basic permissions it needs to run and log to the AWS CloudWatch service.

First, we have to create a trust policy JSON file. This will essentially instruct AWS to allow lambda services to assume the lambda-acme-dns-route53-executor role:

$ touch ~/lambda-acme-dns-route53-executor-policy.json

The content of the created JSON file should be the following:

{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "logs:CreateLogGroup" ], "Resource": "arn:aws:logs:<AWS_REGION>:<AWS_ACCOUNT_ID>:*" }, { "Effect": "Allow", "Action": [ "logs:PutLogEvents", "logs:CreateLogStream" ], "Resource": "arn:aws:logs:<AWS_REGION>:<AWS_ACCOUNT_ID>:log-group:/aws/lambda/acme-dns-route53:*" }, { "Sid": "", "Effect": "Allow", "Action": [ "route53:ListHostedZones", "cloudwatch:PutMetricData", "acm:ImportCertificate", "acm:ListCertificates" ], "Resource": "*" }, { "Sid": "", "Effect": "Allow", "Action": [ "sns:Publish", "route53:GetChange", "route53:ChangeResourceRecordSets", "acm:ImportCertificate", "acm:DescribeCertificate" ], "Resource": [ "arn:aws:sns:<AWS_REGION>:<AWS_ACCOUNT_ID>:<TOPIC_NAME>", "arn:aws:route53:::hostedzone/*", "arn:aws:route53:::change/*", "arn:aws:acm:<AWS_REGION>:<AWS_ACCOUNT_ID>:certificate/*" ] } ] }

Then use the aws iam create-role command to create the role with this trust policy:

$ aws iam create-role --role-name lambda-acme-dns-route53-executor \ --assume-role-policy-document ~/lambda-acme-dns-route53-executor-policy.json

Make a note of the returned ARN (Amazon Resource Name) — you'll need this in the next step.

Now the lambda-acme-dns-route53-executor role has been created we need to specify the permissions that the role has. The easiest way to do this is to use the aws iam attach-role-policy command, passing in the ARN of AWSLambdaBasicExecutionRole permission policy like so:

$ aws iam attach-role-policy --role-name lambda-acme-dns-route53-executor \ --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

Note: you can find a list of other permission policies that might be useful here.

Creating a lambda function which executes acme-dns-route53

Now we're ready to actually deploy the lambda function to AWS, which we can do using the aws lambda create-function command. The lambda function needs to be configured with the following options:

AWS_LAMBDA environment variable with value 1 which adjusts the tool for using inside Lambda function.

environment variable with value which adjusts the tool for using inside Lambda function. DOMAINS is the environment variable which contains comma-separated list of domains for which certificates will be issued.

is the environment variable which contains comma-separated list of domains for which certificates will be issued. LETSENCRYPT_EMAIL is the environment variable which contains Let’s Encrypt expiration email.

is the environment variable which contains Let’s Encrypt expiration email. NOTIFICATION_TOPIC is the environment variable which contains SNS Notification Topic ARN.

is the environment variable which contains SNS Notification Topic ARN. STAGING is the environment variable which must contain 1 value for using staging Let’s Encrypt environment or 0 for production environment.

is the environment variable which must contain value for using staging Let’s Encrypt environment or for production environment. RENEW_BEFORE is the number of days defining the period before expiration within which a certificate must be renewed.

is the number of days defining the period before expiration within which a certificate must be renewed. 1024 MB is the memory limit (can be changed if needed).

MB is the memory limit (can be changed if needed). 900 secs (15 min) is the maximum timeout.

secs (15 min) is the maximum timeout. acme-dns-route53 is the handler name of the lambda function.

is the handler name of the lambda function. fileb://~/acme-dns-route53.zip is the created .zip file above.

Go ahead and try to deploying it:

$ aws lambda create-function \ --function-name acme-dns-route53 \ --runtime go1.x \ --role arn:aws:iam::<AWS_ACCOUNT_ID>:role/lambda-acme-dns-route53-executor \ --environment Variables="{AWS_LAMBDA=1,DOMAINS=\"example1.com,example2.com\",LETSENCRYPT_EMAIL=begmaroman@gmail.com,STAGING=0,NOTIFICATION_TOPIC=acme-dns-route53-obtained,RENEW_BEFORE=7}" \ --memory-size 1024 \ --timeout 900 \ --handler acme-dns-route53 \ --zip-file fileb://~/acme-dns-route53.zip { "FunctionName": "acme-dns-route53", "LastModified": "2019-05-03T19:07:09.325+0000", "RevisionId": "e3fadec9-2180-4bff-bb9a-999b1b71a558", "MemorySize": 1024, "Environment": { "Variables": { "DOMAINS": "example1.com,example2.com", "STAGING": "1", "LETSENCRYPT_EMAIL": "your@email.com", "NOTIFICATION_TOPIC": "acme-dns-route53-obtained", "RENEW_BEFORE": "7", "AWS_LAMBDA": "1" } }, "Version": "$LATEST", "Role": "arn:aws:iam::<AWS_ACCOUNT_ID>:role/lambda-acme-dns-route53-executor", "Timeout": 900, "Runtime": "go1.x", "TracingConfig": { "Mode": "PassThrough" }, "CodeSha256": "+2KgE5mh5LGaOsni36pdmPP9O35wgZ6TbddspyaIXXw=", "Description": "", "CodeSize": 8456317, "FunctionArn": "arn:aws:lambda:us-east-1:<AWS_ACCOUNT_ID>:function:acme-dns-route53", "Handler": "acme-dns-route53" }

Creating a CloudWatch timer that triggers a lambda function once a day

The last step is to create a daily trigger for the function. To do this we can:

create a CloudWatch rule with the desired schedule_expression (when should it run).

(when should it run). create a rule target (what should run) specifying the lambda function’s ARN.

give the CloudWatch rule permission to invoke the lambda function.

I’ve pasted my Terraform config for it below, but it’s also very straightforward to do it from either the AWS console or CLI.

# Cloudwatch event rule that runs acme-dns-route53 lambda every 12 hours resource "aws_cloudwatch_event_rule" "acme_dns_route53_sheduler" { name = "acme-dns-route53-issuer-scheduler" schedule_expression = "cron(0 */12 * * ? *)" } # Specify the lambda function to run resource "aws_cloudwatch_event_target" "acme_dns_route53_sheduler_target" { rule = "${aws_cloudwatch_event_rule.acme_dns_route53_sheduler.name}" arn = "${aws_lambda_function.acme_dns_route53.arn}" } # Give CloudWatch permission to invoke the function resource "aws_lambda_permission" "permission" { action = "lambda:InvokeFunction" function_name = "${aws_lambda_function.acme_dns_route53.function_name}" principal = "events.amazonaws.com" source_arn = "${aws_cloudwatch_event_rule.acme_dns_route53_sheduler.arn}" }

Now you too can have 100% automated TLS certificate renewals!