Yohan Beschi
Developer, Cloud Architect and DevOps Advocate
Custom AWS CodeBuild Image CI/CD pipeline for GraalVM
GraalVM is a revolution in the Java world, which has kept quiet for the past few years. The more than a quarter of a Century old language keeps in impressing us.
GraalVM is an OpenSource product from Oracle (there is an enterprise version as-well) which has two main objectives:
- being a Universal Virtual Machine (to write polyglot applications)
- being able to create a native image (creating an executable for programming languages usually requiring a Virtual Machine)
The version 1.0 of GraalVM has been released in April 2018 and currently the latest version is 20.3.0. Needless to say, it is a very fast-moving project.
The native image feature is the most interesting one for any Java developer which has been struggling with AWS Lambda functions and the good old Cold Start. Expecting a lower start time (how much will be a topic for another article) is the only reason to use a native image in a AWS Lambda. As we will see in another upcoming article, building a native image has quite a few drawbacks (at least for now).
Furthermore, when talking about native code, we are extremely dependent on the Runtime Operating System (Amazon Linux or Amazon Linux 2 for AWS Lambda). In other words, we cannot compile our code on any Operating System (this breaks the “write once, run anywhere” slogan created by Sun Microsystems to illustrate the cross-platform benefits of Java). Even if we are using Linux, our system may have native libraries (.so
) in a different version that what we will have while running our code in AWS Lambda. The best way to solve this issue, is to use AWS CodeBuild. Unfortunately, there is no CodeBuild image with GraalVM installed on it. Therefore, we have to create one.
In this article, we are going to see how we can create a CI/CD pipeline to build a CodeBuild Custom image with GraalVM and another one to use this image, build a native image of our code and deploy it in AWS Lambda with an Amazon API Gateway in front.
All the source code presented in this article is available in a Github repository.
AWS CodeBuild Custom Image
AWS CodeBuild images are Docker images that can easily be built by anyone who has little Docker knowledge or at least in writing bash scripts.
We can create a Docker image easily with a DockerFile
. But before writing it we must first choose which OS we are going to use (between Amazon Linux and Amazon Linux 2 otherwise we will have issues with our Lambdas) and which tools we will need.
We will start with a minimal set of tools, which are more than enough to build and deploy a Lambda:
Fortunately, AWS provides all the Dockerfiles and scripts used to build the default Docker images in a Github repository. At least we have a place to start.
Here a snippet of our Dockerfile to install GraalVM.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[...]
RUN set -ex \
# Install GraalVM - https://github.com/graalvm/graalvm-ce-builds/releases
&& mkdir -p $JAVA_HOME \
&& curl -LSso /var/tmp/graalvm-ce-java11-linux-amd64-$GRAALVM_VERSION.tar.gz \
https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-$GRAALVM_VERSION/graalvm-ce-java11-linux-amd64-$GRAALVM_VERSION.tar.gz \
&& echo "$GRAALVM_DOWNLOAD_SHA256 /var/tmp/graalvm-ce-java11-linux-amd64-$GRAALVM_VERSION.tar.gz" | sha256sum -c - \
&& tar xzvf /var/tmp/graalvm-ce-java11-linux-amd64-$GRAALVM_VERSION.tar.gz -C $JAVA_HOME --strip-components=1 \
&& rm /var/tmp/graalvm-ce-java11-linux-amd64-$GRAALVM_VERSION.tar.gz \
&& for tool_path in $JAVA_HOME/bin/*; do \
tool=`basename $tool_path`; \
update-alternatives --install /usr/bin/$tool $tool $tool_path 10000; \
update-alternatives --set $tool $tool_path; \
done \
# Install GraalVM Native image
&& $GRAALVM_HOME/bin/gu install native-image
[...]
In the directory containing the Dockerfile
we can execute (see. Docker installation guide if needed):
1
docker build -t codebuild:latest
We now have a Docker image with GraalVM, this image is quite big and is only available on our local machine. To be able to use it with AWS CodeBuild we need to push it to Amazon Elastic Container Registry (ECR).
Creating an ECR Repository is quite easy with CloudFormation (cicd-codebuild-graalvm.cfn.yml):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ECRRepository:
Type: AWS::ECR::Repository
DeletionPolicy: Retain
Properties:
RepositoryName: spk/codebuild/graalvm
RepositoryPolicyText:
Version: 2012-10-17
Statement:
- Sid: CodeBuildAccess
Effect: Allow
Principal:
Service:
- codebuild.amazonaws.com
Action:
- ecr:GetDownloadUrlForLayer
- ecr:BatchGetImage
- ecr:BatchCheckLayerAvailability
Then we can execute:
1
2
3
4
5
6
7
8
9
AWS_DEFAULT_REGION=us-east-1
AWS_ACCOUNT_ID=0123456789
IMAGE_REPO_NAME=spk/codebuild/graalvm
IMAGE_TAG=latest
aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG .
docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
With a poor internet connection this can take quite a while, but once the image has been uploaded to Amazon ECR, we have a Docker image with GraalVM usable by AWS CodeBuild (the ECR policy in the example above gives to AWS CodeBuild the permissions to use the image).
This solution does the job, but it is not the DevOps way. If we want to modify the GraalVM version, we simple want to change the variable GRAALVM_VERSION
, push the change and do something else. In another word, a 5 min task.
In the next step, we are going to create an AWS CodePipeline pipeline which does exactly that.
A CI/CD pipeline for our Custom Image
We first need to create an AWS CodeCommit git repository which will contain our Dockerfile and the buildspec.yml for AWS CodeBuild.
Again, with Cloudformation it is quite simple (codecommit.cfn.yml)
1
2
3
4
5
CodeBuildGraalVm:
Type: AWS::CodeCommit::Repository
DeletionPolicy: Retain
Properties:
RepositoryName: codebuild-graalvm
Then we can create our CI/CD Pipeline which is composed of the following elements (cicd-codebuild-graalvm.cfn.yml:
- An S3 bucket which will hold AWS CodePipeline and AWS CodeBuild artifacts
- An ECR repository to push the images into
- Permissions for AWS CodePipeline and AWS CodeBuild
- An AWS CodeBuild Project using an Ubuntu image
aws/codebuild/standard:5.0
and with few environment variables to build the image and push it to ECR - An AWS CodePipeline Pipeline with a CodeCommit Source Stage followed by a Build Stage using the CodeBuild Project defined previously
Now every time we will push into codebuild-graalvm
git repository the CI/CD Pipeline will be triggered and generate a new image.
But before closing this section, it is worth mentioning two issues:
- Using only the latest tag will “untag” the previous Docker image created. As the main purpose of this Docker image is to be able to use GraalVM we have 2 options:
- use GraalVM version as the tag name.
- install multiple versions of GraalVM on the same Docker image and switch between runtimes in the
install
phase of thebuildspec.yml
file. To implement this solution the Dockerfile and runtimes.yml of the AWS Amazon Linux 2 Docker image for AWS CodeBuild (aws/codebuild/amazonlinux2-x86_64-standard:3.0
) is a good place to start. As thebuildspec.yml
file should be in the same repository as the applicaton, it should be the preferred option, making the application code tightly tied with GraalVM version. But tagging a Docker image with at least a timestamp (i.e.YYYYMMDD
) is always a good practice.
- By using
FROM amazonlinux:2
we are pulling the image from Docker Hub as an Anonymous user. Anonymous users have a rate limit (100 container image pull requests per six hours) which is quite high for a single user. But Anonymous users have the limits enforced via IP. By building our image in AWS CodeBuild, in a network managed by AWS we are sharing IPs with hundreds, thousands other AWS users, which can lead to the following message “You have reached your pull rate limit. You may increase the limit by authenticating and upgrading: https://www.docker.com/increase-rate-limits.”. To avoid this situation, we have two solutions:- Deploy the CodeBuild Project in our own VPC in a private subnet with a NAT Gateway and an EIP
- Create a free user in Docker Hub, store the credentials in AWS SSM Parameter Store and execute the following commands in the
buildspec
file:
1
2
3
4
5
6
7
8
# pre_build
- aws ssm get-parameter --region $AWS_DEFAULT_REGION --name /docker/password --with-decryption --output text --query Parameter.Value > docker_passwords.txt
- DOCKER_ID=$(aws ssm get-parameter --region $AWS_DEFAULT_REGION --name /docker/userid --with-decryption --output text --query Parameter.Value)
- cat docker_passwords.txt | docker login --username $DOCKER_ID --password-stdin
- rm docker_passwords.txt
# post_build
- docker logout
Building and Deploying a native image in AWS Lambda with GraalVM and Quarkus
Now that we have our image, it is time to use it and actually build and deploy a native image in AWS Lambda behind a HTTP API Gateway.
The application will be a simple Hello World using Quarkus. Quarkus, “Supersonic Subatomic Java”, supported by RedHat, “is a full-stack, Kubernetes-native Java framework made for Java virtual machines (JVMs) and native compilation, optimizing Java specifically for containers and enabling it to become an effective platform for serverless, cloud, and Kubernetes environments” (What is Quarkus?). Quarkus promises to deliver small artifacts, extremely fast boot time, and lower time-to-first request.
The first step is to create a new AWS CodeCommit repository to hold the code of our application. Like for the pipeline we have defined previously, a push into this repository will trigger our new pipeline. This repository will contain:
- a class;
- a
pom.xml
; - a
buildspec.yml
and - an AWS Cloudformation template.
To build the application we can execute: mvn install -Pnative
(this will only work on Linux machines, with GraalVM, the GRALVM_HOME environment variable defined and GraalVM native-image installed).
As is it not an article about Quarkus we won’t go into more detail about the application code.
The Cloudformation template is a regular Serverless Transform to create an AWS Lambda and an HTTP API Gateway (app.cfn.yml), except for the properties Handler
and Runtime
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
HelloworldQuarkusNative:
Type: AWS::Serverless::Function
Properties:
Handler: not.used.in.provided.runtime
Runtime: provided.al2
CodeUri: target/function.zip
MemorySize: 4096
Policies: AWSLambdaBasicExecutionRole
Timeout: 15
Environment:
Variables:
DISABLE_SIGNAL_HANDLERS: true
Events:
ExplicitApi:
Type: HttpApi
Properties:
Method: ANY
Path: /{proxy+}
PayloadFormatVersion: '2.0'
When building a native image, we need to use a custom Runtime
for AWS Lambda, either provided
(Amazon Linux) or provided.al2
(Amazon Linux 2) and the Handler
is not used.
The next and final step is to create an AWS CodePipeline pipeline with:
- An S3 bucket which will hold AWS CodePipeline and AWS CodeBuild artifacts
- An S3 bucket which will hold the generated native images used to deploy the Lambda
- Permissions for AWS CodePipeline and AWS CodeBuild
- An AWS CodeBuild Project using our custom image
!Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/spk/codebuild/graalvm:latest
to build and deploy the image - An AWS CodePipeline Pipeline with a CodeCommit Source Stage followed by a Build Stage using the CodeBuild Project defined previously
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CodeBuildProject:
Type: AWS::CodeBuild::Project
Properties:
Name: quarkus-app
ServiceRole: !Ref CodeBuildServiceRole
Artifacts:
Type: CODEPIPELINE
Source:
Type: CODEPIPELINE
Environment:
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_MEDIUM
Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${CodeBuildImageName}:${CodeBuildImageTag}
EnvironmentVariables:
- Name: ASSETS_BUCKET_NAME
Value: !Ref SsmAssetsBucketNameKey
- Name: STACK_NAME
Value: l-use1-quarkus-helloworld
This AWS CodeBuild Project definition has two noticeable properties:
ComputeType
cannot be smaller thenBUILD_GENERAL1_MEDIUM
Image
references the Docker image we have built previously
Pushing the code of the application into its git repository and creating this pipeline, triggers the creation the native image and deploy it. We only have to use the AWS Console and go to the API Gateway service to find the URL of our new application and test it.
Conclusion
As it is often the case with AWS services, the learning curve is steep. But once we have understood the principles everything is quite easy.
In this article we have seen that with two very simple CI/CD pipelines we can build a custom Docker image for AWS CodeBuild and, build and deploy a native image in AWS Lambda behind an Amazon API Gatway API.