What is CI/CD?
CI/CD stands for Continuous Integration and Continuous Deployment (or Delivery). It's a set of practices that automate the building, testing, and deployment of applications, enabling teams to deliver software faster and more reliably.
CI/CD Components
- Continuous Integration (CI) - Automatically build and test code when changes are pushed
- Continuous Delivery - Automatically prepare releases for deployment (manual trigger)
- Continuous Deployment - Automatically deploy to production after passing all tests
The CI/CD Pipeline
// Typical CI/CD Pipeline Flow
Code Push → Build → Unit Tests → Integration Tests → Package → Deploy
// Detailed stages:
1. Source // Developer pushes code to Git
2. Build // Compile Java code (mvn compile)
3. Unit Test // Run unit tests (mvn test)
4. Code Analysis // SonarQube, checkstyle
5. Package // Create JAR/WAR (mvn package)
6. Integration // Run integration tests
7. Deploy Stage // Deploy to staging environment
8. Deploy Prod // Deploy to production
Benefits of CI/CD
For Development Teams
- Early Bug Detection - Issues caught immediately after commit
- Reduced Integration Problems - Frequent merges prevent "integration hell"
- Faster Feedback - Know within minutes if code breaks something
- Consistent Builds - Same build process every time
For Business
- Faster Time to Market - Deploy features more frequently
- Reduced Risk - Smaller changes are easier to debug
- Higher Quality - Automated testing catches issues
- Reliable Releases - Consistent deployment process
Popular CI/CD Tools
| Tool | Type | Best For |
|---|---|---|
| GitHub Actions | Cloud-hosted | GitHub repositories, easy setup |
| Jenkins | Self-hosted | Enterprise, highly customizable |
| GitLab CI | Cloud/Self-hosted | GitLab users, integrated DevOps |
| CircleCI | Cloud-hosted | Fast builds, Docker support |
| Azure DevOps | Cloud-hosted | Microsoft ecosystem, enterprise |
| Travis CI | Cloud-hosted | Open source projects |
GitHub Actions for Java
GitHub Actions is integrated directly into GitHub repositories, making it easy to set up CI/CD pipelines.
Basic Java Build Workflow
# .github/workflows/build.yml
name: Java CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Build with Maven
run: mvn -B package --file pom.xml
- name: Run tests
run: mvn test
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: app-jar
path: target/*.jar
Multi-Version Testing
# Test on multiple Java versions
name: Multi-Version CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
java: [11, 17, 21]
steps:
- uses: actions/checkout@v4
- name: Set up JDK ${{ matrix.java }}
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java }}
distribution: 'temurin'
cache: maven
- name: Build and Test
run: mvn -B verify
Spring Boot Deployment
# Deploy Spring Boot to Docker Hub
name: Build and Push Docker
on:
push:
branches: [ main ]
tags: [ 'v*' ]
jobs:
build:
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: Build with Maven
run: mvn -B package -DskipTests
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Build and Push Docker Image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
username/myapp:latest
username/myapp:${{ github.sha }}
Jenkins Pipeline
Jenkins is the most widely used self-hosted CI/CD server, offering maximum flexibility and control.
Declarative Pipeline (Jenkinsfile)
// Jenkinsfile in project root
pipeline {
agent any
tools {
maven 'Maven-3.9'
jdk 'JDK-17'
}
environment {
APP_NAME = 'my-java-app'
DEPLOY_ENV = 'staging'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh 'mvn clean compile'
}
}
stage('Unit Tests') {
steps {
sh 'mvn test'
}
post {
always {
junit 'target/surefire-reports/*.xml'
}
}
}
stage('Code Analysis') {
steps {
withSonarQubeEnv('SonarQube') {
sh 'mvn sonar:sonar'
}
}
}
stage('Package') {
steps {
sh 'mvn package -DskipTests'
archiveArtifacts artifacts: 'target/*.jar'
}
}
stage('Deploy to Staging') {
when {
branch 'develop'
}
steps {
sh './deploy.sh staging'
}
}
stage('Deploy to Production') {
when {
branch 'main'
}
steps {
input message: 'Deploy to production?'
sh './deploy.sh production'
}
}
}
post {
success {
slackSend color: 'good', message: "Build successful: ${env.JOB_NAME}"
}
failure {
slackSend color: 'danger', message: "Build failed: ${env.JOB_NAME}"
}
}
}
Parallel Stages
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'mvn clean compile'
}
}
stage('Tests') {
parallel {
stage('Unit Tests') {
steps {
sh 'mvn test -Dtest=*UnitTest'
}
}
stage('Integration Tests') {
steps {
sh 'mvn test -Dtest=*IntegrationTest'
}
}
stage('Code Coverage') {
steps {
sh 'mvn jacoco:report'
}
}
}
}
}
}
GitLab CI/CD
GitLab CI/CD is integrated into GitLab and configured via a .gitlab-ci.yml file.
# .gitlab-ci.yml
image: maven:3.9-eclipse-temurin-17
variables:
MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
cache:
paths:
- .m2/repository/
stages:
- build
- test
- analyze
- package
- deploy
build:
stage: build
script:
- mvn compile
unit-tests:
stage: test
script:
- mvn test
artifacts:
reports:
junit: target/surefire-reports/TEST-*.xml
integration-tests:
stage: test
script:
- mvn verify -DskipUnitTests
artifacts:
reports:
junit: target/failsafe-reports/TEST-*.xml
sonarqube:
stage: analyze
script:
- mvn sonar:sonar -Dsonar.host.url=$SONAR_URL
only:
- main
- develop
package:
stage: package
script:
- mvn package -DskipTests
artifacts:
paths:
- target/*.jar
deploy-staging:
stage: deploy
script:
- ./deploy.sh staging
environment:
name: staging
url: https://staging.example.com
only:
- develop
deploy-production:
stage: deploy
script:
- ./deploy.sh production
environment:
name: production
url: https://example.com
when: manual
only:
- main
Maven Configuration for CI/CD
CI-Friendly POM Configuration
<!-- pom.xml -->
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>my-app</artifactId>
<version>${revision}</version>
<properties>
<revision>1.0.0-SNAPSHOT</revision>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<!-- Flatten POM for CI -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>
<version>1.5.0</version>
<executions>
<execution>
<goals>
<goal>flatten</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Surefire for unit tests -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.2</version>
</plugin>
<!-- Failsafe for integration tests -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.2.2</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- JaCoCo for code coverage -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Maven CI Commands
# Batch mode (no interactive prompts)
mvn -B clean install
# Set version from CI
mvn -B -Drevision=1.2.3 clean package
# Skip tests for quick builds
mvn -B package -DskipTests
# Run only unit tests
mvn test
# Run only integration tests
mvn verify -DskipUnitTests
# Generate site with reports
mvn site
Docker in CI/CD
Dockerfile for Spring Boot
# Multi-stage Dockerfile
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn -B package -DskipTests
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
# Create non-root user
RUN addgroup -S spring && adduser -S spring -G spring
USER spring
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
Docker Compose for Testing
# docker-compose.yml for integration tests
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=test
- SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/testdb
depends_on:
- db
db:
image: postgres:15-alpine
environment:
- POSTGRES_DB=testdb
- POSTGRES_USER=test
- POSTGRES_PASSWORD=test
ports:
- "5432:5432"
CI/CD Best Practices
Pipeline Best Practices
- Fail Fast - Run quick checks (lint, compile) before slow tests
- Cache Dependencies - Speed up builds by caching Maven/Gradle dependencies
- Parallelize - Run independent stages concurrently
- Use Artifacts - Build once, deploy the same artifact everywhere
- Keep Pipelines Short - Target under 10 minutes for feedback
- Secure Secrets - Never hardcode credentials, use secret management
Security Scanning
# GitHub Actions with security scanning
name: Security Scan
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run OWASP Dependency Check
uses: dependency-check/Dependency-Check_Action@main
with:
project: 'my-app'
path: '.'
format: 'HTML'
- name: Upload Report
uses: actions/upload-artifact@v4
with:
name: dependency-check-report
path: reports/
Deployment Strategies
Blue-Green Deployment
// Two identical production environments
Blue (current) <-- Production traffic
Green (new) <-- Deploy and test new version
// After testing, switch traffic
Blue (old)
Green (current) <-- Production traffic now here
// Easy rollback: switch back to Blue
Canary Deployment
// Gradual rollout to subset of users
v1.0 (current) <-- 95% of traffic
v1.1 (canary) <-- 5% of traffic
// Monitor metrics, gradually increase
v1.0 (current) <-- 80% of traffic
v1.1 (canary) <-- 20% of traffic
// Full rollout when confident
v1.1 <-- 100% of traffic
Rolling Deployment
// Update instances one at a time
Instance 1: v1.0 -> v1.1 (updating)
Instance 2: v1.0
Instance 3: v1.0
Instance 1: v1.1 (done)
Instance 2: v1.0 -> v1.1 (updating)
Instance 3: v1.0
// Continue until all updated