DevOps
CI/CD Pipeline Project
Build a complete CI/CD pipeline for a Java application using Jenkins, GitHub Actions, Docker, and deploying to AWS ECS with automated testing.
By TechCoder TeamLast updated: 2026-06-02
In a Nutshell
Build a complete CI/CD pipeline for a Java application using Jenkins, GitHub Actions, Docker, and deploying to AWS ECS with automated testing. This hands-on tutorial focuses on practical implementation of ci/cd pipeline project concepts.
Project 1: CI/CD Pipeline for Java Application
Build a production-ready CI/CD pipeline from scratch.
Project Overview
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Commit │───>│ Build │───>│ Test │───>│ Deploy │
│ to Git │ │ (Maven) │ │ (Unit/Int) │ │ to AWS │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│ │ │ │
│ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│ │ JAR │ │ Sonar │ │ ECS │
│ │ Build │ │ Scan │ │ Deploy │
│ └─────────┘ └─────────┘ └─────────┘
Prerequisites
- GitHub account
- AWS account
- Docker Desktop installed
- Basic Java knowledge
Part 1: Application Setup
1.1 Create Spring Boot Application
# Using Spring Initializr
curl https://start.spring.io/starter.zip \
-d dependencies=web,actuator \
-d type=maven-project \
-d baseDir=devops-demo \
-o devops-demo.zip
unzip devops-demo.zip
cd devops-demo
1.2 Add Health Endpoint
// src/main/java/com/example/demo/HealthController.java
package com.example.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
public class HealthController {
@GetMapping("/health")
public Map<String, String> health() {
return Map.of(
"status", "UP",
"version", "1.0.0",
"timestamp", java.time.Instant.now().toString()
);
}
@GetMapping("/")
public String hello() {
return "Hello from DevOps Demo!";
}
}
1.3 Configure Actuator
# src/main/resources/application.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
probes:
enabled: true
info:
app:
name: DevOps Demo
version: 1.0.0
1.4 Add Tests
// src/test/java/com/example/demo/HealthControllerTest.java
package com.example.demo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@WebMvcTest(HealthController.class)
public class HealthControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void healthEndpointReturnsUp() throws Exception {
mockMvc.perform(get("/health"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("UP"));
}
@Test
public void helloEndpointReturnsMessage() throws Exception {
mockMvc.perform(get("/"))
.andExpect(status().isOk());
}
}
Part 2: Docker Configuration
2.1 Create Dockerfile
# Dockerfile
FROM eclipse-temurin:17-jdk-alpine AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN apk add --no-cache maven && \
mvn clean package -DskipTests
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
# Create non-root user
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring
# Copy jar
COPY /app/target/*.jar app.jar
EXPOSE 8080
HEALTHCHECK \
CMD wget --quiet --tries=1 --spider http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
2.2 Create docker-compose.yml
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=docker
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
Part 3: GitHub Actions Pipeline
3.1 Create GitHub Actions Workflow
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
AWS_REGION: us-east-1
ECR_REPOSITORY: devops-demo
ECS_SERVICE: devops-demo-service
ECS_CLUSTER: devops-demo-cluster
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Run Tests
run: mvn clean test
- name: Generate Test Report
uses: dorny/test-reporter@v1
if: success() || failure()
with:
name: Maven Tests
path: target/surefire-reports/*.xml
reporter: java-junit
- name: Upload Coverage
uses: codecov/codecov-action@v3
with:
files: target/site/jacoco/jacoco.xml
security-scan:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
ignore-unfixed: true
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v2
if: always()
with:
sarif_file: 'trivy-results.sarif'
build-and-push:
runs-on: ubuntu-latest
needs: [test, security-scan]
if: github.ref == 'refs/heads/main'
outputs:
image: ${{ steps.build-image.outputs.image }}
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
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: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build, tag, and push image
id: build-image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT
- name: Scan image with Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ steps.build-image.outputs.image }}
format: 'table'
exit-code: '1'
ignore-unfixed: true
severity: 'CRITICAL,HIGH'
deploy:
runs-on: ubuntu-latest
needs: build-and-push
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
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: Download task definition
run: |
aws ecs describe-task-definition --task-definition devops-demo \
--query taskDefinition > task-definition.json
- name: Fill in image ID
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: devops-demo
image: ${{ needs.build-and-push.outputs.image }}
- name: Deploy to ECS
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
- name: Verify deployment
run: |
echo "Deployment complete!"
echo "Image: ${{ needs.build-and-push.outputs.image }}"
Part 4: AWS Infrastructure (Terraform)
4.1 Create ECS Cluster
# terraform/main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_ecs_cluster" "main" {
name = "devops-demo-cluster"
setting {
name = "containerInsights"
value = "enabled"
}
}
resource "aws_ecs_cluster_capacity_providers" "main" {
cluster_name = aws_ecs_cluster.main.name
capacity_providers = ["FARGATE", "FARGATE_SPOT"]
default_capacity_provider_strategy {
base = 1
weight = 1
capacity_provider = "FARGATE"
}
}
# CloudWatch Log Group
resource "aws_cloudwatch_log_group" "app" {
name = "/ecs/devops-demo"
retention_in_days = 7
}
# Task Definition
resource "aws_ecs_task_definition" "app" {
family = "devops-demo"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = "256"
memory = "512"
execution_role_arn = aws_iam_role.ecs_execution.arn
task_role_arn = aws_iam_role.ecs_task.arn
container_definitions = jsonencode([
{
name = "devops-demo"
image = "nginx:latest" # Placeholder, updated by CI/CD
portMappings = [
{
containerPort = 8080
protocol = "tcp"
}
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.app.name
"awslogs-region" = "us-east-1"
"awslogs-stream-prefix" = "ecs"
}
}
healthCheck = {
command = ["CMD-SHELL", "wget --quiet --tries=1 --spider http://localhost:8080/actuator/health || exit 1"]
interval = 30
timeout = 5
retries = 3
startPeriod = 60
}
}
])
}
# ECS Service
resource "aws_ecs_service" "app" {
name = "devops-demo-service"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
desired_count = 2
launch_type = "FARGATE"
network_configuration {
subnets = aws_subnet.public[*].id
security_groups = [aws_security_group.app.id]
assign_public_ip = true
}
load_balancer {
target_group_arn = aws_lb_target_group.app.arn
container_name = "devops-demo"
container_port = 8080
}
deployment_circuit_breaker {
enable = true
rollback = true
}
deployment_controller {
type = "ECS"
}
}
Verification Steps
- Push code to GitHub
- Verify pipeline runs successfully
- Check ECR for pushed image
- Verify ECS service deployment
- Access application via ALB URL
- Check CloudWatch logs
Deliverables
- [ ] GitHub repository with working application
- [ ] GitHub Actions pipeline file
- [ ] Terraform configuration for AWS infrastructure
- [ ] Working ECS deployment
- [ ] Pipeline passing all stages
Next Steps
Move on to the Docker and Kubernetes deployment project.