Containers

Enable continuous deployment based on semantic versioning using AWS App Runner

Introduction

In this modern cloud era, customers automatically build, test, and deploy the new version of their application multiple times a day. This common scenario in the software development life cycle creates faster delivery of features, bug fixes, and other updates to end users. One key aspect of continuous deployment is semantic versioning, a system for assigning version numbers to software releases. Semantic versioning uses a standard format to convey the level of change in a release, which allows developers and users to understand the potential impact of an update. Without semantic versioning, it would be difficult track any breaking changes that basically prevents us from moving forward.

In this post, we’ll show you how to use semantic versioning combined with the Continuous integration (CI)/Continuous deployment (CD) capabilities AWS App Runner provides to deploy new versions of the application automatically.

Semantic versioning

Semantic versioning is a system for assigning version numbers to software releases. It uses a standard format to convey the level of change in a release, which allows developers and users to understand the potential impact of an update. The basic format of a semantic version number is MAJOR.MINOR.PATCH, where each component is a non-negative integer.

Semantic versioning overview

Here are some general rules for semantic versioning:

  • When a release contains backward-incompatible changes (like breaking of an API contract), the MAJOR version is incremented.
  • When a release contains backward-compatible changes, the MINOR version is incremented.
  • PATCH version is incremented for releases that contain only bug fixes and no other changes.

By using semantic versioning, developers can communicate the impact of a release to users, making it easier to understand the risks and benefits of updating to a new version.

Problem statement

AWS App Runner is a fully managed container application service that makes deploying containerized applications from source code repositories fast and easy. AWS App Runner provides a fully controlled environment to build, run, and scale containerized applications. It also provides a fully managed CI/CD pipeline to build and deploy new application versions automatically. Customers can leverage AWS App Runner to continuously monitor their Amazon Elastic Container Registry (Amazon ECR) repository for new images based on a fixed tag (e.g., LATEST) and automatically deploy the new version of the application to the underlying AWS App Runner service.

However, this approach doesn’t allow customers to monitor and deploy the new version of the application based on semantic versioning. Let’s say the customer wants AWS App Runner to automatically deploy the new application version based on a match pattern like >= MAJOR1.MINOR2.PATCH3, this isn’t possible natively with the current AWS App Runner capabilities.

Solution overview

In this solution, we use the following AWS services:

  • AWS App Runner – A fully managed container application service that makes it easy to quickly deploy containerized applications from source code repositories.
  • AWS Lambda – A serverless compute service that allows you to run code without provisioning or managing servers.
  • Amazon Elastic Container Registry (Amazon ECR)- A fully managed Docker container registry that makes it easy for developers to store, manage, and deploy Docker container images.
  • Amazon EventBridge – A fully managed event bus that makes it easy to connect applications together using data from your own applications, Software-as-a-Service (SaaS) applications, and AWS services.
  • Amazon Simple Storage Service (Amazon S3) – A fully managed object storage service that offers industry-leading scalability, data availability, security, and performance.
  • Amazon Simple Queue Service (Amazon SQS) – A fully managed message queuing for microservices, distributed systems, and serverless applications.

The following diagram shows the overall architecture of the solution:

The following diagram shows the overall architecture of the solution

The solution uses EventBridge rules to listen to Amazon ECR PUSH events, which get processed by an AWS Lambda function via an Amazon SQS queue. The AWS Lambda function uses AWS App Runner (Application Programming interface)APIs to fetch the currently deployed version of the application and compares it with the imageTag that got pushed to the Amazon ECR repository. If there’s a match (based on semantic versioning), then the AWS Lambda function updates the AWS App Runner service to deploy the new version of the application. Customers can provide the match pattern as an input parameter to the AWS Lambda function in the form of a JSON file (sample below) stored in an Amazon S3 bucket.

[
   {
     "repository": "hello-World-AppRunner-Repo",
      "semVersion": ">1.2.3",
      "serviceArn": "arn:aws:apprunner:us-east-1:123456789123:service/Hello-World-Service/2d0032a93cbb4cbdaef0966607052336"
   }
]

The solution supports Node Package Manager(NPM) style versioning checks, and here are some examples of the match patterns that are supported:

  • >1.2.3 – Matches any version greater than 1.2.3.
  • 1.1 || 1.2.3 – 2.0.0 – Matches 1.1.1 version or any version between 1.2.3 and 2.0.0 (including).
  • 1.* – Matches any version starting with 1.1.
  • ~1.2.1 – Matches any version greater than or equal to 1.2.1 but less than 1.3.0.
  • ^1.2.1 – Matches any version greater than or equal to 1.2.1 but less than 2.0.0.

The following environment variables need to be set in the AWS Lambda function:

  • QUEUE_NAME – Name of the Amazon SQS queue that receives the Amazon ECR push events and trigger the lambda function.
  • CONFIG_BUCKET – Name of the Amazon S3 bucket that contains the JSON file with the match pattern.
  • CONFIG_FILE – Name of the JSON file that contains the match pattern (sample provided under config folder).

