Skip to content

Deploying with App Runner and AWS Cloud Control

Developers can now use App Runner to quickly deploy containerised web applications and APIs

One of the complexities with Amazon Web Services is the wide variety of syntax for the many services that can be deployed, updated and queried via code.

In an effort to bring uniformity to these processes, AWS has released the Cloud Control API . Through a standardised API interface, developers can now run CRUD operations across many of the services within the Amazon ecosystem. AWS has also committed to bring more services into the scope of Cloud Control, so it’s a useful skill to have in your developer’s toolbox.

In addition, the recent released App Runner provides a fully managed method for deploying containerised web applications and APIs with scalability, SSL certificates and CD built-in from the get go.

You can grab the files from this example from the repository to try it out.

As with all AWS services, there are a number of different ways to deploy infrastructure, including:

  • AWS console
  • AWS CLI
  • AWS SDKs
  • Infrastructure as code such as Terraform

We are going to use the Javascript SDK because it gives us greater control over checking if resources exist and enables us to handle responses in our code. For instance, if resources (ECR repository and App Runner service) are already available to us, we won’t need to create them. This multi-step process made Javascript a more logical choice; Terraform proved unwieldy in this particular use case and avoided creating a custom image.

Prerequisites

  • You have obtained an AWS authentication access ID and key, and placed both in your GitHub repository’s secrets.
  • You have an IAM role to allow App Runner to access ECR.

[caption id="attachment_300016312" align="alignnone" width="456"]

Image of the "secrets" settings in Github[/caption]

Building our workflow

As I’ve mentioned, the real benefit with Cloud Control is the standardised structure for CRUD operations, regardless of the resource type. This has some advantages over previous methods of interaction with AWS services.

For our pipeline, we’re going to use a GitHub Action. This will interact with both Cloud Control and App Runner.

The Cloud Control API exposes five main verbs (along with another three for querying request status). The main five are: createResource getResource updateResource deleteResource listResources GitHub will look in .github/workflows/ for our workflows, so we define a cd.yaml file here, which starts by checking out our code so that it’s available to the workflow. We then set up Node.js for use in our scripts, and configure AWS credentials.

cd.yaml

JavaScript
name: cd
 
on:
 push:
   branches:
     - master
 workflow_dispatch:
 
env:
 ECR_REPOSITORY_NAME: artist-info
 
jobs:
 build:
   runs-on: ubuntu-latest
 
   steps:
     - uses: actions/checkout@v2
     - uses: actions/setup-node@v2
       with:
         node-version: '16'
 
     - id: login-aws
       uses: aws-actions/configure-aws-credentials@v1
       with:
         aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
         aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
         aws-region: eu-west-1

Note the workflow_dispatch trigger, which enables us to trigger execution of the workflow manually without having to push any code. We’ll install the aws-sdk here using npm and define the JS to execute within the GitHub workflow. Note that we’re using the github-script action to call our Javascript because it’s not natively supported.

cd.yaml

Plain Text
- run: npm install aws-sdk
 
     - id: create-ecr-repo
       uses: actions/github-script@v5
       with:
         script: require('./.github/workflows/ecr_setup.js')

Now to our Javascript; we’re creating the first of two files — ecr_setup.js  — which, as we’ve already defined, will be in our workflow directory along with our YAML.

We require the aws-sdk we installed in the previous step, create a new instance of the Cloud Control service SDK and pull in our environment variables.

ecr_setup.js

JavaScript
const AWS = require('aws-sdk')
 
const cloudcontrol = new AWS.CloudControl()
 
const { ECR_REPOSITORY_NAME } = process.env
 
const TypeName = 'AWS::ECR::Repository'
 
async function run() {
 try {
   await cloudcontrol
     .getResource({
       TypeName,
       Identifier: ECR_REPOSITORY_NAME
     })
     .promise()
   console.log('ECR Repo exists. Skipping creation.')
 } catch (e) {
   const desiredState = {
     RepositoryName: ECR_REPOSITORY_NAME,
     ImageTagMutability: 'MUTABLE'
   }
 
   await cloudcontrol
     .createResource({
       TypeName,
       DesiredState: JSON.stringify(desiredState)
     })
     .promise()
   console.log('ECR Repo created')
 }
}
 
module.exports = run()

Note how, in our async function above, we getResource to check if our repo exists. If it doesn’t, it will generate an error, which is caught by our repo creation code block. This code will createResource with the desired state and then return control to our YAML script.

Back in our workflow our next step is to build our Docker image and push it to our ECR registry. I’ve written a sample app using Node.js and React for us to deploy here. The app is called “Artist Info” and provides a search facility for looking up any band or singer. It consumes the Musicbrainz API , and displays basic info about the artist, along with any relevant web links (social media, discographies, etc.).

cd.yaml

JavaScript
- id: push-to-ecr
       env:
         ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
       run: |
         ECR_IMAGE_NAME=$ECR_REGISTRY/$ECR_REPOSITORY_NAME
         docker build -t $ECR_IMAGE_NAME  .
         docker push $ECR_IMAGE_NAME
         echo "::set-output name=image::$ECR_IMAGE_NAME"

