Accelerating Development Velocity with AWS App Runner and Cloud Native Buildpacks
In May 2021 we introduced AWS App Runner, the simplest way to build and run your containerized web application in AWS. AWS App Runner gives you a fully managed container-native service. There are no orchestrators to configure, build pipelines to set up, load balancers to optimize, or Transport Layer Security (TLS) certificates to rotate. And of course, there are no servers to manage.
AWS App Runner provides two ways to take your workload from source to production. The first is to package the application yourself in a container image using something like a Dockerfile, bundling the application with its dependencies into a portable, runnable artifact. The second is to give AWS App Runner access to your source code repository and allow it to build the container for you with its integrated build service.
If you’re familiar with Platform-as-a-Service offerings, the latter mechanism won’t sound unusual, with various ways to automatically transform application source code into a runnable artifact that have existed for the last decade. The principle is that if you structure your application source code in a compatible manner, the platform handles this aspect transparently for developers and gives them one less thing to consider when deploying their applications. Proponents of these systems would argue that for many use cases, the need to create a container image with the likes of a Dockerfile is actually a step backward.
At the time of this post, AWS App Runner provides the ability to automatically deploy applications from source code for many runtimes including Node.js, Python, and Java, Ruby, PHP, Golang, and .NET. But what if customers want the productivity benefits of a “source to container image” mechanism with more flexibility? For example they might require support for a runtime version that isn’t currently support by AWS App Runner such as Java 17 or Node.js 18. Another example might be customizing the build process to inject 3rd party components or providing functionality specific to particular frameworks like Spring Boot.
In this post, we’ll explore how to combine the productivity benefits of AWS App Runner and Cloud Native Buildpacks (CNBs).
Overview of cloud native buildpacks
We’ve previously published a post about CNBs that provided an overview of the project, its benefits, and how it differs from the likes of using a Dockerfile to build container images. If you’re unfamiliar with the concepts and terminology, then it’s recommended to review this post before proceeding.
At a high level, CNBs are an incubating project in the Cloud Native Computing Foundation (CNCF) that provides a mechanism to transform your application source code in to an Open Container Initiative (OCI)-compliant container image without using a Dockerfile. The concept of a “buildpack” has historically been associated with Platform-as-a-Service offerings such as Heroku and Cloud Foundry, and this latest iteration of buildpacks can be thought of as decoupling the build mechanism of these platforms to be consumed by the broader container community. As this project has matured, it’s been adopted by a number of vendors and tools.
A primary benefit of the CNB project is that it removes the need to author a Dockerfile to build a container image, which allows application development teams to get their code running in container platforms quicker. This provides an aspect of a Platform-as-a-Service offering like Heroku and Cloud Foundry, while deploying to flexible container-based services. The technology is language-agnostic, and there are open source “builders” that support a range of platforms like Java, .NET Core, Ruby, Node.js, Go, and Python.
Bringing together AWS App Runner with the CNB project allows us to take advantage of this productivity and language support to get applications deployed and running as a container without wrestling with a Dockerfile. This accelerates the process of running workloads that are not yet supported as a “Code-based service” in App Runner.
Even for languages that are supported, we do gain some benefits. For example, the Paketo Java buildpack is sophisticated enough to automatically detect whether you’re using Maven or Gradle, as well as specific frameworks like Spring Boot, and allows you to build and execute your application appropriately. Another feature that may be beneficial is the ability to automatically generate a Software Bill of Materials (SBOM), which enumerates all of the software installed each buildpack as the container image is built. Customers are increasingly finding this valuable as a part of securing their software supply chains.
The steps in the process are as follows:
- The source code of a set of sample applications is populated in an AWS CodeCommit repository
- Commits to the repository trigger AWS CodePipeline to orchestrate the build process
- An AWS CodeBuild project invokes CNBs via the pack command line interface (CLI) to create a container image
- The image is then pushed to an Amazon Elastic Container Registry (Amazon ECR) repository
- An AWS App Runner service is deployed using a AWS CloudFormation template dynamically created in the pipeline
The prerequisites for this solution are as follows:
- The correct AWS Identity and Access Management (AWS IAM) role permissions for your account allowing for the creation of the AWS CodePipeline pipeline, AWS CodeBuild projects, Amazon Elastic Container Registry (Amazon ECR) repositories, Amazon Simple Storage Service (Amazon S3) buckets, and AWS Lambda
Deploying the pipeline
To deploy the pipeline, complete the following steps:
- Download the AWS CloudFormation template and pipeline code from the GitHub repo.
- Log in to your AWS account if you have not done so already.
- On the AWS CloudFormation Console, choose Create Stack.
- Choose the AWS CloudFormation pipeline template called cloudformation.yml from the repository.
- Choose Next.
- Enter a name for the stack. The rest of the instructions assume apprunner-buildpacks-sample.
- The default stack parameters can be used, although changes are possible:
- Under Builder, specify the CNB builder to be used to build the container image. By default this uses the Paketo Buildpacks Base builder.
- Under SampleApp, specify the sample application to be deployed as a container image. By default this is the Java sample application.
Examining the pipeline
By the time the AWS CloudFormation stack has completed its deployment, the AWS CodePipeline will already be running or perhaps have completed. The AWS CloudFormation template creates an AWS CodeCommit repository that contains several sample applications that we can deploy to show the flexibility of CNBs. You can open the AWS CodeCommit repository to inspect the code, as shown in the following diagram:
These cover a variety of languages:
- .NET Core
On closer inspection of each of the sample applications, you’ll notice there are no Dockerfiles or any hints provided on how to build the container image for the language or platform used for that application. CNBs automatically attempt to determine the language used for each sample application and build the container image accordingly.
The pipeline can be configured to deploy a single sample for a given execution and deploys the Java sample by default. By the time you inspect the pipeline, it will likely already be executing, and may even have completed its first deployment of the sample application selected when the AWS CloudFormation template was deployed.
The main phases of the pipeline are:
- Checkout the source code from AWS CodeCommit
- Build the container image using CNBs and push it to Amazon ECR
- Prepare a AWS CloudFormation template using the latest image
- Deploy the container image to AWS App Runner using the AWS CloudFormation template
We won’t cover the first two of these four phases in depth, since this was explored in the previous post. Please review that post to understand the how CNBs are applied to create the container image as they are largely unchanged.
After the container image has been built and pushed to Amazon ECR, the next step creates a small AWS CloudFormation template by substituting in the latest image tag to the AWS App Runner service configuration.
The final pipeline stage deploys the AWS CloudFormation, which creates or updates the AWS App Runner service.
Accessing the application
Once the pipeline has completed, you can find the service running in the AWS App Runner Console:
Select the link under the column Default domain to access the application deployed by the pipeline:
Deploy another sample
Let’s prove that we can support multiple languages with no changes by updating the AWS CloudFormation stack and selecting a different sample application.
- Open AWS CloudFormation Console and select the apprunner-buildpacks stack
- Select Update
- Select Next
- Select a different SampleApp option (e.g., go-sample)
- Complete the AWS CloudFormation flow as previously outlined when we first deployed the stack
- Open the AWS CodePipeline Console and select the pipeline
- Select Release change
- Once the pipeline execution has completed, access the AWS App Runner application URL again
The web page should now display the language you selected when you updated the stack:
You can try similar changes with the other sample applications available in the AWS CloudFormation template parameters.
To avoid incurring future charges, clean up the resources created as part of this post.
- On the Amazon S3 Console, open the bucket from the stack Outputs tab named PipelineS3Bucket
- Select all objects in the bucket and choose Delete
- When prompted, enter permanently delete and choose Delete
- On the Amazon ECR Console, open the repository from the stack Outputs tab named EcrRepository
- Select all tags in the repository and choose Delete
- When prompted, enter delete and choose Delete
Once this has been completed, open the AWS CloudFormation Console and delete the following stacks to remove the remaining resources:
- First delete the stack named apprunner-buildpacks-sample-service
- Finally delete the stack name apprunner-buildpacks-sample
In this post, we explored how CNBs can be used with AWS App Runner to quickly transform source code into a running application. This approach allows us to augment the languages supported by the source-based deployment mechanism built into AWS App Runner and effectively leverage languages and runtimes that do not yet have first-class support.
Leveraging CNB provided us with a number of benefits for packaging our applications to deploy to AWS App Runner:
- No Dockerfile was necessary
- We used a single, consistent pipeline that works with all the sample languages and frameworks
- Our pipeline has a built-in caching mechanism for dependencies whether its Java/Maven, Node.js/NPM, or Go modules that speeds up subsequent executions
There is currently an open roadmap item for integration CNB support directly into AWS App Runner. Our roadmap is primarily driven by customer feedback, so we welcome your comments. If you would like to see tighter integration with AWS App Runner please leave feedback on that GitHub issue.
To learn more about CNB you can visit buildpacks.io.