Use AWS App Runner, DynamoDB and CDK to deploy and run a Cloud native Go app
Tags: tutorial, database, go, aws
Published: 2022-08-17 03:06:00

Earlier, I wrote about a Serverless URL shortener application on AWS using `DynamoDB`, AWS Lambda and API Gateway. 

{% embed https://dev.to/abhirockzz/build-a-serverless-url-shortener-with-go-10i2 %}


In this blog post, we will deploy that as a REST API on [AWS App Runner](https://docs.aws.amazon.com/apprunner/latest/dg/what-is-apprunner.html) 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](https://docs.aws.amazon.com/cdk/v2/guide/work-with-cdk-go.html) and deploy the entire stack, including the database, application and other AWS resources.
- Also see the [DynamoDB Go SDK](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/dynamodb) (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](https://go.dev/dl/) (**v1.16** or higher) and [AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html#getting_started_install) installed.

Clone the project and change to the right directory:

```bash
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.

```bash
cdk deploy
```


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nt1obuvpqfot59tb3f55.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c49d7m4zpc3ljtipg9c7.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iph48op971lzi7e6vjfh.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ozdgw81kha28yqa51unv.png)


### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kqosg5bz3vlauqytonb1.png)

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

```bash
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.

```bash
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:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xkjxc4kbroznezx4x7ww.png)

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:

```bash
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](https://docs.aws.amazon.com/apprunner/latest/dg/manage-autoscaling.html)

Here is the auto-scale configuration for the URL shortener App Runner Service:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e7ft2uy31myffindci75.png)

**DynamoDB**

In case of [On-demand mode](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadWriteCapacityMode.html#HowItWorks.OnDemand), `DynamoDB` instantly accommodates your workloads as they ramp up or down to any previously reached traffic level. [Provisioned mode](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadWriteCapacityMode.html#HowItWorks.ProvisionedThroughput.Manual) 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](https://github.com/rakyll/hey#installation) 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](https://github.com/rakyll/hey#installation) and execute a basic test - `200` requests with `50` workers concurrently (as per default settings):

```bash
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.

```bash
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)**:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xog3sk5bl1t1msfguich.png)

More importantly, check **Read throttled events (count)**:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/81a2jjwup3thdocbjax0.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/59vxiaszir8bmpblquuc.png)

**What about App Runner??**

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


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g4uahxo7sh87ugtdqxqj.png)


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

```bash
cdk destroy
```

## 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](https://github.com/abhirockzz/apprunner-dynamodb-golang/tree/master/cdk)

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](https://pkg.go.dev/github.com/aws/aws-cdk-go/awscdk/v2/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](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html) and [PutItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html).

```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](https://pkg.go.dev/github.com/aws/aws-cdk-go/awscdk/v2/awsecrassets#NewDockerImageAsset) allows 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 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](https://docs.aws.amazon.com/apprunner/latest/dg/security_iam_service-with-iam.html)

> 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](https://pkg.go.dev/github.com/aws/aws-cdk-go/awscdkapprunneralpha/v2) has been used and this is much simple compared to the [CloudFormation based L1 construct](https://pkg.go.dev/github.com/aws/aws-cdk-go/awscdk/v2/awsapprunner). It offers a convenient [NewService](https://pkg.go.dev/github.com/aws/aws-cdk-go/awscdkapprunneralpha/v2#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 is possible to define the application and its infrastructure as (Go) code.

Happy building!