DEV Community

Cover image for Mastering CI/CD: How to Build a Robust Pipeline with GitHub Actions, Docker, and ECS
Jones Ndzenyuy
Jones Ndzenyuy

Posted on

Mastering CI/CD: How to Build a Robust Pipeline with GitHub Actions, Docker, and ECS

I will guide you in this project how to build a secure CI/CD pipeline on AWS that detects code on a Github repository, runs static code analysis on sonar cloud, builds a docker image, scans the image for known vulnerabilities and deploys to ECS using DevSecOps Best practices.

Architecture

Architecture

Principle

Each time a developer commits code to GitHub, it triggers GitHub actions to run static code analysis. The static code analysis consists of maven tests, checkstyle tests, junit, jacoco and quality gates. These static test will ensure that code works as expected, checks for adherence to a set of defined coding conventions, provide annotations to define test methods, assertions to test expected results and runners to execute the tests, generates reports that show which parts of the code base are covered by tests and enforce rules regarding code quality such as minimum code coverage, number of code smells or the absence of critical bugs.

The next step is to build a docker image, the steps involved are declared in the Dockerfile which will be in the code repository, after building the image, it will be safe to run Trivy scan for known vulnerabilities using OWASP scans; the various security assessment techniques to obtain a secure docker image. The image is then Pushed to ECR, AWS' image repository from where we can easily pull to deploy on ECS

How to build it

GitHub Setup

Login to your github account, open gitbash terminal on your local machine and clone the code

git clone https://github.com/Ndzenyuy/Mastering-CICD.git
Enter fullscreen mode Exit fullscreen mode

