CI/CD Pipelines
CI/CD là gì?
Continuous Integration (CI)
Định nghĩa: CI là practice trong đó developers thường xuyên integrate code changes vào shared repository. Mỗi integration được verify bằng automated build và tests.
Continuous Delivery (CD)
Định nghĩa: CD đảm bảo code changes được automatically prepared for release to production.
GitHub Actions Example
name: Java CI Pipeline
on:
push:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Run tests
run: mvn test
Best Practices
- Fast Feedback: Run tests quickly
- Security: Scan for vulnerabilities
- Quality Gates: Set coverage thresholds
graph LR
A[Code Commit] --> B[Build]
B --> C[Unit Tests]
C --> D[Integration Tests]
D --> E[Deploy to Staging]
E --> F[E2E Tests]
F --> G[Deploy to Production]
H[CI] --> B
H --> C
H --> D
I[CD] --> E
I --> F
I --> G
GitHub Actions CI/CD
Basic GitHub Actions Pipeline
# .github/workflows/ci-cd.yml
name: Java CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
JAVA_VERSION: '17'
MAVEN_OPTS: '-Xmx1024m'
jobs:
# Job 1: Build và Test
test:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: testdb
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0 # Full clone for SonarCloud
- name: Set up JDK ${{ env.JAVA_VERSION }}
uses: actions/setup-java@v3
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
cache: maven
- name: Cache Maven dependencies
uses: actions/cache@v3
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-m2
- name: Run unit tests
run: mvn test -Dspring.profiles.active=test
- name: Run integration tests
run: mvn verify -Dspring.profiles.active=integration-test
env:
SPRING_DATASOURCE_URL: jdbc:mysql://localhost:3306/testdb
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: rootpassword
- 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: Code coverage
run: mvn jacoco:report
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: target/site/jacoco/jacoco.xml
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
# Job 2: Security Scanning
security:
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up JDK ${{ env.JAVA_VERSION }}
uses: actions/setup-java@v3
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
cache: maven
- name: OWASP Dependency Check
run: mvn org.owasp:dependency-check-maven:check
- name: Upload dependency check results
uses: actions/upload-artifact@v3
if: always()
with:
name: dependency-check-report
path: target/dependency-check-report.html
# Job 3: Build và Package
build:
runs-on: ubuntu-latest
needs: [test, security]
if: github.ref == 'refs/heads/main'
outputs:
image-tag: ${{ steps.meta.outputs.tags }}
image-digest: ${{ steps.build.outputs.digest }}
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up JDK ${{ env.JAVA_VERSION }}
uses: actions/setup-java@v3
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
cache: maven
- name: Build application
run: mvn clean package -DskipTests
- name: Extract metadata
id: meta
uses: docker/metadata-action@v4
with:
images: |
mycompany/user-service
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
id: build
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
BUILD_DATE=${{ github.event.head_commit.timestamp }}
VCS_REF=${{ github.sha }}
VERSION=${{ steps.meta.outputs.version }}
# Job 4: Deploy to Staging
deploy-staging:
runs-on: ubuntu-latest
needs: build
environment: staging
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Deploy to staging
uses: azure/k8s-deploy@v1
with:
namespace: staging
manifests: |
k8s/deployment.yaml
k8s/service.yaml
k8s/ingress.yaml
images: |
mycompany/user-service:${{ needs.build.outputs.image-tag }}
kubectl-version: 'latest'
- name: Run smoke tests
run: |
sleep 30 # Wait for deployment
curl -f https://staging.myapp.com/actuator/health
curl -f https://staging.myapp.com/api/users/health
# Job 5: Deploy to Production
deploy-production:
runs-on: ubuntu-latest
needs: [build, deploy-staging]
environment: production
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Deploy to production
uses: azure/k8s-deploy@v1
with:
namespace: production
manifests: |
k8s/deployment.yaml
k8s/service.yaml
k8s/ingress.yaml
images: |
mycompany/user-service:${{ needs.build.outputs.image-tag }}
kubectl-version: 'latest'
strategy: blue-green
- name: Health check
run: |
sleep 60 # Wait for deployment
curl -f https://api.myapp.com/actuator/health
curl -f https://api.myapp.com/api/users/health
- name: Notify Slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
channel: '#deployments'
message: |
Deployment to production completed!
Image: ${{ needs.build.outputs.image-tag }}
Commit: ${{ github.sha }}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
Advanced Pipeline với Matrix Strategy
# .github/workflows/matrix-build.yml
name: Matrix Build Strategy
on:
push:
branches: [ main, develop ]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
java-version: [11, 17, 21]
exclude:
- os: windows-latest
java-version: 21
- os: macos-latest
java-version: 11
steps:
- uses: actions/checkout@v3
- name: Set up JDK ${{ matrix.java-version }}
uses: actions/setup-java@v3
with:
java-version: ${{ matrix.java-version }}
distribution: 'temurin'
- name: Run tests
run: mvn test
- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: test-results-${{ matrix.os }}-java${{ matrix.java-version }}
path: target/surefire-reports/
Jenkins Pipeline
Declarative Pipeline
// Jenkinsfile
pipeline {
agent any
environment {
JAVA_HOME = '/usr/lib/jvm/java-17-openjdk'
MAVEN_HOME = '/usr/share/maven'
DOCKER_REGISTRY = 'docker.company.com'
APP_NAME = 'user-service'
SONAR_PROJECT_KEY = 'user-service'
}
tools {
maven 'Maven-3.8.6'
jdk 'JDK-17'
}
options {
buildDiscarder(logRotator(numToKeepStr: '10'))
timeout(time: 30, unit: 'MINUTES')
skipStagesAfterUnstable()
parallelsAlwaysFailFast()
}
stages {
stage('Checkout') {
steps {
checkout scm
script {
env.GIT_COMMIT_SHORT = sh(
script: 'git rev-parse --short HEAD',
returnStdout: true
).trim()
env.BUILD_VERSION = "${env.BUILD_NUMBER}-${env.GIT_COMMIT_SHORT}"
}
}
}
stage('Build') {
steps {
sh 'mvn clean compile -DskipTests'
sh 'mvn versions:set -DnewVersion=${BUILD_VERSION}'
}
post {
success {
echo 'Build completed successfully'
}
failure {
echo 'Build failed'
}
}
}
stage('Test') {
parallel {
stage('Unit Tests') {
steps {
sh 'mvn test'
}
post {
always {
publishTestResults testResultsPattern: 'target/surefire-reports/*.xml'
publishCoverage adapters: [
jacocoAdapter('target/site/jacoco/jacoco.xml')
], sourceFileResolver: sourceFiles('STORE_LAST_BUILD')
}
}
}
stage('Integration Tests') {
steps {
sh 'mvn verify -DskipUnitTests'
}
post {
always {
publishTestResults testResultsPattern: 'target/failsafe-reports/*.xml'
}
}
}
stage('Static Analysis') {
steps {
script {
def scannerHome = tool 'SonarQubeScanner'
withSonarQubeEnv('SonarQube') {
sh """
${scannerHome}/bin/sonar-scanner \
-Dsonar.projectKey=${SONAR_PROJECT_KEY} \
-Dsonar.sources=src/main/java \
-Dsonar.tests=src/test/java \
-Dsonar.java.binaries=target/classes \
-Dsonar.junit.reportPaths=target/surefire-reports \
-Dsonar.jacoco.reportPaths=target/jacoco.exec
"""
}
}
timeout(time: 10, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
}
}
stage('Security Scan') {
steps {
sh 'mvn org.owasp:dependency-check-maven:check'
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'target',
reportFiles: 'dependency-check-report.html',
reportName: 'OWASP Dependency Check Report'
])
}
}
stage('Package') {
steps {
sh 'mvn package -DskipTests'
archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
script {
def jarFile = sh(
script: 'find target -name "*.jar" -not -name "*-sources.jar" | head -1',
returnStdout: true
).trim()
env.JAR_FILE = jarFile
}
}
}
stage('Docker Build') {
steps {
script {
def dockerImage = docker.build(
"${DOCKER_REGISTRY}/${APP_NAME}:${BUILD_VERSION}",
"--build-arg JAR_FILE=${JAR_FILE} ."
)
docker.withRegistry("https://${DOCKER_REGISTRY}", 'docker-registry-credentials') {
dockerImage.push()
dockerImage.push('latest')
}
env.DOCKER_IMAGE = "${DOCKER_REGISTRY}/${APP_NAME}:${BUILD_VERSION}"
}
}
}
stage('Deploy to Staging') {
when {
branch 'develop'
}
steps {
script {
kubernetesDeploy(
configs: 'k8s/staging/*.yaml',
kubeconfigId: 'k8s-staging-config',
enableConfigSubstitution: true
)
}
sh """
sleep 30
curl -f https://staging-api.company.com/actuator/health
"""
}
}
stage('Deploy to Production') {
when {
allOf {
branch 'main'
not { changeRequest() }
}
}
steps {
input message: 'Deploy to production?', ok: 'Deploy',
submitterParameter: 'DEPLOYER'
script {
kubernetesDeploy(
configs: 'k8s/production/*.yaml',
kubeconfigId: 'k8s-production-config',
enableConfigSubstitution: true
)
}
sh """
sleep 60
curl -f https://api.company.com/actuator/health
"""
}
post {
success {
slackSend(
channel: '#deployments',
color: 'good',
message: """
✅ Production deployment successful!
Application: ${APP_NAME}
Version: ${BUILD_VERSION}
Deployed by: ${DEPLOYER}
Build: ${BUILD_URL}
"""
)
}
failure {
slackSend(
channel: '#deployments',
color: 'danger',
message: """
❌ Production deployment failed!
Application: ${APP_NAME}
Version: ${BUILD_VERSION}
Build: ${BUILD_URL}
"""
)
}
}
}
}
post {
always {
cleanWs()
}
success {
echo 'Pipeline completed successfully!'
}
failure {
emailext(
subject: "Pipeline Failed: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
body: """
Pipeline failed for ${env.JOB_NAME} - Build ${env.BUILD_NUMBER}
Check the build at: ${env.BUILD_URL}
Last commit: ${env.GIT_COMMIT_SHORT}
""",
to: "${env.CHANGE_AUTHOR_EMAIL ?: 'dev-team@company.com'}"
)
}
}
}
Shared Library cho Jenkins
// vars/javaPipeline.groovy
def call(Map config) {
pipeline {
agent any
environment {
APP_NAME = config.appName
JAVA_VERSION = config.javaVersion ?: '17'
MAVEN_GOALS = config.mavenGoals ?: 'clean package'
}
stages {
stage('Build') {
steps {
buildJavaApp(config)
}
}
stage('Test') {
steps {
testJavaApp(config)
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
deployJavaApp(config)
}
}
}
}
}
// vars/buildJavaApp.groovy
def call(Map config) {
sh """
mvn clean compile -DskipTests
mvn ${config.mavenGoals ?: 'package'} -DskipTests
"""
}
// vars/testJavaApp.groovy
def call(Map config) {
sh 'mvn test'
publishTestResults testResultsPattern: 'target/surefire-reports/*.xml'
if (config.enableSonarQube) {
withSonarQubeEnv('SonarQube') {
sh 'mvn sonar:sonar'
}
}
}
// vars/deployJavaApp.groovy
def call(Map config) {
script {
def dockerImage = docker.build("${config.appName}:${env.BUILD_NUMBER}")
docker.withRegistry(config.dockerRegistry, config.dockerCredentials) {
dockerImage.push()
dockerImage.push('latest')
}
if (config.kubernetesConfig) {
kubernetesDeploy(
configs: config.kubernetesConfig,
kubeconfigId: config.kubeconfigId
)
}
}
}
Sử dụng Shared Library
// Jenkinsfile in application repository
@Library('jenkins-shared-library') _
javaPipeline([
appName: 'user-service',
javaVersion: '17',
mavenGoals: 'clean package',
enableSonarQube: true,
dockerRegistry: 'https://docker.company.com',
dockerCredentials: 'docker-registry-credentials',
kubernetesConfig: 'k8s/*.yaml',
kubeconfigId: 'k8s-production-config'
])
GitLab CI/CD
GitLab CI Configuration
# .gitlab-ci.yml
image: maven:3.8.6-eclipse-temurin-17
variables:
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version"
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
cache:
paths:
- .m2/repository/
- target/
stages:
- validate
- test
- quality
- build
- security
- deploy-staging
- deploy-production
before_script:
- apt-get update -y && apt-get install -y curl
# Validate stage
validate:
stage: validate
script:
- mvn $MAVEN_CLI_OPTS validate compile
# Test stages
unit-tests:
stage: test
script:
- mvn $MAVEN_CLI_OPTS test
artifacts:
when: always
reports:
junit:
- target/surefire-reports/TEST-*.xml
paths:
- target/surefire-reports/
expire_in: 1 week
coverage: '/Total.*?([0-9]{1,3})%/'
integration-tests:
stage: test
services:
- mysql:8.0
variables:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: testdb
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/testdb
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: rootpassword
script:
- mvn $MAVEN_CLI_OPTS verify -DskipUnitTests
artifacts:
when: always
reports:
junit:
- target/failsafe-reports/TEST-*.xml
paths:
- target/failsafe-reports/
expire_in: 1 week
# Quality stage
code-quality:
stage: quality
script:
- mvn $MAVEN_CLI_OPTS sonar:sonar
-Dsonar.host.url=$SONAR_HOST_URL
-Dsonar.login=$SONAR_TOKEN
-Dsonar.projectKey=$CI_PROJECT_NAME
-Dsonar.projectName="$CI_PROJECT_TITLE"
-Dsonar.projectVersion=$CI_COMMIT_SHORT_SHA
-Dsonar.sources=src/main/java
-Dsonar.tests=src/test/java
-Dsonar.java.binaries=target/classes
-Dsonar.junit.reportPaths=target/surefire-reports
-Dsonar.jacoco.reportPaths=target/jacoco.exec
only:
- main
- develop
- merge_requests
# Build stage
build:
stage: build
script:
- mvn $MAVEN_CLI_OPTS package -DskipTests
- echo "APPLICATION_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" >> build.env
artifacts:
paths:
- target/*.jar
reports:
dotenv: build.env
expire_in: 1 week
# Security stage
dependency-check:
stage: security
script:
- mvn org.owasp:dependency-check-maven:check
artifacts:
when: always
paths:
- target/dependency-check-report.html
expire_in: 1 week
allow_failure: true
# Docker build
docker-build:
stage: build
image: docker:latest
services:
- docker:dind
dependencies:
- build
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker build -t $CI_REGISTRY_IMAGE:latest .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
only:
- main
- develop
# Deploy to staging
deploy-staging:
stage: deploy-staging
image: alpine/k8s:latest
environment:
name: staging
url: https://staging-api.company.com
script:
- kubectl config use-context staging
- sed -i "s/<VERSION>/$CI_COMMIT_SHORT_SHA/g" k8s/staging/*.yaml
- kubectl apply -f k8s/staging/
- kubectl rollout status deployment/user-service -n staging
- sleep 30
- curl -f https://staging-api.company.com/actuator/health
only:
- develop
# Deploy to production
deploy-production:
stage: deploy-production
image: alpine/k8s:latest
environment:
name: production
url: https://api.company.com
when: manual
script:
- kubectl config use-context production
- sed -i "s/<VERSION>/$CI_COMMIT_SHORT_SHA/g" k8s/production/*.yaml
- kubectl apply -f k8s/production/
- kubectl rollout status deployment/user-service -n production
- sleep 60
- curl -f https://api.company.com/actuator/health
only:
- main
Pipeline Best Practices
1. Pipeline as Code
- Store pipeline configuration in version control
- Use declarative syntax when possible
- Make pipelines reproducible
2. Fast Feedback
- Run fastest tests first
- Parallel execution where possible
- Fail fast on critical issues
3. Security Integration
- Dependency vulnerability scanning
- SAST (Static Application Security Testing)
- Container image scanning
- Secrets management
4. Quality Gates
- Code coverage thresholds
- Static analysis quality gates
- Performance benchmarks
- Security vulnerability limits
5. Environment Consistency
- Use containers for consistent environments
- Infrastructure as Code
- Configuration management
- Immutable deployments
6. Monitoring và Observability
- Pipeline metrics và monitoring
- Deployment tracking
- Error alerting
- Performance monitoring
CI/CD pipelines là backbone của modern software development, enabling teams to deliver high-quality software reliably và efficiently.