Use AWS App Runner, DynamoDB, and Cdk To Deploy and Run a Cloud-native Go App
Earlier, I covered a Serverless URL shortener application on AWS using DynamoDB
, AWS Lambda and API Gateway.
In this blog post, we will deploy that as a REST API on AWS App Runner and continue to use DynamoDB
as the database. AWS App Runner is a compute service that makes it easy to deploy applications from a container image (or source code), manage their scalability, deployment pipelines, and more.
With the help of a practical example presented in this blog, you will:
- Learn about AWS App Runner, and how to integrate it with
DynamoDB
- Run simple benchmarks to explore the scalability characteristics of your App Runner service as well as
DynamoDB
- Apply "Infrastructure-as-Code" with AWS CDK Go and deploy the entire stack, including the database, application, and other AWS resources.
- Also, see the DynamoDB Go SDK (v2) in action and some of the basic operations such as
PutItem
,GetItem
.
Let’s Start by Deploying the URL Shortener Application
Before you begin, make sure you have the Go programming language (v1.16 or higher) and AWS CDK installed.
Clone the project and change it to the right directory:
git clone https://github.com/abhirockzz/apprunner-dynamodb-golang
cd cdk
To start the deployment...
Run cdk deploy
and provide your confirmation to proceed. The subsequent sections will provide a walk-through of the CDK code for you to better understand what's going on.
cdk deploy
This will start creating the AWS resources required for our application.
If you want to see the AWS CloudFormation template which will be used behind the scenes, run
cdk synth
and check thecdk.out
folder
You can keep track of the progress in the terminal or navigate to the AWS console: CloudFormation > Stacks > DynamoDBAppRunnerStack
Once all the resources are created, you should have the DynamoDB
table, the App Runner Service (along with the related IAM roles, etc.).
URL Shortener Service on App Runner
You should see the landing page of the App Runner service that was just deployed.
Also, look at the Service Settings under Configuration which shows the environment variables (configured at runtime by CDK), as well as the, compute resources (1 VCPU
and 2 GB
) that we specified
Our URL Shortener Is Ready!
The application is relatively simple and exposes two endpoints:
- To create a short link for a URL
- Access the original URL via the short link
To try out the application, you need to get the endpoint URL provider by the App Runner service. It's available in the stack output (in the terminal or the Outputs tab in the AWS CloudFormation console for your Stack):
First, export the App Runner service endpoint as an environment variable,
export APP_URL=<enter App Runner service URL>
# example
export APP_URL=https://jt6jjprtyi.us-east-1.awsapprunner.com
Invoke it with a URL that you want to access via a short link.
curl -i -X POST -d 'https://abhirockzz.github.io/' $APP_URL
# output
HTTP/1.1 200 OK
Date: Thu, 21 Jul 2022 11:03:40 GMT
Content-Length: 25
Content-Type: text/plain; charset=utf-8
{"ShortCode":"ae1e31a6"}
You should get a JSON
response with a short code and see an item in the DynamoDB
table as well:
You can continue to test the application with a few other URLs.
To access the URL associated with the shortcode
... enter the following in your browser http://<enter APP_URL>/<shortcode>
For example, when you enter https://jt6jjprtyi.us-east-1.awsapprunner.com/ae1e31a6
, you will be redirected to the original URL.
You can also use curl
. Here is an example:
export APP_URL=https://jt6jjprtyi.us-east-1.awsapprunner.com
curl -i $APP_URL/ae1e31a6
# output
HTTP/1.1 302 Found
Location: https://abhirockzz.github.io/
Date: Thu, 21 Jul 2022 11:07:58 GMT
Content-Length: 0
Auto-scaling in Action
Both App Runner and DynamoDB
are capable of scaling up (and down) according to workload.
AWS App Runner
AWS App Runner automatically scales up the number of instances in response to an increase in traffic and scales them back when the traffic decreases.
This is based on AutoScalingConfiguration which is driven by the following user-defined properties - Max concurrency, Max size and Min size. For details, refer to Managing App Runner automatic scaling
Here is the auto-scale configuration for the URL shortener App Runner Service:
DynamoDB
In the case of On-demand mode, DynamoDB
instantly accommodates your workloads as they ramp up or down to any previously reached traffic level. The provisioned mode requires us to specify the number of reads and writes per second that you require for your application, but you can use auto-scaling to adjust your table’s provisioned capacity automatically in response to traffic changes.
Let’s Run Some Tests
We can run a simple benchmark and witness how our service reacts. I will be using a load testing tool called hey but you can also do use Apache Bench etc.
Here is what we'll do:
- Start off with a simple test and examine the response.
- Ramp up the load such that it breaches the provisioned capacity for the
DynamoDB
table. - Update the
DynamoDB
table capacity and repeat.
Install hey and execute a basic test - 200
requests with 50
workers concurrently (as per default settings):
hey $APP_URL/<enter the short code>
#example
hey https://jt6jjprtyi.us-east-1.awsapprunner.com/ae1e31a6
This should be well within the capacity of our stack. Let's bump it to 500
concurrent workers to execute requests for a sustained period of 4 minutes.
hey -c 500 -z 4m $APP_URL/<enter the short code>
#example
hey -c 500 -z 4m https://jt6jjprtyi.us-east-1.awsapprunner.com/ae1e31a6
How Is DynamoDB Doing?
In DynamoDB
console under Table capacity metrics, check Read usage (average units/second):
More importantly, check Read throttled events (count):
Since our table was in Provisioned
capacity mode (with 5 RCU
and WCU
), the requests got throttled and some of them failed.
Edit the table to change its mode to On-demand, re-run the load test. You should not see throttling errors now since DynamoDB
will auto-scale in response to the load.
What About App Runner??
In the Metrics seton in the App Runner console, check the Active Instances count.
You can also track the other metrics and experiment with various load capacities
Alright, now that you've actually seen what the application does and examined the basic scalability characteristics of the stack, let's move on to the how.
But, before that...
Don't forget to delete resources
Once you're done, to delete all the services, simply use:
cdk destroy
AWS CDK Code Walk-through...
We will go through the key parts of the NewDynamoDBAppRunnerStack
function which define the entire stack required by the URL shortener application (I've omitted some code for brevity).
You can refer to the complete code on GitHub
We start by defining a DynamoDB
table with shorturl
as the Partition key (Range/Sort key is not required for our case). Note that the BillingMode
attribute decides the table capacity mode, which is Provisioned in this case (with 5 RCU
and WCU
). As demonstrated in the previous section, this was chosen on purpose.
func NewDynamoDBAppRunnerStack(scope constructs.Construct, id string, props *DynamoDBAppRunnerStackProps) awscdk.Stack {
//....
dynamoDBTable := awsdynamodb.NewTable(stack, jsii.String("dynamodb-short-urls-table"),
&awsdynamodb.TableProps{
PartitionKey: &awsdynamodb.Attribute{
Name: jsii.String(shortCodeDynamoDBAttributeName),
Type: awsdynamodb.AttributeType_STRING,
},
BillingMode: awsdynamodb.BillingMode_PROVISIONED,
ReadCapacity: jsii.Number(5),
WriteCapacity: jsii.Number(5),
RemovalPolicy: awscdk.RemovalPolicy_DESTROY,
})
//...
Then, we use awsiam.NewRole to define a new IAM role and also add a policy that allows App Runner to execute actions in DynamoDB
. In this case, we provide granular permissions - GetItem and PutItem.
//...
apprunnerDynamoDBIAMrole := awsiam.NewRole(stack, jsii.String("role-apprunner-dynamodb"),
&awsiam.RoleProps{
AssumedBy: awsiam.NewServicePrincipal(jsii.String("tasks.apprunner.amazonaws.com"), nil),
})
apprunnerDynamoDBIAMrole.AddToPolicy(awsiam.NewPolicyStatement(&awsiam.PolicyStatementProps{
Effect: awsiam.Effect_ALLOW,
Actions: jsii.Strings("dynamodb:GetItem", "dynamodb:PutItem"),
Resources: jsii.Strings(*dynamoDBTable.TableArn())}))
awsecrassets.NewDockerImageAsset allows us to create and push our application Docker image to ECR - with a single line of code.
//...
appDockerImage := awsecrassets.NewDockerImageAsset(stack, jsii.String("app-image"),
&awsecrassets.DockerImageAssetProps{
Directory: jsii.String(appDirectory)})
Once all the pieces are ready, we define the App Runner Service. Notice how it references the information required by the application:
- The name of the
DynamoDB
table (defined previously) is seeded asTABLE_NAME
env var (required by the application) - The Docker image that we defined is directly used by the
Asset
attribute - The IAM role that we defined is attached to the App Runner service as an Instance Role
The instance role is an optional role that App Runner uses to provide permissions to AWS service actions that your service's compute instances need.
Note that an alpha version (at the time of writing) of the L2 App Runner CDK construct has been used and this is much simple compared to the CloudFormation based L1 construct. It offers a convenient NewService function with which you can define the App Runner Service including the source (locally available in this case), the IAM roles (Instance and Access), etc.
//...
app := awscdkapprunneralpha.NewService(stack, jsii.String("apprunner-url-shortener"),
&awscdkapprunneralpha.ServiceProps{
Source: awscdkapprunneralpha.NewAssetSource(
&awscdkapprunneralpha.AssetProps{
ImageConfiguration: &awscdkapprunneralpha.ImageConfiguration{Environment: &map[string]*string{
"TABLE_NAME": dynamoDBTable.TableName(),
"AWS_REGION": dynamoDBTable.Env().Region},
Port: jsii.Number(appPort)},
Asset: appDockerImage}),
InstanceRole: apprunnerDynamoDBIAMrole,
Memory: awscdkapprunneralpha.Memory_TWO_GB(),
Cpu: awscdkapprunneralpha.Cpu_ONE_VCPU(),
})
app.ApplyRemovalPolicy(awscdk.RemovalPolicy_DESTROY)
Wrap up
This brings us to the end of this blog post! You explored a URL shortener application that exposed REST APIs, used DynamoDB
as its persistent store, and deployed it to AWS App Runner. Then we looked at how the individual services scaled elastically in response to the workload. Finally, we also explored the AWS CDK code that made it possible to define the application and its infrastructure as (Go) code.
Happy building!