DEV Community

Abhishek Gupta for AWS

Posted on • Originally published at abhishek1987.Medium

Build Cloud-Native apps with AWS App Runner, Redis and AWS CDK

Run your Go app on AWS App Runner service, integrate it with MemoryDB for Redis and use AWS CDK to package and deploy the app along with infrastructure

AWS App Runner allows you to deploy and run cloud-native applications in a fast, simple, and cost-effective manner. You can choose the programming language of your choice since App Runner can deploy directly from source code (in GitHub for example) or a Docker container image (from private or public repo in ECR) - all this without worrying about provisioning and managing the underlying infrastructure.

Image description

This blog post showcases how to run a Go application on AWS App Runner which will further integrate with Amazon MemoryDB for Redis (a Redis compatible, durable, in-memory database service). You will deploy the application and its infrastructure using AWS CDK. This includes App Runner VPC Connector config to connect with MemoryDB as well as using CDK to package your Go application as a Docker image, uploading to ECR and seamlessly deployment to App Runner (no manual steps needed). I will close the blog with a brief walk through of the CDK code which is written in Go, thanks to the CDK Go support (which is in Developer Preview at the time of writing).

The code is available on GitHub

Before you proceed, make sure you have the following ready:

Pre-requisites

  • Create an AWS account (if you do not already have one) and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources.
  • Install and configure AWS CLI
  • Install and bootstrap AWS CDK
  • Setup Docker
  • Install Go

Use a single command to deploy infra and app

Clone the GitHub repo and change to the right directory:

git clone https://github.com/abhirockzz/go-redis-apprunner

cd cdk
Enter fullscreen mode Exit fullscreen mode

Set environment variables:

  • App Runner service name and port
  • A password of your choice for MemoryDB (this is just for demonstration purposes - for production, you will have specific processes in place to handle sensitive info)

Be mindful of the password requirements. From the documentation:

"In particular, be aware of these user password constraints when using ACLs for MemoryDB:

  • Passwords must be 16–128 printable characters.
  • The following non-alphanumeric characters are not allowed: , "" / @."
export APPRUNNER_SERVICE_NAME=apprunner-memorydb-go-app
export APPRUNNER_SERVICE_PORT=8080
export MEMORYDB_PASSWORD=<password of your choice e.g. P@ssw0rd12345678>

cdk deploy --all
Enter fullscreen mode Exit fullscreen mode

This will kick-off the stack creation. All you need to do now is .... wait.

Why?? Well, that's because CDK is doing everything for us behind the scenes. Starting with VPC (and subnets, NAT gateway etc.), MemoryDB cluster, security groups, packaging and uploading our app as a Docker image (to a private ECR repo) and finally deploying it as a App Runner service - that's quite a lot!

Feel free to navigate to the AWS Console > CloudFormation > Stacks to see what's going on...

Image description

Once both the Stack run to completion, you can explore all the components:

MemoryDB Subnet Group - VPC and two subnets, each in one availability zone.

Image description

MemoryDB cluster - CDK code was hard-coded to create two-node cluster (single shard) i.e. with one primary and one replica node. Note that the primary and replica nodes are spread across different AZs (as per Subnet Group config above)

Image description

Also, the ACL and user setting for MemoryDB - this will be used for authentication (username/password) and access control (authorization).

Image description

Remember that MemoryDB runs in a different VPC and it's not possible to connect your App Runner service to it by default. You need to associate your the service to the MemoryDB VPC - that's where App Runner VPC connector comes in and allows it to communicate with MemoryDB.

The VPC and subnets are same as that of MemoryDB. Notice the security group as well - more on this in a minute

Image description

Check the MemoryDB security group. There is an Inbound rule that says: the the source security group (that is associated with App Runner VPC Connector config in this case) can access TCP port 6379 of instance associated with target security group (MemoryDB in this case)

Image description

You can also confirm the IAM access role that was created for App Runner as well as the service environment variables:

Environment variables have been used for demonstration purposes. For production apps, you should use AWS Secrets Manager for storing and retrieving sensitive information such as passwords, auth tokens etc.

Image description

Test the application

The application itself is fairly simple and exposes a couple of HTTP endpoints to create sample data.
Locate the App Runner service URL from the details page.

Image description

You can test the application using any HTTP client (I have used curl in this example):