Make a new folder and copy the files to it, initialize git and open VS Code with the following commands:

  mkdir CICD-with-GitActions
  cp -r Mastering-CICD/* CICD-with-GitActions
  cd CICD-with-GitActions
  git init
  code .
Enter fullscreen mode Exit fullscreen mode

Now VSCode opens, under source control icon, publish project to your github under a public repository.

Sonar Analysis and Quality Gates

We need to start checking our workflow step by step, first we analyse our code for bugs with sonarqube in the file CICD-with-GitActions/.github/workflows/main.yml replace all its contents with

name: CICD-with-GitActions
on: workflow_dispatch

jobs: 
  Testing:
    runs-on: ubuntu-latest
    steps:
      - name: Testing workflow
        uses: actions/checkout@v4

      - name: Maven test
        run: mvn test

      - name: Checkstyle
        run: mvn checkstyle:checkstyle
Enter fullscreen mode Exit fullscreen mode

This portion of code does three things: downloads the source code to github actions, runs maven test and runs mvn checkstyle. This is the first set of tests to be sure the code structure and styles are good and bug free. Now push this code and run it going to:

Github -> Mastering-CICD -> Actions -> Run Workflow
Upon successful completion, we'll have the following

Image description

Image description

Create a sonar cloud organization

Login to Sonar Cloud create an account and link it to your github then
Create a new organization -> Create an organization manually and give the following parameters
Organization name: mastercicd
choose plan: free plan -> create organization

Image description

Next select choose Analyse new project and configure with the following
Organization: mastercicd
Display name: github-actions
Project key: mastercicd_github-actions
Project visibility: public -> next
Previous version = true -> create project

Choose analysis method: Github actions

Copy the sonar token to a sticky note

Image description

Create a quality gate

Under organizations select mastercicd -> Quality gates -> Create -> Name: actionsQG

Image description

Add conditions -> Where?: on overall code
Quality gate fails when: Bugs
Operator: is greater than, Value 35;
Projects: mastercicd_github-actions

Image description

Store Secrets in GitHub

In out Github project repository, go to settings -> secrets and variables -> Actions -> new repository secret: add the following secrets(name: value):

SONAR_TOKEN : xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (input the token retrieved from above when creating project)
SONAR_URL : https://sonarcloud.io
SONAR_ORGANIZATION: mastercicd
SONAR_PROJECT_KEY: mastercicd_github-actions

Test the sonar scanner and the quality gates, replace the content of the workflow(main.yml) with the following

  name: github Actions
on: [push, workflow_dispatch]   
jobs: 
  Testing:
    runs-on: ubuntu-latest
    steps:
      - name: Testing workflow
        uses: actions/checkout@v4

      - name: Maven test
        run: mvn test

      - name: Checkstyle
        run: mvn checkstyle:checkstyle

# Setup java 17 to be default (sonar-scanner requirement as of 5.x)
      - name: Set Java 17
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin' # See 'Supported distributions' for available options
          java-version: '17' 

      # Setup sonar-scanner
      - name: Setup SonarQube
        uses: warchant/setup-sonar-scanner@v7

      # Run sonar-scanner
      - name: SonarQube Scan
        run: sonar-scanner
          -Dsonar.host.url=${{ secrets.SONAR_URL }}
          -Dsonar.login=${{ secrets.SONAR_TOKEN }}
          -Dsonar.organization=${{ secrets.SONAR_ORGANIZATION }}
          -Dsonar.projectKey=${{ secrets.SONAR_PROJECT_KEY }}
          -Dsonar.sources=src/
          -Dsonar.junit.reportsPath=target/surefire-reports/ 
          -Dsonar.jacoco.reportsPath=target/jacoco.exec 
          -Dsonar.java.checkstyle.reportPaths=target/checkstyle-result.xml
          -Dsonar.java.binaries=target/test-classes/com/visualpathit/account/controllerTest/

              # Check the Quality Gate status.

      - name: SonarQube Quality Gate check
        id: sonarqube-quality-gate-check
        uses: sonarsource/sonarqube-quality-gate-action@master
        # Force to fail step after specific time.
        timeout-minutes: 5
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
          SONAR_HOST_URL: ${{ secrets.SONAR_URL }} #OPTIONAL
Enter fullscreen mode Exit fullscreen mode

Job should build successfully

Image description

And sonar cloud should have data

Image description

AWS IAM, ECR and RDS Setup

Create an IAM user

Go to the console and search IAM then create a user with the following policies

Cloudwatch full access
ECR full access
RDS full access
Create access keys -> use case: CLI

Save the access keys in a sticky note.

Setup ECR

Create a new private repository in ECR
name: github-actions -> create
Copy repository URI of the form xxxxxxxxx.dkr.ecr.us-east-2.amazonaws.com and store in sticky notes

Create RDS Database

Goto console and search RDS, create a database with the following parameters
Standard create = true
engine options: MySQL
engine version: 8.0.35
templates: freetier
db instance identifier: github-actions-db
credentials settings:
master username: admin
password: admin123
Instance configuration: db.t3.micro
Connectivity:
VPC security group: create new; -> name: github-actions-sg
Additional configuration:
initial database name: accounts
Leave defaults and Create database

After the database finishes the creating phase, we need to spin an EC2 instance to set up the database with initial data for our configuration. Start an ec2 instance

instance type: t2.micro
OS: ubuntu
security group: ubuntu-actions-sg
edit inbound rules: allow all traffic from my IP

Go to the RDS security group and edit the inbound rules to allow MySQL traffic(3306) from ubuntu-actions-sg. Wait for the RDS to be available and copy the endpoint. Go back in our terminal on local machine, ssh into the ec2 isntance and run the following code(make sure to replace with the RDS endpoint)

 sudo apt-get update
 sudo apt-get install mysql-server -y      
 wget https://raw.githubusercontent.com/Ndzenyuy/vprofile-project/refs/heads/cd-aws/src/main/resources/db_backup.sql
mysql -h <enter-rds-edpoint> -u admin -padmin123 accounts < db_backup.sql
Enter fullscreen mode Exit fullscreen mode

The above code will install mysql server, used to connect to mysql database, clone the project code and import the mysql dump found in db_backup.sql to the db server. At this point, the ec2 instance can be terminated. The Database is ready to be used in our app.

Build and Publish image to ECR

Back to GitHub secrets, add the following secrets
RDS_USER: admin
RDS_PASS: admin123
RDS_ENDPOINT: (url of your rds endpoint)
AWS_ACCESS_KEY_ID: < access key of the created IAM user >
AWS_SECRET_ACCESS_KEY:
REGISTRY: paste the URI of the copied ECR registry that was saved above and remove the /github-actions at the end, it should be like: XXXXXXXXXXXX.dkr.ecr.us-east-1.amazonaws.com

Now update the content of .github/main.yml with the following content

name: github Actions
on: [push, workflow_dispatch]

jobs: 
  Testing:
    runs-on: ubuntu-latest
    steps:
      - name: Testing workflow
        uses: actions/checkout@v4

      - name: Maven test
        run: mvn test

      - name: Checkstyle
        run: mvn checkstyle:checkstyle

# Setup java 17 to be default (sonar-scanner requirement as of 5.x)
      - name: Set Java 17
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin' # See 'Supported distributions' for available options
          java-version: '17' 

      # Setup sonar-scanner
      - name: Setup SonarQube
        uses: warchant/setup-sonar-scanner@v7

      # Run sonar-scanner
      - name: SonarQube Scan
        run: sonar-scanner
          -Dsonar.host.url=${{ secrets.SONAR_URL }}
          -Dsonar.login=${{ secrets.SONAR_TOKEN }}
          -Dsonar.organization=${{ secrets.SONAR_ORGANIZATION }}
          -Dsonar.projectKey=${{ secrets.SONAR_PROJECT_KEY }}
          -Dsonar.sources=src/
          -Dsonar.junit.reportsPath=target/surefire-reports/ 
          -Dsonar.jacoco.reportsPath=target/jacoco.exec 
          -Dsonar.java.checkstyle.reportPaths=target/checkstyle-result.xml
          -Dsonar.java.binaries=target/test-classes/com/visualpathit/account/controllerTest/

              # Check the Quality Gate status.

      - name: SonarQube Quality Gate check
        id: sonarqube-quality-gate-check
        uses: sonarsource/sonarqube-quality-gate-action@master
        # Force to fail step after specific time.
        timeout-minutes: 5
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 

  BUILD_AND_PUBLISH:
    needs: Testing
    runs-on: ubuntu-latest
    steps:
      - name: Code checkout
        uses: actions/checkout@v4

      - name: Update application.properties file
        run: |
          sed -i "s/^jdbc.username.*$/jdbc.username\=${{ secrets.RDS_USER }}/" src/main/resources/application.properties
          sed -i "s/^jdbc.password.*$/jdbc.password\=${{ secrets.RDS_PASS }}/" src/main/resources/application.properties
          sed -i "s/db01/${{ secrets.RDS_ENDPOINT }}/" src/main/resources/application.properties

      - name: Build & Upload image to ECR
        uses: appleboy/docker-ecr-action@master
        with:
          access_key: ${{ secrets.AWS_ACCESS_KEY_ID }}
          secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          registry: ${{ secrets.REGISTRY }}
          repo: github-actions
          region: ${{ env.AWS_REGION }}
          tags: latest,${{ github.run_number }}
          daemon_off: false
          dockerfile: ./Dockerfile
          context: ./
Enter fullscreen mode Exit fullscreen mode

In this pipeline code, we just added the build and publish stage. It'll will build the docker image and push it to ECR.

Now push the code to Github and see the pipeline triggered, If everything works well, we should see a success and the image hosted in ECR.

Image description

I succeeded in my 12th build because i had to update most library versions. That is part of the DevOps Career, solving similar issues.

ECS Setup

Once the image is in ECR, we need to setup ECS to pick this image and deploy it. In AWS console, goto ECS

Create a cluster -> name: github-actions-app -> create. Wait for few minutes for the creation to complete
Create a task definition:
Task definition configuration: github-td
CPU: 1vCPU, memery: 2GiB
Task execution role: create new role
Container details:
name: github-actions
container port: 8080
image URI: <paste the image uri from ECR> : leave the rest as defaults

Now create it. After creation, click on the task execution role which will lead you to IAM. Make sure the policy in this role has AmazonECSTaskExecutionRolePolicy(AWS managed) now we have to add cloudwatch logs full access(CloudWatchLogsFullAccess).

Image description

Back to clusters, create a service with the following configurations:
Deployment configuration:
family: github-actions
service name: github-actions-svc
Deployment failue detection
Use the Amazon ECS deployment circuit breaker = false(uncheck)
Networking:
Security group: create new
name: github-actions-sg
inbound rules: HTTP from anywhere, custom tcp 8080 from anywhere

Load balancing:
Application load balancer
name: github-actions
healthcheck grace period: 30s
listener: create new listener: port 80
Create new target group:
name: github-actions-tg
healthcheck path: /login

Click create, this will take about 10 minutes to create the service and launch the container.

Image description

Select the service github-actions-svc, it opens a new tab, select configuration and networking, scroll down and Copy the DNS of the load balancer
Image description

Paste it in a browser to verify the app is running and is stable

Image description

Now we need to automate our pipeline to update the environment every time there is a new build

Deployment

The last job will be to deploy the latest version of the app each time the pipeline runs. Replace the main.yaml code with the following(I had to comment out Trivy scan because this code had many severe vulnerabilities causing the pipeline to fail. In real life situations, the Developers are to fix the vulnerabilities)

name: github Actions
on: [push, workflow_dispatch]
env: 
  AWS_REGION: us-east-1
  ECR_REPOSITORY: github-actions
  ECS_SERVICE: github-actions-svc
  ECS_CLUSTER: github-actions
  ECS_TASK_DEFINITION: aws-files/taskdeffile.json
  CONTAINER_NAME: github  
jobs: 
  Testing:
    runs-on: ubuntu-latest
    steps:
      - name: Testing workflow
        uses: actions/checkout@v4

      - name: Maven test
        run: mvn test

      - name: Checkstyle
        run: mvn checkstyle:checkstyle

# Setup java 17 to be default (sonar-scanner requirement as of 5.x)
      - name: Set Java 17
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin' # See 'Supported distributions' for available options
          java-version: '17' 

      # Setup sonar-scanner
      - name: Setup SonarQube
        uses: warchant/setup-sonar-scanner@v7

      # Run sonar-scanner
      - name: SonarQube Scan
        run: sonar-scanner
          -Dsonar.host.url=${{ secrets.SONAR_URL }}
          -Dsonar.login=${{ secrets.SONAR_TOKEN }}
          -Dsonar.organization=${{ secrets.SONAR_ORGANIZATION }}
          -Dsonar.projectKey=${{ secrets.SONAR_PROJECT_KEY }}
          -Dsonar.sources=src/
          -Dsonar.junit.reportsPath=target/surefire-reports/ 
          -Dsonar.jacoco.reportsPath=target/jacoco.exec 
          -Dsonar.java.checkstyle.reportPaths=target/checkstyle-result.xml
          -Dsonar.java.binaries=target/test-classes/com/visualpathit/account/controllerTest/

              # Check the Quality Gate status.

      - name: SonarQube Quality Gate check
        id: sonarqube-quality-gate-check
        uses: sonarsource/sonarqube-quality-gate-action@master
        # Force to fail step after specific time.
        timeout-minutes: 5
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 

  BUILD_AND_PUBLISH:
    needs: Testing
    runs-on: ubuntu-latest
    steps:
      - name: Code checkout
        uses: actions/checkout@v4

      - name: Update application.properties file
        run: |
          sed -i "s/^jdbc.username.*$/jdbc.username\=${{ secrets.RDS_USER }}/" src/main/resources/application.properties
          sed -i "s/^jdbc.password.*$/jdbc.password\=${{ secrets.RDS_PASS }}/" src/main/resources/application.properties
          sed -i "s/db01/${{ secrets.RDS_ENDPOINT }}/" src/main/resources/application.properties

      - name: Build & Upload image to ECR
        uses: appleboy/docker-ecr-action@master
        with:
          access_key: ${{ secrets.AWS_ACCESS_KEY_ID }}
          secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          registry: ${{ secrets.REGISTRY }}
          repo: github-actions
          region: ${{ env.AWS_REGION }}
          tags: latest,${{ github.run_number }}
          daemon_off: false
          dockerfile: ./Dockerfile
          context: ./
    #- name: Run Trivy vulnerability scanner
     #  uses: aquasecurity/trivy-action@master
     #  with:
       #   image-ref: ${{ secrets.REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ github.run_number }}
      #    format: 'json'
      #    exit-code: '1'
       #   ignore-unfixed: true
       #   vuln-type: 'os,library'
      #    severity: 'HIGH' 

Deploy:
    needs: BUILD_AND_PUBLISH
    runs-on: ubuntu-latest
    steps:
      - name: Code checkout
        uses: actions/checkout@v4

      - 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: ${{ env.AWS_REGION }}

      - 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: ${{ env.ECS_TASK_DEFINITION }}
          container-name: ${{ env.CONTAINER_NAME }}
          image: ${{ secrets.REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ github.run_number }}

      - name: Deploy Amazon ECS task definition
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          service: ${{ env.ECS_SERVICE }}
          cluster: ${{ env.ECS_CLUSTER }}
          wait-for-service-stability: true
Enter fullscreen mode Exit fullscreen mode

On ECS, goto Task definitions -> github-td -> Revision 1, select the json tab on the page and copy the JSON content of the task definition and paste it in the file /aws-files/taskdeffile.json.

{
    "taskDefinitionArn": "arn:aws:ecs:us-east-1:XXXXXXXXXXXX:task-definition/github-td:2",
    "containerDefinitions": [
        {
            "name": "github",
            "image": "781655249241.dkr.ecr.us-east-1.amazonaws.com/github-actions",
            "cpu": 0,
            "portMappings": [
                {
                    "name": "github-actions-port",
                    "containerPort": 8080,
                    "hostPort": 8080,
                    "protocol": "tcp",
                    "appProtocol": "http"
                }
            ],
            "essential": true,
            "environment": [],
            "environmentFiles": [],
            "mountPoints": [],
            "volumesFrom": [],
            "ulimits": [],
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "/ecs/github-td",
                    "mode": "non-blocking",
                    "awslogs-create-group": "true",
                    "max-buffer-size": "25m",
                    "awslogs-region": "us-east-1",
                    "awslogs-stream-prefix": "ecs"
                },
                "secretOptions": []
            },
            "systemControls": []
        }
    ],
    "family": "github-td",
    "executionRoleArn": "arn:aws:iam::XXXXXXXXXXXX:role/ecsTaskExecutionRole",
    "networkMode": "awsvpc",
    "revision": 2,
    "volumes": [],
    "status": "ACTIVE",
    "requiresAttributes": [
        {
            "name": "com.amazonaws.ecs.capability.logging-driver.awslogs"
        },
        {
            "name": "ecs.capability.execution-role-awslogs"
        },
        {
            "name": "com.amazonaws.ecs.capability.ecr-auth"
        },
        {
            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.19"
        },
        {
            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.28"
        },
        {
            "name": "ecs.capability.execution-role-ecr-pull"
        },
        {
            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.18"
        },
        {
            "name": "ecs.capability.task-eni"
        },
        {
            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.29"
        }
    ],
    "placementConstraints": [],
    "compatibilities": [
        "EC2",
        "FARGATE"
    ],
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "cpu": "1024",
    "memory": "2048",
    "runtimePlatform": {
        "cpuArchitecture": "X86_64",
        "operatingSystemFamily": "LINUX"
    },
    "registeredAt": "2024-12-03T03:24:41.874Z",
    "registeredBy": "arn:aws:iam::XXXXXXXXXXXX:user/ndzenyuyjones@gmail.com",
    "tags": []
}
Enter fullscreen mode Exit fullscreen mode

Now edit the different environment variables to match those you gave in your setup. Push your code to GitHub and watch the pipeline gets built and deployed. The pipeline should be successful with all three stages.

Now edit the security group of the data base to accept inbound transfer of MySQL traffic(3306) from the security group(githubactions-sg) of the ECS service.

Back to our web page, login with the following credentials
username: admin_vp
password: admin_vp

You should land on the welcome page.

Image description

Congratulations, you just succesfully deployed your application on a pipeline using GitHub actions.

Top comments (0)