The below sequence diagram shows the interaction between different components of the solution, when a new version of the application gets pushed to the Amazon ECR repository:

sequence diagram shows the interaction between different components of the solution.

The solution includes a retry logic. In case of multiple Amazon ECR push events in the same repository trigger, the function waits if the target AWS App Runner service has an update in progress. It retries again in 10 minutes up to a maximum of 3 times before terminating.

Prerequisites

To implement this solution, you need the following prerequisites:

Walkthrough

Setup Amazon ECR repository and AWS App Runner service

Let’s demonstrate the solution by using a sample application from the AWS App Runner documentation.

Store your AWS account number in an environment variable:

AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
AWS_REGION=us-east-1 # Change this to region of your choice

If your Region is not configured, then please configure your AWS CLI using aws configure.

  1. Checkout the sample application from GitHub.
git clone https://github.com/aws-containers/hello-app-runner.git
cd hello-app-runner
  1. Create a new Amazon ECR repository.
aws ecr create-repository --repository-name hello-world-apprunner-repo

The output should be similar to the following text:

{
    "repository": {
        "repositoryArn": "arn:aws:ecr:us-east-1:<<account>>:repository/hello-world-apprunner-repo",
        "registryId": "<<account>>",
        "repositoryName": "hello-world-apprunner-repo",
        "repositoryUri": "<<account>>.dkr.ecr.us-east-1.amazonaws.com/hello-world-apprunner-repo",
        "createdAt": "2023-01-02T15:20:14-08:00",
        "imageTagMutability": "MUTABLE",
        "imageScanningConfiguration": {
            "scanOnPush": false
        },
        "encryptionConfiguration": {
            "encryptionType": "AES256"
        }
    }
}
  1. Login to the Amazon ECR repository.
aws ecr get-login-password --region $AWS_REGION | \
  docker login --username AWS --password-stdin \
  $AWS_ACCOUNT_ID.dkr.ecr.${AWS_REGION}.amazonaws.com
  1. Build the container image, tag and push it to the Amazon ECR repository.
docker build --platform linux/amd64 -t hello-world-apprunner-repo .
docker tag hello-world-apprunner-repo:latest ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/hello-world-apprunner-repo:1.2.3
docker push ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/hello-world-apprunner-repo:1.2.3

Note: Depending upon your hardware and network configuration docker build will take a couple of minutes to complete

  1. Create an AWS App Runner access role and attach Amazon ECR access policy to it.
export TP_FILE=$(mktemp)
export ROLE_NAME=AppRunnerSemVarAccessRole
cat <<EOF | tee $TP_FILE
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "build.apprunner.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
aws iam create-role --role-name $ROLE_NAME --assume-role-policy-document file://$TP_FILE
rm $TP_FILE
aws iam attach-role-policy --role-name $ROLE_NAME --policy-arn arn:aws:iam::aws:policy/service-role/AWSAppRunnerServicePolicyForECRAccess
  1. Create a new AWS App Runner service using the Amazon ECR repository that was created in the previous step.
    1. Create a configuration file for AWS App Runner service:
cat > input.json << EOF
{
    "ServiceName": "hello-world-service",
    "SourceConfiguration": {
        "AuthenticationConfiguration": {
            "AccessRoleArn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/${ROLE_NAME}"
        },
        "ImageRepository": {
            "ImageIdentifier": "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/hello-world-apprunner-repo:1.2.3",
            "ImageConfiguration": {
                "Port": "8000"
            },
            "ImageRepositoryType": "ECR"
        }
    },
    "InstanceConfiguration": {
        "Cpu": "1 vCPU",
        "Memory": "3 GB"
    }
}
EOF
SERVICE_ARN=$(aws apprunner create-service --cli-input-json file://input.json \
 --output text \
 --query 'Service.ServiceArn')
 
SERVICE_ID=$(aws apprunner describe-service --service-arn ${SERVICE_ARN} | jq -r '.Service.ServiceId') 
SERVICE_URL=$(aws apprunner describe-service --service-arn ${SERVICE_ARN} | jq -r '.Service.ServiceUrl') 

echo $SERVICE_URL

## Run describe service command to check the status
aws apprunner describe-service --service-arn \
arn:aws:apprunner:${AWS_REGION}:${AWS_ACCOUNT_ID}:service/hello-world-service/${SERVICE_ID} | jq -r '.Service.Status'
  1. Point your browsers to $SERVICE_URL Once the service is created, you can access the application using the URL. Below is a screenshot of a running service:

Screenshot of a running service

Deploy the solution

  1. Checkout the solution from GitHub
git clone https://github.com/aws-samples/containers-blog-maelstrom.git
cd containers-blog-maelstrom/sem-var-ecr-watcher-app-runner
  1. Create a config file under config/directory with the ServiceArn:
cat > config/config.json<< EOF
[
    {
      "repository": "hello-world-apprunner-repo",
      "semVersion": ">1.2.2",
      "serviceArn": "arn:aws:apprunner:${AWS_REGION}:${AWS_ACCOUNT_ID}:service/hello-world-service/${SERVICE_ID}"
    }
  ]