# create a couple of user entries
curl -i -X POST -d '{"email":"user1@foo.com", "name":"user1"}' <enter APPRUNNER_APP_URL>
curl -i -X POST -d '{"email":"user2@foo.com", "name":"user2"}' <enter APPRUNNER_APP_URL>

HTTP/1.1 200 OK
Date: Fri, 20 May 2022 08:05:06 GMT
Content-Length: 0

# search for user via email
curl -i <enter APPRUNNER_APP_URL>/user2@foo.com

HTTP/1.1 200 OK
Date: Fri, 20 May 2022 08:05:11 GMT
Content-Length: 41
Content-Type: text/plain; charset=utf-8

{"email":"user2@foo.com","name":"user2"}

# is a user does not exist
curl -i <enter APPRUNNER_APP_URL>/not_there@foo.com

HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Fri, 20 May 2022 08:05:36 GMT
Content-Length: 38

user does not exist not_there@foo.co
Enter fullscreen mode Exit fullscreen mode

Clean up

Once you've completed this tutorial, delete the stack(s):

cdk destroy --all
Enter fullscreen mode Exit fullscreen mode

Quick walk through of the CDK code

The Infrastructure part (IaaC to be specific) is comprised of two (CDK) Stacks (in the context of a single CDK App).

I will provide a walk through of the CDK code which is written in Go, thanks to the CDK Go support (which is Developer Preview at the time of writing).

Please note that some of the code has been redacted/omitted for brevity - you can always refer to complete code in the GitHub repo

Here is the first stack:

To summarise:

  • A single line of code to create VPC and related components!
  • We create ACL, User, Subnet group for MemoryDB cluster and refer to them when during cluster creation with awsmemorydb.NewCfnCluster
  • We also create required security groups for MemoryDB as well as AppRunner (VPC config)
...
vpc = awsec2.NewVpc(stack, jsii.String("demo-vpc"), nil)

authInfo := map[string]interface{}{"Type": "password", "Passwords": []string{getMemoryDBPassword()}}
user = awsmemorydb.NewCfnUser(stack, jsii.String("demo-memorydb-user"), &awsmemorydb.CfnUserProps{UserName: jsii.String("demo-user"), AccessString: jsii.String(accessString), AuthenticationMode: authInfo})
acl := awsmemorydb.NewCfnACL(stack, jsii.String("demo-memorydb-acl"), &awsmemorydb.CfnACLProps{AclName: jsii.String("demo-memorydb-acl"), UserNames: &[]*string{user.UserName()}})

//snip...

subnetGroup := awsmemorydb.NewCfnSubnetGroup(stack, jsii.String("demo-memorydb-subnetgroup"), &awsmemorydb.CfnSubnetGroupProps{SubnetGroupName: jsii.String("demo-memorydb-subnetgroup"), SubnetIds: &subnetIDsForSubnetGroup})

memorydbSecurityGroup := awsec2.NewSecurityGroup(stack, jsii.String("memorydb-demo-sg"), &awsec2.SecurityGroupProps{Vpc: vpc, SecurityGroupName: jsii.String("memorydb-demo-sg"), AllowAllOutbound: jsii.Bool(true)})

memorydbCluster = awsmemorydb.NewCfnCluster(stack, jsii.String("demo-memorydb-cluster"), &awsmemorydb.CfnClusterProps{ClusterName: jsii.String("demo-memorydb-cluster"), NodeType: jsii.String(memoryDBNodeType), AclName: acl.AclName(), NumShards: jsii.Number(numMemoryDBShards), EngineVersion: jsii.String(memoryDBRedisEngineVersion), Port: jsii.Number(memoryDBRedisPort), SubnetGroupName: subnetGroup.SubnetGroupName(), NumReplicasPerShard: jsii.Number(numMemoryDBReplicaPerShard), TlsEnabled: jsii.Bool(true), SecurityGroupIds: &[]*string{memorydbSecurityGroup.SecurityGroupId()}, ParameterGroupName: jsii.String(memoryDBDefaultParameterGroupName)})

//snip...

appRunnerVPCConnSecurityGroup = awsec2.NewSecurityGroup(stack, jsii.String("apprunner-demo-sg"), &awsec2.SecurityGroupProps{Vpc: vpc, SecurityGroupName: jsii.String("apprunner-demo-sg"), AllowAllOutbound: jsii.Bool(true)})

