CI/CD & GitOps - Automated Deployment Pipeline
Tổng quan
CI/CD với Kubernetes automation deployment process. GitOps sử dụng Git như single source of truth cho deployments.
CI/CD Pipeline
GitHub Actions Example
name: Build and Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build Docker image
run: |
docker build -t myapp:${{ github.sha }} .
docker push myregistry/myapp:${{ github.sha }}
- name: Deploy to Kubernetes
run: |
kubectl set image deployment/myapp myapp=myregistry/myapp:${{ github.sha }}
Jenkins Pipeline
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'docker build -t myapp:${BUILD_NUMBER} .'
}
}
stage('Test') {
steps {
sh 'helm lint ./charts/myapp'
sh 'helm template ./charts/myapp | kubectl apply --dry-run=client -f -'
}
}
stage('Deploy') {
steps {
sh 'helm upgrade --install myapp ./charts/myapp --set image.tag=${BUILD_NUMBER}'
}
}
}
}
Java-based CI/CD with Jenkins Shared Libraries
Bạn có thể tạo các bước CI/CD phức tạp hơn bằng Java thông qua Jenkins Shared Libraries, cho phép tái sử dụng logic và quản lý pipeline dưới dạng code.
// vars/buildAndDeploy.groovy
// Example of a Jenkins Shared Library function in Groovy (which can call Java code)
def call(body) {
def config = [:]
body.resolveStrategy(Closure.DELEGATE_FIRST).delegate = config
body()
pipeline {
agent any
stages {
stage('Build Docker Image') {
steps {
script {
docker.withRegistry(config.dockerRegistry, config.dockerCredentialsId) {
def customImage = docker.build("${config.imageName}:${env.BUILD_NUMBER}", ".")
customImage.push()
}
}
}
}
stage('Deploy to Kubernetes') {
steps {
script {
// Call a Java utility class for Kubernetes deployment logic
def k8sDeployer = new com.example.cicd.KubernetesDeployer()
k8sDeployer.deploy(config.kubeConfig, config.namespace, config.deploymentName, config.imageName, env.BUILD_NUMBER)
}
}
}
}
}
}
// src/com/example/cicd/KubernetesDeployer.java
package com.example.cicd;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.ConfigBuilder;
public class KubernetesDeployer {
public void deploy(String kubeConfigContent, String namespace, String deploymentName, String imageName, String buildNumber) {
Config config = new ConfigBuilder().withMasterUrl("https://kubernetes.default.svc").withOauthToken(kubeConfigContent).build();
try (KubernetesClient client = new DefaultKubernetesClient(config)) {
Deployment deployment = client.apps().deployments().inNamespace(namespace).withName(deploymentName).get();
if (deployment == null) {
System.out.println("Deployment " + deploymentName + " not found. Please create it first.");
return;
}
String newImage = imageName + ":" + buildNumber;
deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setImage(newImage);
client.apps().deployments().inNamespace(namespace).createOrReplace(deployment);
System.out.println("Deployment " + deploymentName + " updated to image " + newImage);
} catch (Exception e) {
System.err.println("Failed to deploy to Kubernetes: " + e.getMessage());
e.printStackTrace();
}
}
}
GitOps
ArgoCD Installation
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/myapp-config
targetRevision: HEAD
path: k8s
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
Flux v2
apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: GitRepository
metadata:
name: myapp
namespace: flux-system
spec:
interval: 1m
ref:
branch: main
url: https://github.com/myorg/myapp-config
---
apiVersion: kustomize.toolkit.fluxcd.io/v1beta1
kind: Kustomization
metadata:
name: myapp
namespace: flux-system
spec:
interval: 10m
sourceRef:
kind: GitRepository
name: myapp
path: "./clusters/production"
prune: true
Deployment Strategies
Rolling Update
apiVersion: apps/v1
kind: Deployment
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
Blue-Green Deployment
# Blue deployment (current)
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-blue
spec:
replicas: 3
selector:
matchLabels:
app: myapp
version: blue
---
# Green deployment (new)
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-green
spec:
replicas: 3
selector:
matchLabels:
app: myapp
version: green
Canary Deployment
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: myapp
spec:
strategy:
canary:
steps:
- setWeight: 10
- pause: {duration: 1m}
- setWeight: 50
- pause: {duration: 2m}
- setWeight: 100
Security in CI/CD
Image Scanning
- name: Scan image
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myregistry/myapp:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results.sarif'
RBAC for CI/CD
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: deployment-manager
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "create", "update", "patch"]
Testing Strategies
Unit Tests
# Run unit tests
go test ./...
python -m pytest tests/
Integration Tests
# Deploy to test environment
helm install myapp-test ./charts/myapp --namespace test
# Run integration tests
kubectl exec -it test-pod -- /run-tests.sh
# Cleanup
helm uninstall myapp-test --namespace test
Java Unit and Integration Testing
Sử dụng JUnit và Mockito cho unit tests, và Testcontainers cho integration tests với các dịch vụ thực tế.
// Unit Test Example (JUnit + Mockito)
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;
public class UserServiceTest {
@Test
void testGetUserById() {
UserRepository userRepository = mock(UserRepository.class);
when(userRepository.findById(1L)).thenReturn(new User(1L, "testuser"));
UserService userService = new UserService(userRepository);
User user = userService.getUserById(1L);
assertEquals("testuser", user.getUsername());
verify(userRepository, times(1)).findById(1L);
}
}
// Integration Test Example (Testcontainers with PostgreSQL)
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import static org.junit.jupiter.api.Assertions.assertTrue;
@Testcontainers
public class UserRepositoryIntegrationTest {
@Container
public static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@Test
void testDatabaseConnection() throws Exception {
try (Connection conn = DriverManager.getConnection(postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword());
Statement stmt = conn.createStatement()) {
stmt.execute("CREATE TABLE users (id SERIAL PRIMARY KEY, name VARCHAR(255))");
stmt.execute("INSERT INTO users (name) VALUES ('Test User')");
ResultSet rs = stmt.executeQuery("SELECT name FROM users WHERE id = 1");
assertTrue(rs.next());
assertEquals("Test User", rs.getString("name"));
}
}
}
Best Practices
- Store configurations trong Git
- Use immutable tags cho container images
- Implement proper testing stages
- Use secrets management
- Monitor deployment metrics
- Implement rollback strategies
- Separate application code từ configuration
Tools Comparison
| Tool | Type | Pros | Cons |
|---|---|---|---|
| ArgoCD | GitOps | UI, multi-cluster | Complex setup |
| Flux | GitOps | Lightweight, native | Less features |
| Tekton | CI/CD | Kubernetes-native | Learning curve |
| Jenkins | CI/CD | Mature, plugins | Heavy, complex |
Nội dung đã được mở rộng với advanced pipeline configurations và security best practices, cùng các ví dụ Java.