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
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
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 .
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
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
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
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
Create a quality gate
Under organizations select mastercicd -> Quality gates -> Create -> Name: actionsQG
Add conditions -> Where?: on overall code
Quality gate fails when: Bugs
Operator: is greater than, Value 35;
Projects: mastercicd_github-actions
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
Job should build successfully
And sonar cloud should have data
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
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: ./
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.
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).
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.
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
Paste it in a browser to verify the app is running and is stable
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
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": []
}
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.
Congratulations, you just succesfully deployed your application on a pipeline using GitHub actions.
Top comments (0)