AWS TEST-DRIVE
Test-driving AWS App Runner
A closer look at the latest AWS service for running container apps
One of the most popular requests, well, actually the only request, following the Java 16 Records article was for a discussion on how to package a Java 16 application as a Docker container image. So, tag along as we containerize our Java 16 app and then take it for a spin on the latest AWS offering for running container apps: AWS App Runner.
Update July 22, 2022: Updates on App Runner support for VPCs and building Java apps.
To quickly recap, the application we are working with is Guessing Game — a Dropwizard web application that we previously retrofitted to use Java 16 records. Guessing Game is part of the sample applications developed by Serialized to showcase how to use their managed platform to build Event Sourced/CQRS style applications.
A Java 16 Docker Image
Update: As Java 16 is no longer supported, replace Java 16 references with Java 17 (LTS), or with a current release.
To get started we need to build a Java 16 Docker image with our application. This is pretty straight-forward as there are now images available from multiple providers including OpenJDK, AdoptOpenJDK, AWS, and even Microsoft, and SAP.
We will use the official Amazon Corretto 16 image for our build. Now, all we need is a Dockerfile
in the root of our source directory, and we should be good to go:
This will base our image on the amazoncorreto:16
image from AWS. We copy the configuration file and the jar file with the application. Finally, we set an ENTRYPOINT
to start the application.
For this build to work we need Docker Desktop installed, in addition to the JDK and Maven tools. Now we can compile and package our Java application, and then build the Docker image:
$ mvn clean install$ docker build -t guessing-game .
After the image has been built, let’s first test it locally to make sure it is running ok. As the application uses the Serialized platform, we need to supply our API keys as environment variables upon startup (you can get your very own keys by creating a free Serialized account):
$ docker run -p 8080:8080 -eSERIALIZED_ACCESS_KEY=<access-key> -eSERIALIZED_SECRET_ACCESS_KEY=<secret-access-key> guessing-game
The application should start and be ready to serve requests on port 8080.
AWS App Runner
AWS App Runner is a fully managed container application service from AWS launched in May 2021. It builds on previous offerings such as ECS Fargate to provide an easier way to get your web application up and running quickly. App Runner comes bundled with support for building and deploying applications, and provides load balancing, scaling, and application health monitoring. Applications are served over HTTPS and we can easily associate our own domain name to the application if we want to. For Python and Node.js applications App Runner can pull source code directly from GitHub to build and deploy, alternatively you can provide a Docker image pushed to Amazon ECR to be deployed. Since we’re working with a Java application, our only option at this time is to go the container image route.
Update: As of February 2022 a Java platform has been added to App Runner for building and deploying Java 8 and 11 applications from source.
There are plenty of other AWS services for running container workloads, including ECS, EKS, Elastic Beanstalk, and even Lambda. The App Runner service has clearly been designed to make application deployments as easy as possible, compared to the somewhat daunting task to get up and running with some of the other offerings. As such, the service is similar to Google Cloud Run and Azure Container Instances.
Limiting the number of clicks needed to do something tends to come with the trade-off of limited control and configurability, and that is also the case here — we’ll come back to that later.
Test-Driving App Runner
Note that using ECR and App Runner will incur costs, also, App Runner is not included in the AWS Free Tier.
Setting up our ECR repository
To get started we first need to create a private ECR repository to hold our Docker image. App Runner supports both public and private ECR repositories, but no other image repository providers at this time.
Before we start, we need a recent version of the AWS CLI installed and configured with our AWS account credentials. After that, we can create our repository:
$ aws ecr create-repository --region eu-west-1 --repository-name guessing-game --image-scanning-configuration scanOnPush=true --image-tag-mutability MUTABLE
As App Runner is a new service, region availability is limited. In my initial trials App Runner seemed less excited to pull images cross-region, so we make sure the ECR repository is created in a region where App Runner is available, e.g. eu-west-1, us-east-1, or ap-northeast-1.
To use the docker
CLI to push to our new repository, we first need to authenticate (replacing 123456789012
with your our account id):
$ aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.eu-west-1.amazonaws.com
Now we can tag and push our image:
$ docker tag guessing-game:latest 123456789012.dkr.ecr.eu-west-1.amazonaws.com/guessing-game:latest$ docker push 123456789012.dkr.ecr.eu-west-1.amazonaws.com/guessing-game:latest
If all went well, we should be able to see our image if we list the images in the repository:
$ aws ecr list-images --region eu-west-1 --repository-name guessing-game
Giving App Runner access to ECR
Before we can create our new service we need to give App Runner access to our ECR repository. This requires two things:
- Creating a service role
- Attaching the service policy to that role
Create the service role:
$ aws iam create-role --role-name AppRunnerAccessECR --assume-role-policy-document file://trust-policy.json
With the following trust policy:
Attach permissions:
$ aws iam attach-role-policy --role-name AppRunnerAccessECR --policy-arn arn:aws:iam::aws:policy/service-role/AWSAppRunnerServicePolicyForECRAccess
Now, if that didn’t feel easy enough, creating the service using the AWS console will set this up automatically.
In addition to the service role, App Runner will also, on its own, create a service-linked role to get permission for pushing logs to CloudWatch and monitoring ECR image pushes (the latter is used for enabling auto deployment on new image pushes). This service-linked role will show up among our IAM roles and if we decide to stop using App Runner, we will have to manually delete it.
Creating the App Runner service
Finally, we are ready to create our service and deploy our application:
$ aws apprunner create-service --region eu-west-1 --cli-input-json file://guessing-game.json
With the following configuration:
apprunner create-service
is an asynchronous operation and the deployment will take a few minutes. The returned payload contains a ServiceUrl
and a ServiceArn
. We can follow the deployment in the console, or use the ServiceArn
with the aws CLI:
$ aws apprunner list-operations --region-eu-west-1 --service-arn <service-arn>
We are looking for something like this:
[...]
{
[..]
"Type": "CREATE_SERVICE",
"Status": "SUCCEEDED",
[..]
}
[..]
The ServiceUrl
is the endpoint where our application will be available, so once deployment has finished we should be able to start a new game by issuing:
And there we have it, our containerized Java 16 app deployed and running!
Now, if we look carefully at the response from the POST above, we notice that the returned location header is missing the s in the https protocol prefix. This is due to an issue with how header forwarding is configured in App Runner. Until fixed, this will require a manual work-around in the code.
Update 2021–10–14, this issue is now reported to have been fixed.
Configuration
There are a number of configurations that can be made during deployment, and here we pretty much went with the default on all of them, including port mapping and application start command. This gives us 1 vCPU with 2GB of memory per instance (compute resource) and an autoscaling policy that adds one instance per 100 concurrent requests, up to a maximum of 25 instances. We also get a TCP health check at the application listening port with default interval, timeout and thresholds.
There are no specific settings for availability beyond scaling, but according to the docs we will “benefit from the availability and fault tolerance mechanisms that AWS offers.”, whatever that means.
Deploying a New Application Version
As we configured auto deployments to false
, after pushing a new image we need to manually initiate any new updates to the application, either from the console or the CLI:
$ aws apprunner start-deployment --service-arn <service-arn>
Setting auto deployments to true
will automatically deploy newly pushed images (this comes at an additional monthly cost for each time this is enabled on a new service).
Deployments will temporarily add instances to enable zero-downtime deployments.
Adding a Custom Domain Name
As we saw above, when a new service is created it is assigned a default Service URL. In addition to this, custom domain names can be associated with the application. App Runner supports apex domains, as well as subdomains, and wildcards. To associate the subdomain guessing-game.example.com to the application we can use the associate-custom-domain
command:
$ aws apprunner associate-custom-domain --region eu-west-1 --service-arn <ServiceArn> --domain-name guessing-game.example.com --no-enable-www-subdomain
And, to get the required configuration records:
$ aws apprunner describe-custom-domains --region eu-west-1 --service-arn <ServiceArn>
This will return a DNS target and a CertificateValidationRecords
struct with one or more entries. We will add this info as CNAME records to our DNS zone file. And then wait. When validation has been completed, we can now reach our app using our custom domain name with a fresh AWS root CA issued certificate.
For a zone apex, the story is a little bit more complicated and depends on what support is available from the DNS provider, e.g. Route53 does not support App Runner as target for ALIAS records at this time.
So, yeah, that’s alright.
Accessing Other AWS Services
We can configure our service with an instance role, giving our application access to other AWS services. So if we are feeling uncomfortable supplying our database API keys as environment parameters via the CLI or console, an alternative could be to store them encrypted in the Systems Manager’s Parameter Store.
Accessing the keys from the application could be accomplished by attaching an instance role with a policy looking something like this:
And using the Java AWS SDK to pull the required information during application startup:
There is no native App Runner integration with Parameter Store or Secrets Manager at this time.
Logs and Metrics
Event, deployment and application logs are accessible in the App Runner console, and are also collected in CloudWatch log groups.
Some basic metrics are also collected and forwarded to CloudWatch, including CPU and memory utilization on instance level, as well as HTTP requests and statuses on service level.
Notable Limitations
As mentioned in the beginning of this article, higher-abstract services such as App Runner tend to trade comprehensive configuration and control for ease of use. Following are some notable observations and limitations of the current App Runner offering.
VPC support: App Runner runs in its own VPC, there is no support for deploying App Runner in your own VPC or connecting it to your VPC (e.g. using an Elastic Network Interface). Consequently, there is no way to reach resources in a private VPC, such as an RDS instance. In our example app above we use a third-party service offered over the internet, similarly, AWS services reachable over the internet can be used from App Runner applications, including S3 and DynamoDB.
There is also no way to control outgoing traffic from an application, which might be necessary for security or compliance reasons.
Update: Accessing services in a VPC is now supported using VPC connectors.
It is possible to setup a VPC interface endpoint to enable connections from a private VPC to access App Runner applications without leaving the AWS network. While this will prevent private traffic to hit the internet, the application will still be accessible over its public endpoint.
Limited Autoscaling: The only scaling strategy available is based on concurrent connections, more sophisticated scaling strategies using other metrics are not supported. Also, it is not possible to automatically scale down provisioned instances to zero, so there will always be a cost for running App Runner applications, even if there is no traffic. App Runner services can however be manually paused and resumed.
Simplistic Health Checks: The health checks configurable in the console only support TCP connection checks. Using the API it is possible to configure HTTP health checks with a path variable. This is largely undocumented, so it is unclear how HTTP health checks work, but since there is no way of configuring a separate port it doesn’t look like we can expect support for Dropwizard-style checks running on a separate admin port.
No Secrets Management: As mentioned earlier, there is no direct integration with Parameter Store or Secrets Manager, so the only built-in way to configure secrets is using the support for passing environment variables.
Stability: During my initial trials of App Runner directly after launch of the product, I ran in to a number of issues with failing deployments, services that refused to start up after being paused, and issues during service deletion. It seems most of these issues have now been sorted out, but I would certainly do comprehensive testing before using App Runner in a production setting.
Deployment and event logs offer very little help in tracking down a failed deployment. If a deployment fails — and it isn’t an issue with your application — you really have no idea what went wrong.
Regional Availability: As a new service, region availability is limited.
Pricing
Finally, a little bit about pricing. App Runner is billed on both provisioned capacity, and active usage. There is a GB-hour cost for provisioned capacity and a combined vCPU and GB cost for active instances, i.e. instances actively serving requests.
In addition, there are add-ons for various build configurations and the usual AWS data transfer charges. Refer to the App Runner pricing page for details and pricing examples.
Conclusion
AWS App Runner is an opinionated service for running container applications, targeting a fairly narrow, but probabaly rather common, use case. If your requirements fall within the supported features, and the stability concerns are amended, I can see how this can be an attractive offering. App Runner can be particularly well suited for prototyping, developing/demoing kind of scenarios, especially if support for scaling down to zero is added for situations where cold starts aren’t an issue. If your are comfortable with all the magic, that is.
As a new service we can hopefully expect some of the issues and limitations above addressed sooner rather than later. The roadmap is publicly available, so make sure to request missing features and vote for the issues that bug you the most!
👋