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:

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:

Shell
 
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.

Shell
 
cdk deploy

creating the AWS resources required for our application

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 the cdk.out folder

You can keep track of the progress in the terminal or navigate to the AWS console: CloudFormation > Stacks > DynamoDBAppRunnerStack

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.

landing page of the App Runner service

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

Service settings

Our URL Shortener Is Ready!

The application is relatively simple and exposes two endpoints:

  1. To create a short link for a URL
  2. 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):

Output

First, export the App Runner service endpoint as an environment variable,

Go
 
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.

Shell
 
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:

items returned

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:

Shell
 
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:

Auto scaling

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:

  1. Start off with a simple test and examine the response.
  2. Ramp up the load such that it breaches the provisioned capacity for the DynamoDB table.
  3. Update the DynamoDB table capacity and repeat.

Install hey and execute a basic test - 200 requests with 50 workers concurrently (as per default settings):

Shell
 
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.

Shell
 
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):

Read usage (average units/second)

More importantly, check Read throttled events (count):

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.

Edit the table to change its mode to On-demand

What About App Runner??

In the Metrics seton in the App Runner console, check the Active Instances count.

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:

Shell
 
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.

Go
 
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.

Go
 
//...
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.

Go
 
//...
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 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.

Go
 
//...
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!

 

 

 

 

Top