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.