EOF
  1. If you’re running AWS CDK for the first time, then bootstrap the AWS CDK environment (provide your AWS account ID and AWS Region):
npm i
cdk bootstrap \
    --template bootstrap-template.yaml \
    aws://${AWS_ACCOUNT_ID}/${AWS_REGION}

Note: You only need to bootstrap the AWS CDK one time (skip this step if you have already done this).

  1. Run the following command to deploy the code:
npm i
cdk deploy --requires-approval

Testing

You must publish a new application version to the Amazon ECR repository to test the solution. The latest version should match the semver pattern (>1.2.3) that is specified in the config.json file inside the Amazon S3 bucket.

  1. Update hello world application, by opening templates\index.html and changing And we’re live! to And we’re live, one more time! v1.2.4 in line #184
  2. Build the docker image, tag and push it to the AmazonECR repository.
cd ..
VERSION=1.2.4
docker build --platform linux/amd64 -t hello-world-apprunner-repo .
docker tag hello-world-apprunner-repo:latest ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/hello-world-apprunner-repo:${VERSION}
docker push ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/hello-world-apprunner-repo:${VERSION}

The above action triggers an Amazon ECR event, which gets picked up by the AWS Lambda function. The AWS Lambda function updates the AWS App Runner service with the new image in a few seconds. You can verify this by running the following command:

aws apprunner describe-service --service-arn \
  arn:aws:apprunner:${AWS_REGION}:${AWS_ACCOUNT_ID}:service/hello-world-service/${SERVICE_ID} | jq -r '.Service.Status'

Output

OPERATION_IN_PROGRESS

Event logs for the AWS App Runner service will show the following, which confirms the update is being triggered by the deployed solution:

12-30-2022 01:55:48 PM [CI/CD] Semantic version >1.2.2 matched with the recent 
ECR push 1.2.4, so updating the service to the deploy from the latest version

When the update is successful, you’ll see the following output with the changes applied to the AWS App Runner service:

Output with the changes applied to the App Runner service

Customer benefits

Here are some of the benefits of using the solution outlined in this post:

  • Semantic versioning communicates the impact of a release to users, which makes it easier to understand the risks and benefits of updating to a new version.
  • AWS App Runner deploys new versions of the application automatically based on semantic versioning.
  • Customers can use unique tags (based on build ID, git commit) for each version of the application, which makes tracking and managing the application versions easier.
  • With this approach, customers can start following the best practices in versioning and releasing their software. Yet, they can still leverage AWS App Runner to roll out these changes to their end users without worrying about the underlying infrastructure.
  • The solution outlined in this post is scalable and can deploy multiple applications.

Considerations

Here are some essential items to consider before using this solution:

  • The solution uses AWS App Runner APIs to update and deploy the new version of the application, so it isn’t a fully managed solution. The customer needs to manage the AWS CDK stack and the AWS Lambda function.
  • The solution doesn’t support tracking latest If the customer wants to track the latest or fixed tag, then we recommend using the native CI/CD support in AWS App Runner.
  • The solution uses various AWS services (e.g., Amazon Eventbridge, Amazon SQS, and AWS Lambda) to track the semantic version pattern. As the solution relays on Amazon Eventbridge events, Amazon SQS messages, and AWS Lambda invocations to track the semantic version, it can get expensive if the customer tracks multiple AWS App Runner services and Amazon ECR repositories as it would result in multiple events, Amazon SQS messages, and invocations.
  • The code isn’t production ready and is provided as is. The customer should test the solution in a non-production environment before using it.
  • The solution does not support tracking multiple AWS App Runner services using the same repository. If the customer wants to use the same repository for multiple AWS App Runner services based on the semantic version, then the solution code needs to get updated to support this use case.

Cleaning up

You continue to incur cost until deleting the infrastructure that you created for this post. Use the commands below to delete resources created during this post:

cdk destroy
aws ecr delete-repository --repository-name hello-world-apprunner-repo --force
aws iam detach-role-policy --role-name ${ROLE_NAME} --policy-arn arn:aws:iam::aws:policy/service-role/AWSAppRunnerServicePolicyForECRAccess
aws iam delete-role --role-name ${ROLE_NAME}
aws apprunner delete-service --service-arn ${SERVICE_ARN}
rm  -rf ../../../hello-app-runner

Conclusion

In this blog post I have showed you how to power your release pipelines based on semantic versioning and deliver new versions of the application to their customers fully automated using AWS App Runner. AWS App Runner integrates with many AWS services, such as AWS Secrets Manager and AWS Systems Manager Parameter Store, to securely refer configuration data to deploy and run your applications safely. You can further enhance your application network security using AWS Runner VPC support, which enables private communication for your services with databases and other applications hosted in an Amazon Virtual Private Cloud (Amazon VPC). Please reference our AWS App Runner posts to learn more to build well architected solutions on AWS with AWS App Runner service.

For more information, see the following references:

Build a Continuous Delivery Pipeline for Your Container Images with Amazon ECR as Source