We’re using the registry provided to us at login to ECR, and the repository name defined earlier. Our runlist ends by outputting the ECR_IMAGE_NAME constructed from our registry and repo, so it can be used by App Runner to pull the correct image.

Still in our main workflow, we set out the required variables that our second Javascript file will consume and call it.

cd.yaml

JavaScript
- uses: actions/github-script@v5
       env:
         AWS_ACCOUNT_ID: ${{ steps.login-aws.outputs.aws-account-id }}
         ECR_IMAGE_NAME: ${{ steps.push-to-ecr.outputs.image }}
       with:
         script: require('./.github/workflows/apprunner_setup.js')

It’s worth noting here that, as well as running the container built in our GitHub workflow, App Runner has the ability to build from our source code instead, by connecting to the GitHub repository directly.

This is one of the features that could make App Runner extra useful in streamlining CD pipelines. We’re not using that in this example, however, because we found that this method involved carrying out at least one part of the flow manually.

Also, App Runner’s build from source coverage currently extends only to Node version 12 .

Our second Javascript file is going to handle our App Runner configuration and starts as before, by pulling in our SDK and variables. As a reminder, App Runner’s purpose is to simplify deployment and continuous delivery for containerised applications. It’s not suitable for every case, of course. It has limits in terms of memory and CPU resources per instance, for example. You can find out more on the AWS website .

We’ll use another of Cloud Control’s methods — listResources — to get our existing App Runner services. Then, we can simply return if the ResourceDescriptions array contains our service.

App Runner should need to be set up just once. One of its strengths is its ability to monitor our ECR image and redeploy it whenever it changes. For this, we set AutoDeploymentsEnabled to true .

If our service exists, there’s nothing to do here.

apprunner_setup.js

JavaScript
const AWS = require('aws-sdk')
 
const cloudcontrol = new AWS.CloudControl()
 
const { ECR_REPOSITORY_NAME, ECR_IMAGE_NAME, AWS_ACCOUNT_ID } = process.env
const serviceName = `${ECR_REPOSITORY_NAME}-service`
 
const TypeName = 'AWS::AppRunner::Service'
 
async function run() {
 const services = await cloudcontrol
   .listResources({
     TypeName
   })
   .promise()
 
 const service = services.ResourceDescriptions.find(
   r => JSON.parse(r.Properties).ServiceName === serviceName
 )
 
 if (service) {
   return console.log(
     `App runner service ${serviceName} exists. Skipping creation`
   )
 }

If our service is not found, the workflow is either being run for the first time, or perhaps the service has been manually destroyed for some reason. If so, we create the resource. Remember, we need to submit the desiredState (Cloud Control’s uniform resource definition) in a JSON blob or, to put it another way, a stringified JSON object.

apprunner_setup.js

JavaScript
const desiredState = {
   ServiceName: serviceName,
   SourceConfiguration: {
     AuthenticationConfiguration: {
       AccessRoleArn: `arn:aws:iam::${AWS_ACCOUNT_ID}:role
                          /service-role
                          /AppRunnerECRAccessRole`
     },
     ImageRepository: {
       ImageConfiguration: {
         Port: '5000'
       },
       ImageIdentifier: `${ECR_IMAGE_NAME}:latest`,
       ImageRepositoryType: 'ECR'
     },
     AutoDeploymentsEnabled: true
   }
 }
 
 const resource = {
   TypeName: TypeName,
   DesiredState: JSON.stringify(desiredState)
 }

Now, all that’s left to do is create the App Runner service. Resource creation will happen asynchronously, so our request will receive a response only to confirm that the process has started or that it has immediately failed.

Here, the response from our GitHub workflow log shows that the request has been accepted:

JavaScript
App Runner service created {
  ProgressEvent: {
    TypeName: 'AWS::AppRunner::Service',
    RequestToken: 'b0a0ce03-e16c-4caa-a781-989cd6bc9ca5',
    Operation: 'CREATE',
    OperationStatus: 'IN_PROGRESS',
    EventTime: 2021-10-20T14:26:41.380Z
  }
}

We must therefore waitFor a success (or failure) message using the token we received when we issued createResource .

apprunner_setup.js

JavaScript
try {
   const response = await cloudcontrol.createResource(resource).promise()
   console.log('App Runner service creation', response)
 
   await cloudcontrol
     .waitFor('resourceRequestSuccess', {
       RequestToken: response.ProgressEvent.RequestToken
     })
     .promise()
 } catch (err) {
   console.error('Failed to create App Runner service', err)
 }
}
 
module.exports = run()

[caption id="attachment_300016315" align="alignnone" width="1282"]

A diagram showing an overview of the the flow of interactions for deploying an AWS App Runner service with Cloud Control[/caption]

If you’ve followed these steps correctly, you’ll now have your app deployed and running within App Runner. Any commits you make to the main branch of the repository will re-deploy the infrastructure and code automatically, which should save you a lot of time in the future. You can grab a coffee and relax!

Further reading

Sébastien Stormacq, Principal Developer Advocate at AWS, has written a great blog post on the release of Cloud Control . You can also discover more about App Runner on the AWS website .

Insight, imagination and expertly engineered solutions to accelerate and sustain progress.

Contact