Spring Boot Gradle build to ECS: A simple Github Actions workflow

Where I started?

I already had a Spring boot microservice that builds with Gradle and has a docker file. I used to manually deploy in ECS. I needed to automate this workflow so that it deploys automatically on commit.

Here is what I already had,

  • Github repository

  • Code that is built using Gradle with Dockerfile

  • ECR container repository to store images

  • ECS service with task definition already created

Process

We need to have a plan of what we want the Actions workflow to do. Here is the list of things I wanted to do in the workflow.

Step 1: Get triggered on commits to a particular release branch

Step 2: Checkout and build the code

Step 3: Configure AWS ECR container repository access

Step 4: Docker build, tag and push the image to the ECR

Step 5: Re-deploy the ECS service

Create the Actions Workflow

Go to Actions tab in the repository and click on ‘New Workflow’. You can choose to build from scratch or choose one of the templates. There are many options to choose. Since, my service is built using Gradle, I chose a simple Java with Gradle. This will show an editor with a file under .github/workflows. You can choose to name this file anything you want. This is where we write our workflow.

Give a name for this workflow if you choose. Then comes the section where you define when this workflow needs to be triggered.

Step 1: Get triggered on commits to a particular release branch

The on push or pull_request defines that the actions workflow will get triggered on activity in the ‘main’ branch. If you have another release branch, you can mention it instead.

name: Java CI with Gradle

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

Step 2: Checkout and build the code

Now comes, what to do after getting triggered. I need the workflow to define a set of steps or what Github calls “jobs”. You can define each step you want to do as a “job”.

The first job is to build the code. For this, I need to define a runner for Github Action. Github Actions provide free runners (with specific limits). There are options to use Github cloud or Self-hosted runners too. In this example, I am using the public runner available from Github.

I choose the ‘ubuntu-latest’ as the platform to run my build on.

Then comes the steps in the job. I need to checkout the code, setup the right Java version and then build with Gradle. I use jdk17 and hence I specify that and choose the distribution as Eclipse Temurin after looking at different distribution options.

runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'
    - name: Build with Gradle
      uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0
      with:
        arguments: build

    - name: Upload Artifacts  
      uses: actions/upload-artifact@v3
      with:
        name: Package
        path: build/libs

Finally, the built jar is then uploaded to a specific location (build/libs).

Step 3: Configure AWS ECR container repository access

There are existing actions available to manage login to AWS especially to ECR container repository so that the built Docker images can be pushed. To use this, you need to have secrets defined in your repository. As you know, it is a bad idea to have access and secret keys directly in the repository. These are environment variables for Github Actions runner to read and work on.

Defining repository secret(s)

  1. Go to the repository -> Click on ‘Settings’ tab -> Go to Secrets and variables in the left side navigation menu. Choose ‘Actions’.

  2. Then click on ‘New repository secret’ and add the values.

Once done, you can refer to these secerts directly in the workflow.

- name: Configure AWS credentials
      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: us-east-1

    - name: Login to Amazon ECR
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v1

Step 4: Docker build, tag and push the image to the ECR

The ECR_Registry is the output of the login step performed earlier. The repository name (ECR_REPOSITORY) here is defined a Gihub repository secret (as defined in the step earlier).

The next step executes the docker build and tags the image as latest.

Note: I am tagging the image as latest for the build. It is advisable to use specific release versions in order to have better traceability.

Then finally the image is pushed to the container repository.

- name: Build, tag, and push the image to Amazon ECR
      id: build-image
      env:
        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        ECR_REPOSITORY: ${{ secrets.REPO_NAME }}
        IMAGE_TAG: latest
      run: |
        # Build a docker container and push it to ECR 
        docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
        echo "Pushing image to ECR..."
        docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
        # echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"

Step 5: Re-deploy the ECS service

You can either define this as a separate job or add it as a step as part of the previous job. I chose to do the latter.

Just need to add a step to update the service by forcing deployment. Please note that force deployment typically pulls the images from the repository for FARGATE launch type. For provisioned (with EC2), the image may get cached and that might need additional step(s).

aws ecs update-service --cluster <cluster_name> --service <service_name> --force-new-deployment

It is also safer to logout from the AWS ECR once the run is completed. It is especially important if you choose to run from public runners.

- name: Logout of Amazon ECR
      run: docker logout ${{ steps.login-ecr.outputs.registry }}

Completed Workflow

Now, that is done. Let us look at the completed workflow.

name: Java CI with Gradle

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

permissions:
  contents: read

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'
    - name: Build with Gradle
      uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0
      with:
        arguments: build

    - name: Upload Artifacts  
      uses: actions/upload-artifact@v3
      with:
        name: Package
        path: build/libs

    - name: Configure AWS credentials
      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: us-east-1

    - name: Login to Amazon ECR
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v1

    - name: Build, tag, and push the image to Amazon ECR
      id: build-image
      env:
        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        ECR_REPOSITORY: ${{ secrets.REPO_NAME }}
        IMAGE_TAG: latest
      run: |
        # Build a docker container and push it to ECR 
        docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
        echo "Pushing image to ECR..."
        docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
        # echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
        aws ecs update-service --cluster <cluster_name> --service <service_name> --force-new-deployment
    - name: Logout of Amazon ECR
      run: docker logout ${{ steps.login-ecr.outputs.registry }}

Validating this is simple. Do any buildable commits to the branch you have set the trigger on (in this case ‘main’) and the actions wofkflow will trigger and execute. The outcome of the build is shown in the ‘Actions’ tab of the repository.

That’s it. Hope this helps.

💡
It is important to note that free tier of Github Actions executes in shared runners provided by Github. While it is relatively secure by obscurity, it is important to know that it is risk of exposure to secrets and/or credentials that you may configure in the actions. There is an option to have your own self-hosted runners and you might consider that for better security posture.
0
Subscribe to my newsletter

Read articles from Ramesh Lakshmipathy directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ramesh Lakshmipathy
Ramesh Lakshmipathy