Test Driving App Runner's Experimental AWS CDK Library
I discovered the relatively new App Runner service while testing deployments with AWS CDK. If App Runner is new to you, it’s possible to think of it as an AWS service abstraction level above ECS Fargate. It’s what AWS calls a fully managed service and will deploy your application as a container as well as handle auto scaling, SSL/TLS certificate management, CI/CD, and load balancing. With its auto scaling and load balancing features, it’s also possible to think of App Runner as a service that sits in between Lambda and ECS Fargate.
I decided to test drive App Runner using its experimental AWS CDK library. App Runner has many features, but I chose deployment with AWS CDK as the focus. I’m hoping it will provide a glimpse into the future of App Runner deployments.
Since this article is about using an experimental AWS CDK construct library, I might assume most readers are familiar with AWS CDK concepts, but just in case, AWS CDK is an infrastructure as code tool that transpiles AWS CDK code, written in a general-purpose programming language to JSON and then sends these assets to CloudFormation to deploy infrastructure and resources. In this article, I will be using TypeScript for my AWS CDK code, but AWS CDK also supports JavaScript, Python, Java, C#/.Net, and Go.
The AWS Cloud Development Kit Library contains constructs for specific AWS resources, and these constructs may consist of three levels of abstraction from those resources: L1, L2, and L3.
L1 constructs map directly to how CloudFormation defines resources. L2 and L3 constructs are levels of abstraction above this and include best practices for creating resources and groups of resources with default settings and assets. Basically, some of the complex or repetitive work is handled by L2 and L3 constructs.
It’s possible to create your own constructs, so a custom L3 construct could enforce best practices for resources created all at once for specific deployments.
For my test drive, in addition to App Runner, I’m going to deploy infrastructure for networking and a database. In AWS CDK, code for related resources is organized into stacks, so I’ll have an App Runner stack, a DNS stack, a VPC stack, and so forth.
There are two AWS CDK libraries for App Runner: a construct library with generated L1 constructs and an experimental construct library with higher level L2 constructs. As I mentioned above, I’ll be test driving App Runner with the constructs in the experimental library.
There isn’t a lot to the App Runner experimental library at the moment. There are two constructs: Service
and VpcConnector
:
// AWS::AppRunner CloudFormation Resources:
export * from './service';
export * from './vpc-connector';
I’ll look at VpcConnector
a bit later. For now, the construct Service
is where we can begin defining how App Runner deploys our application. At the source
property of the Service construct, we reach an intersection with what appears to be four directions. We can deploy from a GitHub repository, a private or public ECR repository, or from local assets:
These avenues seem limiting. I don’t see GitLab, Docker Hub, GitHub Packages, and other container registry options. Also, when I look more closely at fromAsset
, I see that this road also leads to ECR:
//...
public bind(_scope: Construct): SourceConfig {
return {
imageRepository: {
imageConfiguration: this.props.imageConfiguration,
imageIdentifier: this.props.asset.imageUri,
imageRepositoryType: ImageRepositoryType.ECR,
},
ecrRepository: this.props.asset.repository,
};
}
// ...
asset
used here is an import from the aws-cdk-lib/aws-ecr-assets
package, and the repository type is hard-coded to private ECR: ImageRepositoryType.ECR
.
If you prefer to deploy from containers that you build and don’t use ECR, I guess the rest of this test drive is going to be a scenic backseat joyride for you.
Perhaps one day these properties will be consolidated with more options for version controlled, packaged, and local sources. Until then, as someone who doesn’t use ECR, I’m most curious about the fromGitHub
route.
With Source.fromGitHub
, it seems like I’ve reached cruising speed for my test drive. Here the fully managed features of App Runner really start to take over for a deployment, and I get to tell App Runner to take my code and go.
There are up to five properties I can use to configure an App Runner deployment from a GitHub source code repository:
First, let’s deal with the connection
property. There’s currently no option to create and approve a connection to GitHub using AWS CDK or CloudFormation without creating a custom resource (more on custom resources later), and since this process requires UI approval in the App Runner Console anyway, I followed the guide in the AWS App Runner Workshop to create a connection in the Console. I’m going to use GitHub Actions to deploy, so I’ll create an environment secret for the connection’s ARN in my repository and reference it from there:
- name: Deploy CDK stack
working-directory: ./
run: CONNECTION=${{ secrets.APP_RUNNER_CONNECTION }} npx cdk deploy --all --require-approval never
Next, the configurationSource
property determines whether to use values defined in an App Runner configuration file, apprunner.yaml
, or values defined by the optional codeConfigurationValues?
property for deployment.
An App Runner configuration file includes additional build options, such as pre-build and post-build commands. Setting configurationSource
to ConfigurationSourceType.REPOSITORY
will tell the property to use an App Runner configuration file in your repo.
Maybe the next time I take App Runner for a spin, I’ll try using an App Runner configuration file and use the pre-build step to run unit tests similar to what I do with multi-stage builds in a Dockerfile.
For this test drive, however, since I want to work with TypeScript as much as I can, I’m going to set configurationSource
to ConfigurationSourceType.API
and add my deployment configurations to the codeConfigurationValues?
property:
// ...
source: AppRunnerAlpha.Source.fromGitHub({
configurationSource: AppRunnerAlpha.ConfigurationSourceType.API,
codeConfigurationValues: {
runtime: AppRunnerAlpha.Runtime.GO_1,
port: containerPort,
environmentSecrets: {
DSN: AppRunnerAlpha.Secret.fromSecretsManager(props.dbSecret),
},
buildCommand: "go build ./cmd/web/",
startCommand: "./web",
},
connection: AppRunnerAlpha.GitHubConnection.fromConnectionArn(
`${process.env.CONNECTION ?? ""}`,
),
repositoryUrl,
branch,
}),
// ...
Once again, App Runner is fully managed, so the build process is now going to happen behind the scenes. I don’t have to worry about a container registry or repository, provisioning a pipeline or build instance, pulling images to production instances, and orchestration to get and keep my container in a running state. App Runner does all that.
When App Runner builds your image, there is a fee of $0.005 per build minute.
The other construct in the App Runner experimental library is VpcConnector
. If your application architecture includes resources in private subnets, you’ll need a VPC Connector for outgoing traffic from your App Runner application to resources there.
The app I tested was a web app backed by an RDS database, so I created a VPC Connector with my deployment:
// ...
const vpcConnector = new AppRunnerAlpha.VpcConnector(this, "VpcConnector", {
vpc: props.vpc,
vpcSubnets: props.vpc.selectSubnets({ onePerAz: true }),
vpcConnectorName: "VpcConnector",
});
// ...
Before writing any AWS CDK code, I took a quick peek at the App Runner API reference and the deployment process in the App Runner Console, and I noticed there’s an important feature in the App Runner API that’s missing in the CloudFormation resources: custom domain management.
If you’ve worked with AWS CDK (or CloudFormation) before, running into a limitation like this doesn’t mean you’ve run out of road, not if you have custom resources under your hood.
In a previous article, I wrote about custom resources so to paraphrase from there: a custom resource allows us to create our own customized provisioning logic using API calls, such as associateCustomDomain
, describeCustomDomain
, and disassociateCustomDomain
from the App Runner API.
In AWS CDK, a custom resource executes using a special Lambda function that must handle create, update, and delete operations. These operations are what CloudFormation expects during provisioning.
To start, I figured I’d need a custom resource to handle the associateCustomDomain
and disassociateCustomDomain
App Runner API calls:
Now I’ll update my App Runner stack to use this custom resource:
This deploys successfully, but I notice that my custom domain isn’t reachable, and in the Console under the Custom domains tab, its status is “Pending certificate DNS validation”.
I click through to my domain name in the Console, and I see there’s a Configure DNS section with two certificate validation records and a DNS target record that I need to add to my custom Route 53 hosted zone, similar to what’s described in the App Runner Documentation:
So in my custom resource, I’m also going to need to loop through the API’s CustomDomains.CertificateValidationRecords
and add them as CNAME’s to my custom hosted zone. For this update, I’ll use the Route 53 changeResourceRecordSets
API call:
But there’s a problem here: I can add my custom domain immediately, but the certificate validation records need time to populate before I can add them, so these two operations must synchronize. A quick way to sync these operations is to use separate AWS CDK stacks, so I’ll create a new stack and custom resource for the certification validation actions:
Now when I deploy, my custom domain is reachable on HTTPS (after about 15 minutes for me).
There are some more interesting things happening behind the scenes here. If I look at AWS Certificate Manager in the Console, I see that there are no certificates listed.
App Runner is managing the certificate validation, and I really only need to add the DNS target records to my custom Route 53 hosted zone.
If you’re interested in looking at more AWS CDK code, I also created a GitHub Actions to destroy my App Runner deployment and created a DNS Delete Stack and custom resource for this workflow.
Overall, I feel this was an enjoyable test drive. App Runner is a promising service. It made many things easier, and the pricing is pretty good.
In the future, I would like to see custom domain management added to CloudFormation and AWS CDK, but for today, I have no problem with the abstractions App Runner makes. A fully managed service means that I don’t need to see container images in repos or certificates in ACM. I’m comfortable with App Runner taking the wheel.