memorydbSecurityGroup.AddIngressRule(awsec2.Peer_SecurityGroupId(appRunnerVPCConnSecurityGroup.SecurityGroupId(), nil), awsec2.Port_Tcp(jsii.Number(memoryDBRedisPort)), jsii.String("for apprunner to access memorydb"), jsii.Bool(false))
...
Enter fullscreen mode Exit fullscreen mode

For the App Runner service:

  • Docker image build and upload to private ECR repo process is done by CDK
  • We specify the environment variables that the service will need
  • App Runner source config refers to the IAM role, ECR image and environment variables
  • Then there is the networking config that encapsulates the VPC connector config,
  • and finally, the App Runner service is created
...
ecrAccessPolicy := awsiam.ManagedPolicy_FromManagedPolicyArn(stack, jsii.String("ecr-access-policy"), jsii.String(appRunnerServicePolicyForECRAccessARN))

apprunnerECRIAMrole := awsiam.NewRole(stack, jsii.String("role-apprunner-ecr"), &awsiam.RoleProps{AssumedBy: awsiam.NewServicePrincipal(jsii.String(appRunnerServicePrincipal), nil), RoleName: jsii.String("role-apprunner-ecr"), ManagedPolicies: &[]awsiam.IManagedPolicy{ecrAccessPolicy}})

ecrAccessRoleConfig := awsapprunner.CfnService_AuthenticationConfigurationProperty{AccessRoleArn: apprunnerECRIAMrole.RoleArn()}

memoryDBEndpointURL := fmt.Sprintf("%s:%s", *memorydbCluster.AttrClusterEndpointAddress(), strconv.Itoa(int(*memorydbCluster.Port())))

appRunnerServiceEnvVarConfig := []awsapprunner.CfnService_KeyValuePairProperty{{Name: jsii.String("MEMORYDB_CLUSTER_ENDPOINT"), Value: jsii.String(memoryDBEndpointURL)}, {Name: jsii.String("MEMORYDB_USERNAME"), Value: user.UserName()}, {Name: jsii.String("MEMORYDB_PASSWORD"), Value: jsii.String(getMemoryDBPassword())}}

imageConfig := awsapprunner.CfnService_ImageConfigurationProperty{RuntimeEnvironmentVariables: appRunnerServiceEnvVarConfig, Port: jsii.String(getAppRunnerServicePort())}

appDockerImage := awsecrassets.NewDockerImageAsset(stack, jsii.String("app-image"), &awsecrassets.DockerImageAssetProps{Directory: jsii.String("../app/")})

sourceConfig := awsapprunner.CfnService_SourceConfigurationProperty{AuthenticationConfiguration: ecrAccessRoleConfig, ImageRepository: awsapprunner.CfnService_ImageRepositoryProperty{ImageIdentifier: jsii.String(*appDockerImage.ImageUri()), ImageRepositoryType: jsii.String(ecrImageRepositoryType), ImageConfiguration: imageConfig}}

//snip...

vpcConnector := awsapprunner.NewCfnVpcConnector(stack, jsii.String("apprunner-vpc-connector"), &awsapprunner.CfnVpcConnectorProps{Subnets: &subnetIDsForSubnetGroup, SecurityGroups: &[]*string{appRunnerVPCConnSecurityGroup.SecurityGroupId()}, VpcConnectorName: jsii.String("demo-apprunner-vpc-connector")})

networkConfig := awsapprunner.CfnService_NetworkConfigurationProperty{EgressConfiguration: awsapprunner.CfnService_EgressConfigurationProperty{EgressType: jsii.String(appRunnerEgressType), VpcConnectorArn: vpcConnector.AttrVpcConnectorArn()}}

app := awsapprunner.NewCfnService(stack, jsii.String("apprunner-app"), &awsapprunner.CfnServiceProps{SourceConfiguration: sourceConfig, ServiceName: jsii.String(getAppRunnerServiceName()), NetworkConfiguration: networkConfig})
...
Enter fullscreen mode Exit fullscreen mode

Time to wrap up!

You deployed a Go application to App Runner using AWS CDK (along with the infra!). In the process, you also learnt how to configure App Runner to integrate with MemoryDB for Redis using the VPC connector as well as a high-level overview of the CDK code for the entire solution.

That's all for this blog. Stay tuned for more and Happy coding!

Discussion (0)