DEV Community

Cover image for Use AWS App Runner, DynamoDB and CDK to deploy and run a Cloud native Go app
Abhishek Gupta for AWS

Posted on • Edited on • Originally published at abhishek1987.Medium

Use AWS App Runner, DynamoDB and CDK to deploy and run a Cloud native Go app

Earlier, I wrote about 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, 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 to the right directory:

git clone https://github.com/abhirockzz/apprunner-dynamodb-golang
cd cdk
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Image description

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 AWS console: CloudFormation > Stacks > DynamoDBAppRunnerStack

Image description

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.

Image description

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

Image description

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

Image description

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
Enter fullscreen mode Exit fullscreen mode

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"}
Enter fullscreen mode Exit fullscreen mode

You should get a JSON response with a short code and see an item in the DynamoDB table as well:

Image description

You can continue to test the application with a few other URLs.

To access the URL associated with the short code

... 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 re-directed 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
Enter fullscreen mode Exit fullscreen mode

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:

Image description

DynamoDB

In case of On-demand mode, DynamoDB instantly accommodates your workloads as they ramp up or down to any previously reached traffic level. 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.

Lets run some tests

We can run a simple benchmarks 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):

hey $APP_URL/<enter the short code>

#example
hey https://jt6jjprtyi.us-east-1.awsapprunner.com/ae1e31a6
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

How is DynamoDB doing?

In DynamoDB console under Table capacity metrics, check Read usage (average units/second):

Image description

More importantly, check Read throttled events (count):

Image description

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.

Image description

What about App Runner??

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

Image description

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
Enter fullscreen mode Exit fullscreen mode

AWS CDK code walk through...

We will go through the keys parts of the NewDynamoDBAppRunnerStack function which defines 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,
        })
  //...
Enter fullscreen mode Exit fullscreen mode

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())}))
Enter fullscreen mode Exit fullscreen mode

awsecrassets.NewDockerImageAsset allows 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)})
Enter fullscreen mode Exit fullscreen mode

Once all the pieces 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 as TABLE_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 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 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)
Enter fullscreen mode Exit fullscreen mode

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 is possible to define the application and its infrastructure as (Go) code.

Happy building!

Top comments (0)