Github Actions: how to deploy application to AWS ECS and migrate database with Flyway
Tags: githubactions, devops, aws, cloud
Published: 2024-05-14 18:38:14

Automatization is extremely important in the context of application development and this is where Github Actions will be great. In this article, we will focus on the necessary instructions to run a workflow that will perform the deployment of our application on AWS ECS and run the database migration using Flyway. An application built using the Spring Framework will be used as an example.

The following instructions necessary for deployment have been developed for the deployment of [CRESH](https://cresh.me), a great platform for validating business ideas.

#Adding secrets in Github Repo
The first step will be to add secrets relating to information such as:
* aws user access key
* aws use secret access key
* datasource connection pool
* database username
* database password

Those fields are necessary for the deployment and database migration.

#Setting workflow environment variables
At the very beginning, we can define the environment variables that we will use in the workflow area. Below are the necessary variables that we will need for deployment:
```
env:
  ECR_REPOSITORY: <ecr_repo_name>
  ECS_SERVICE: <ecs_service_name>
  ECS_CLUSTER: <ecs_cluster_name>
  CONTAINER_NAME: <container_name>
```
#Action Steps
###Setup AWS CLI
Logging into the AWS CLI is essential for the entire workflow. An Access Key and Secret Access Key can be created on the AWS Console for a specific user. Let's assume that for this article, a github_user user has been created to allow you to perform any action using the AWS CLI. Remember that the user you create on AWS should have the appropriate roles.
```
- name: Setup AWS CLI
        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: eu-west-1
```
###Setup Java
A simple step that will be used to set the Java version:
```
- name: Setup Java
        uses: actions/setup-java@v2
        with:
          distribution: 'adopt'
          java-version: '17'
```
###Setup Flyway
Downloading and unpacking a specific version of the flyway. This will enable us to run the migration:
```
      - name: Setup Flyway
        run: |
          curl -LJO https://repo1.maven.org/maven2/org/flywaydb/flyway-commandline/7.14.0/flyway-commandline-7.14.0-linux-x64.tar.gz
          tar xvzf flyway-commandline-7.14.0-linux-x64.tar.gz
          sudo mv flyway-7.14.0 /usr/local/flyway
          sudo ln -s /usr/local/flyway/flyway /usr/local/bin/flyway
```
###Run Flyway Migration
Start the migration process, in which it is necessary to provide database access secrets and indicate the location of the migration files:
```
      - name: Run Flyway Migration
        run: |
          /usr/local/flyway/flyway -url=${{ secrets.SPRING_DATASOURCE_URL }} -user=${{ secrets.SPRING_DATASOURCE_USERNAME }} -password=${{ secrets.SPRING_DATASOURCE_PASSWORD }} -locations=filesystem:src/main/resources/database/migrations migrate
```
###Login to Amazon ECR
**And here app deployment starts!** [The AWS Github actions repo](https://github.com/aws-actions) comes to your aid here. Logging can be done as follows:
```
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@62f4f872db3836360b72999f4b87f1ff13310f3a
```
###Build and Push Docker Image to AWS ECR
Building our application and pushing it to the AWS Elastic Container Registry. In order to build the application, you will need a Dockerfile, which will contain the maven command that prepares the jar file. Example Dockefile:
```
# Use an official Maven image as the build image with Java 17
FROM maven:3.8.4-openjdk-17-slim AS build

# Set the working directory in the container
WORKDIR /app

# Copy the project files into the container
COPY . .

# Build the application
RUN mvn clean install

# Use a lightweight JRE image with Java 17 as the final base image
FROM openjdk:17-jdk-slim

# Set the working directory in the container
WORKDIR /app

# Copy the JAR file from the build image to the runtime image
COPY --from=build /app/target/app_name.jar .

# Specify the command to run the application
CMD ["java", "-jar", "app_name.jar"]
```
In this step, we can also determine how we want to version the pushed image. In the example below, the timestamp will be used along with the short revision specifier, e.g: `2024.05.13_08-18-37-<git_rev>`
```
      - name: Build and Push Docker Image to AWS ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        run: |
          VERSION="$(date +'%Y.%m.%d_%H-%M-%S')-$(git rev-parse --short HEAD)"
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$VERSION .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$VERSION
          echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$VERSION" >> $GITHUB_OUTPUT
```
###Download task definition
This step will download the existing task-definition file, which contains the necessary information about the container, including the Image ID.
```
      - name: Download task definition
        run: |
          aws ecs describe-task-definition --task-definition cresh-backend-ecs-task --query taskDefinition > task-definition.json
          echo "Docker Image URI: ${{ steps.build-image.outputs.image }}"
          replacement_image=${{ steps.build-image.outputs.image }}
          json_file="task-definition.json"
          jq --arg replacement_image "$replacement_image" '.containerDefinitions[0].image = $replacement_image' "$json_file" > tmp.json && mv tmp.json "$json_file"
```
###Fill the new Image ID in the Amazon ECS task definition
Replacing the image ID in the downloaded task-definition.json file is necessary.
```
      - name: Fill in the new image ID in the Amazon ECS task definition
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: ./task-definition.json
          container-name: ${{ env.CONTAINER_NAME }}
          image: ${{ steps.build-image.outputs.image }}
```
###Trigger deployment
Finally, the deployment process takes place by replacing the task definition file with the image ID we want to deploy.
```
      - name: Deploy Amazon ECS task definition
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ./task-definition.json
          service: ${{ env.ECS_SERVICE }}
          cluster: ${{ env.ECS_CLUSTER }}
          wait-for-service-stability: true
```
#Summary
The above article provides the necessary instructions needed to create the workflow responsible for deploying the application on AWS ECS and migrating the database using Flyway. What do you think about this? Maybe you know other ways to build such a workflow? Share it in the comments!

#Useful links
[AWS Github Actions Repo](https://github.com/aws-actions)
[Information about ECS](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/Welcome.html)
[Information about ECR](https://docs.aws.amazon.com/AmazonECR/latest/userguide/what-is-ecr.html)