How to deploy a SpringBoot docker image to ECR from GitLab pipeline?
This is the second installment of our series on deploying a Spring Boot application on AWS Elastic Container Service (ECS) using the AWS Cloud Development Kit (CDK) and Gitlab CI/CD. In the previous post, we created an ECS infrastructure using the AWS CLI and AWS CDK capable of hosting our application.
In this post, we will automate the deployment step using a GitLab pipeline for continuous deployment.
Assuming all code quality, unit tests, and vulnerability scan jobs are configured as required, the following job will run a Maven build using a Java 17 image.
maven-build:
stage: build
image: maven:3.9.6-eclipse-temurin-17-alpine
script:
- mvn -s ci-settings.xml -Dmaven.repo.local=.m2/repository clean install -DskipTests
artifacts:
expire_in: 2 days
paths:
- ./target/
The ci-settings.xml
contains an entry to provide authentication details to interact with the GitLab hosted repository for storing the generated artifacts (in case of Maven deploy).
<servers>
<server>
<id>gitlab-maven</id>
<configuration>
<httpHeaders>
<property>
<name>Job-Token</name>
<value>${CI_JOB_TOKEN}</value>
</property>
</httpHeaders>
</configuration>
</server>
</servers>
Once the Maven build is complete and an executable Spring Boot jar is created, the next step is to build a Docker image for the same and upload it to the Amazon Elastic Container Registry (ECR).
This step expects that you have configured the following values as pipeline variables:
AWS_ACCESS_KEY_ID
: Used to programmatically access AWS services.AWS_SECRET_ACCESS_KEY
: Used to programmatically access AWS services.AWS_ACCOUNT_ID
: Unique identifier for your account.AWS_DEFAULT_REGION
: Default deployment region.docker-build:
# Only on master branch, the container image would be generated
# if the variable is set true or the commit message contains
# 'CI BUILD IMAGE'.
rules:
- if: $CI_COMMIT_REF_NAME == "master" && ( $CREATE_CONTAINER_IMAGE == "true" || $CI_COMMIT_MESSAGE =~ /CI BUILD IMAGE/i )
when: always
image: docker:stable
needs:
- maven-build
stage: build
services:
- docker:dind
before_script:
- apk add --no-cache python3 py3-pip
- pip3 install --no-cache-dir awscli
script:
- 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 pull $TAG_LATEST || true
- docker build --cache-from $TAG_LATEST -t $TAG_LATEST .
- docker push $TAG_LATEST
The next step is to trigger a deployment on ECS. This will recreate our container, possibly changing the public IP address. To handle this, we reset the Route53
record to point to the new IP address.
Following are the high level steps performed in the ecs-deploy
job executed using amazon/aws-cli
image:
ecs-deploy:
stage: deploy
# Only on master branch, the container image would be generated
# if the variable is set true or the commit message contains
# 'CI DEPLOY IMAGE'.
rules:
- if: $CI_COMMIT_REF_NAME == "master" && ( $DEPLOY_CONTAINER_IMAGE == "true" || $CI_COMMIT_MESSAGE =~ /CI DEPLOY IMAGE/i )
when: always
needs:
- docker-build
image:
name: amazon/aws-cli
entrypoint: [""]
before_script:
- yum -y install jq
- aws configure set region $AWS_DEFAULT_REGION
script:
# update the service with the new task definition and desired count
- echo "Updating the service to use latest IMAGE..."
- aws ecs update-service --region $AWS_DEFAULT_REGION --cluster "$ECS_CLUSTER_NAME" --service "$ECS_SERVICE_NAME" --task-definition "$ECS_TASK_DEFINITION_NAME" --force-new-deployment --desired-count 1
- echo sleeping for 180 seconds to allow the service to be deployed
- sleep 180
- TASK_ARN=$(aws ecs list-tasks --cluster "$ECS_CLUSTER_NAME" --service-name "$ECS_SERVICE_NAME" --query 'taskArns[0]' --output text)
- TASK_DETAILS=$(aws ecs describe-tasks --task "$TASK_ARN" --cluster=$ECS_CLUSTER_NAME --query 'tasks[0].attachments[0].details')
- ENI=$(echo $TASK_DETAILS | jq -r '.[] | select(.name=="networkInterfaceId").value')
- PUBLIC_IP=$(aws ec2 describe-network-interfaces --network-interface-ids $ENI --query 'NetworkInterfaces[0].Association.PublicIp' --output text)
- echo "Public IP address is $PUBLIC_IP"
- echo "Updating DNS record with the new IP address"
- EXISTING_IP=$(aws route53 list-resource-record-sets --hosted-zone-id $HOSTED_ZONE_ID --query "ResourceRecordSets[?Name == '"$R53_HOSTNAME."'].ResourceRecords[0].Value" --output text)
- echo "Existing IP address is $EXISTING_IP"
- aws route53 change-resource-record-sets --hosted-zone-id $HOSTED_ZONE_ID --change-batch '{"Changes":[{"Action":"UPSERT","ResourceRecordSet":{"Name":"'$R53_HOSTNAME'","Type":"A","TTL":300,"ResourceRecords":[{"Value":"'$PUBLIC_IP'"}]}}]}'
- echo "DNS record updated successfully to $PUBLIC_IP; Please wait for sometime for the DNS to propagate"
That is all for this post. If you want to share any feedback, please drop me an email, or contact me on any social platforms. I’ll try to respond at the earliest. Also, please consider subscribing feed for regular updates.
Be notified of new posts. Subscribe to the RSS feed.