Containers
How to build container images with Amazon EKS on Fargate
This post was contributed by Re Alvarez Parmar and Olly Pomeroy
Containers help developers simplify the way they package, distribute, and deploy their applications. Developers package their code into a container image that includes the application code, libraries, and any other dependencies. This image can be used to deploy the containerized application on any compatible operating system. Since its launch in 2013, Docker has made it easy to run containers, build images, and push them to repositories.
However, building containers using Docker in environments like Amazon ECS and Amazon EKS requires running Docker in Docker, which has profound implications. Perhaps the least attractive prerequisite for using Docker to build container images in containerized environments is the requirement to run containers in privileged mode, a practice most security-conscious developers would like to avoid. Using Docker to build an image on your laptop may not have severe security implications. Still, it is best to avoid giving containers elevated privileges in a Kubernetes cluster. This hard requirement also makes it impossible to use Docker with EKS on Fargate to build container images because Fargate doesn’t permit privileged containers.
kaniko
New tools have emerged in the past few years to address the problem of building container images without requiring privileged mode. kaniko is one such tool that builds container images from a Dockerfile, much like Docker does. But unlike Docker, it doesn’t depend on a Docker daemon and it executes each command within a Dockerfile entirely in userspace. Thus, it permits you to build container images in environments that can’t easily or securely run a Docker daemon, such as a standard Kubernetes cluster, or on Fargate.
Jenkins
DevOps teams automate container images builds using continuous delivery (CD) tools. AWS customers can either use a fully managed continuous delivery service, like AWS CodePipeline, that automates the software builds, tests, and deployments. Customers can also deploy a self-managed solution like Jenkins on Amazon EC2, Amazon ECS, or Amazon EKS.
Many AWS customers that run a self-managed Jenkins cluster choose to run it in ECS or EKS. Customers running Jenkins on EKS or ECS can use Fargate to run a Jenkins cluster and Jenkins agents without managing servers. Given that Jenkins requires data persistence, you needed EC2 instances to run a Jenkins cluster in the past. Fargate now integrates with Amazon Elastic File System (EFS) to provide storage for your applications, so you can also run the Jenkins controller and agents with EKS and Fargate.
Using kaniko to build your containers and Jenkins to orchestrate build pipelines, you can operate your entire CD infrastructure without any EC2 instances.
How can Fargate help with your self-managed CD infrastructure?
CD workloads are bursty. During business hours, developers check-in their code changes, which triggers CD pipelines, and the demand on the CD system increases. If adequate compute capacity is unavailable, the pipelines compete for resources, and developers have to wait longer to know the effects of their changes. The result is a decline in developer productivity. During off hours, the infrastructure needs to scale back down to the reduce expenses.
Optimizing infrastructure capacity for performance and cost at the same time is challenging for DevOps engineers. On top of that, DevOps teams running self-managed CD infrastructure on Kubernetes are also responsible for managing, scaling, and upgrading their worker nodes.
Teams using Fargate have more time for solving business challenges because they spend less time maintaining servers. Running your CD infrastructure on EKS on Fargate reduces your DevOps team’s operational burden. Besides the obvious benefit of not having to create and manage servers or AMIs, Fargate makes it easy for DevOps teams to operate CD workloads in Kubernetes in these ways:
Easier Kubernetes data plane scaling
Continuous delivery workload constantly fluctuates as code changes trigger pipeline executions. With Fargate, your Kubernetes data plane scales automatically as pods are created and terminated. This means your Kubernetes data plane will scale up as build pipelines get triggered, and scale down as the jobs complete. You don’t even have to run Kubernetes Cluster Autoscaler if your cluster is entirely run on Fargate.
Improved process isolation
Shared clusters without strict compute resource isolation can experience resource contention as multiple containers compete for CPU, memory, disk, and network. Fargate runs each pod in a VM-isolated environment; in other words, no two pods share the same VM. As a result, concurrent CD work streams don’t compete for compute resources.
Simplify Kubernetes upgrades
Upgrading EKS is a two step process. First, you’ll upgrade the EKS control plane. Once finished, you’ll upgrade the data plane and Kubernetes add-ons. Whereas in EC2, you have to cordon nodes, evict pods, and upgrade nodes in batches, in Fargate, to upgrade a node, all you have to do is restart its pod. That’s it.
Pay per pod
In Fargate, you pay for the CPU and memory you reserve for your pods. This can help you reduce your AWS bill since you don’t have to pay for any idle capacity you’d usually have when using EC2 instances to execute CI pipelines. You can further reduce your Fargate costs by getting a Compute Savings Plan.
Fargate also meets the standards for PCI DSS Level 1, ISO 9001, ISO 27001, ISO 27017, ISO 27018, SOC 1, SOC 2, SOC 3, and HIPAA eligibility. For an in-depth look at the benefits of Fargate, we recommend Massimo Re Ferre’s post saving money a pod at a time with EKS, Fargate, and AWS Compute Savings Plans.
Solution
We will create an EKS cluster that will host our Jenkins cluster. Jenkins will run on Fargate, and we’ll use Amazon EFS to persist Jenkins configuration. A Network Load Balancer will distribute traffic to Jenkins. Once Jenkins is operational, we’ll create a pipeline to build container images on Fargate using kaniko.
You will need the following to complete the tutorial:
Let’s start by setting a few environment variables:
export JOF_ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
export JOF_REGION="ap-southeast-1"
export JOF_EKS_CLUSTER=jenkins-on-fargate
Create an EKS cluster
We’ll use eksctl
to create an EKS cluster backed by Fargate. This cluster will have no EC2 instances. Create a cluster:
eksctl create cluster \
--name $JOF_EKS_CLUSTER \
--region $JOF_REGION \
--version 1.18 \
--fargate
With the –-fargate
option, eksctl
creates a pod execution role and Fargate profile and patches the coredns
deployment so that it can run on Fargate.
Prepare the environment
The container image that we’ll use to run Jenkins stores data under /var/jenkins_home
path of the container. We’ll use Amazon EFS to create a file system that we can mount in the Jenkins pod as a persistent volume. This persistent volume will prevent data loss if the Jenkins pod terminates or restarts.
Download the script to prepare the environment:
curl -O https://raw.githubusercontent.com/aws-samples/containers-blog-maelstrom/main/EFS-Jenkins/create-env.sh
chmod +x create-env.sh
. ./create-env.sh
The script will:
- create an EFS file system, EFS mount points, an EFS access point, and a security group
- create an EFS-backed storage class, persistent volume, and persistent volume claim
- deploy the AWS Load Balancer Controller
Install Jenkins
With the load balancer and persistent storage configured, we’re ready to install Jenkins.
Use Helm to install Jenkins in your EKS cluster:
helm repo add jenkins https://charts.jenkins.io && helm repo update &>/dev/null
helm install jenkins jenkins/jenkins \
--set rbac.create=true \
--set controller.servicePort=80 \
--set controller.serviceType=ClusterIP \
--set persistence.existingClaim=jenkins-efs-claim \
--set controller.resources.requests.cpu=2000m \
--set controller.resources.requests.memory=4096Mi \
--set controller.serviceAnnotations."service\.beta\.kubernetes\.io/aws-load-balancer-type"=nlb-ip
The Jenkins Helm chart creates a statefulset
with 1 replica, and the pod will have 2 vCPUs and 4 GB memory. Jenkins will store its data and configuration at /var/jenkins_home
path of the container, which is mapped to the EFS file system we created for Jenkins earlier in this post.
Get the load balancer’s DNS name:
printf $(kubectl get service jenkins -o \
jsonpath="{.status.loadBalancer.ingress[].hostname}");echo
Copy the load balancer’s DNS name and paste it in your browser. You should be taken to the Jenkins dashboard. Log in with username admin
. Retrieve the admin user’s password from Kubernetes secrets:
printf $(kubectl get secret jenkins -o \
jsonpath="{.data.jenkins-admin-password}" \
| base64 --decode);echo
Build images
With Jenkins set up, let’s create a pipeline that includes a step to build container images using kaniko.
Create three Amazon Elastic Container Registry (ECR) repositories that will be used to store the container images for the Jenkins agent, kaniko executor, and sample application used in this demo:
JOF_JENKINS_AGENT_REPOSITORY=$(aws ecr create-repository \
--repository-name jenkins \
--region $JOF_REGION \
--query 'repository.repositoryUri' \
--output text)
JOF_KANIKO_REPOSITORY=$(aws ecr create-repository \
--repository-name kaniko \
--region $JOF_REGION \
--query 'repository.repositoryUri' \
--output text)
JOF_MYSFITS_REPOSITORY=$(aws ecr create-repository \
--repository-name mysfits \
--region $JOF_REGION \
--query 'repository.repositoryUri' \
--output text)
Prepare the Jenkins agent container image:
aws ecr get-login-password \
--region $JOF_REGION | \
docker login \
--username AWS \
--password-stdin $JOF_JENKINS_AGENT_REPOSITORY
docker pull jenkins/inbound-agent:4.3-4-alpine
docker tag docker.io/jenkins/inbound-agent:4.3-4-alpine $JOF_JENKINS_AGENT_REPOSITORY
docker push $JOF_JENKINS_AGENT_REPOSITORY
Prepare the kaniko container image:
mkdir kaniko
cd kaniko
cat > Dockerfile<<EOF
FROM gcr.io/kaniko-project/executor:debug
COPY ./config.json /kaniko/.docker/config.json
EOF
cat > config.json<<EOF
{ "credsStore": "ecr-login" }
EOF
docker build -t $JOF_KANIKO_REPOSITORY .
docker push $JOF_KANIKO_REPOSITORY
Create an IAM role for Jenkins service account. The role lets Jenkins agent pods push and pull images to and from ECR:
eksctl create iamserviceaccount \
--attach-policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser \
--cluster $JOF_EKS_CLUSTER \
--name jenkins-sa-agent \
--namespace default \
--override-existing-serviceaccounts \
--region $JOF_REGION \
--approve
Create a new job in the UI:
Give your job a name and create a new pipeline:
Return to the CLI and create a file with the pipeline configuration:
cat > kaniko-demo-pipeline.json <<EOF
pipeline {
agent {
kubernetes {
label 'kaniko'
yaml """
apiVersion: v1
kind: Pod
metadata:
name: kaniko
spec:
serviceAccountName: jenkins-sa-agent
containers:
- name: jnlp
image: '$(echo $JOF_JENKINS_AGENT_REPOSITORY):latest'
args: ['\\\$(JENKINS_SECRET)', '\\\$(JENKINS_NAME)']
- name: kaniko
image: $(echo $JOF_KANIKO_REPOSITORY):latest
imagePullPolicy: Always
command:
- /busybox/cat
tty: true
restartPolicy: Never
"""
}
}
stages {
stage('Make Image') {
environment {
DOCKERFILE = "Dockerfile.v3"
GITREPO = "git://github.com/ollypom/mysfits.git"
CONTEXT = "./api"
REGISTRY = '$(echo ${JOF_MYSFITS_REPOSITORY%/*})'
IMAGE = 'mysfits'
TAG = 'latest'
}
steps {
container(name: 'kaniko', shell: '/busybox/sh') {
sh '''#!/busybox/sh
/kaniko/executor \\
--context=\${GITREPO} \\
--context-sub-path=\${CONTEXT} \\
--dockerfile=\${DOCKERFILE} \\
--destination=\${REGISTRY}/\${IMAGE}:\${TAG}
'''
}
}
}
}
}
EOF
Copy the contents of kaniko-demo-pipeline.json and paste it into the pipeline script section in Jenkins. It should look like this:
Click the Build Now button to trigger a build.
Once you trigger the build you’ll see that Jenkins has a created another pod. The pipeline uses the Kubernetes plugin for Jenkins to run dynamic Jenkins agents in Kubernetes. The kaniko executor container in this pod will clone to code from the sample code repository, build a container image using the Dockerfile in the project, and push the built image to ECR.
kubectl get pods
NAME READY STATUS RESTARTS AGE
jenkins-0 2/2 Running 0 4m
kaniko-wb2pr-ncc61 0/2 Pending 0 2s
You can see the build by selecting the build in Jenkins and going to Console Output.
Once the build completes, return to AWS CLI and verify that the built container image has been pushed to the sample application’s ECR repository:
aws ecr describe-images \
--repository-name mysfits \
--region $JOF_REGION
The output of the command above should show a new image in the ‘mysfits’ repository.
Cleanup
helm delete jenkins
helm delete aws-load-balancer-controller --namespace kube-system
aws efs delete-access-point --access-point-id $(aws efs describe-access-points --file-system-id $JOF_EFS_FS_ID --region $JOF_REGION --query 'AccessPoints[0].AccessPointId' --output text) --region $JOF_REGION
for mount_target in $(aws efs describe-mount-targets --file-system-id $JOF_EFS_FS_ID --region $JOF_REGION --query 'MountTargets[].MountTargetId' --output text); do aws efs delete-mount-target --mount-target-id $mount_target --region $JOF_REGION; done
sleep 5
aws efs delete-file-system --file-system-id $JOF_EFS_FS_ID --region $JOF_REGION
aws ec2 delete-security-group --group-id $JOF_EFS_SG_ID --region $JOF_REGION
eksctl delete cluster $JOF_EKS_CLUSTER --region $JOF_REGION
aws ecr delete-repository --repository-name jenkins --force --region $JOF_REGION
aws ecr delete-repository --repository-name mysfits --force --region $JOF_REGION
aws ecr delete-repository --repository-name kaniko --force --region $JOF_REGION
Conclusion
With EKS on Fargate, you can run your continuous delivery automation without managing servers, AMIs, and worker nodes. Fargate autoscales your Kubernetes data plane as applications scale in and out. In the case of automated software builds, EKS on Fargate autoscales as pipelines trigger builds, which ensures that each build gets the capacity it requires. This post demonstrated how you can a Jenkins cluster entirely on Fargate and perform container image builds without the need of --privileged
mode.