June 11, 2025

Модуль 7: CI/CD — Непрерывная интеграция и развертывание

Курс DevOps для новичков 2025


🔄 Что такое CI/CD?

CI/CD — это набор практик, которые автоматизируют процесс доставки кода от разработчика до пользователя. Это позволяет выпускать изменения быстро, безопасно и с минимальным вмешательством человека.

Continuous Integration (CI) — автоматическая сборка, тестирование и интеграция изменений в основную ветку кода

Continuous Delivery (CD) — автоматическая подготовка изменений к развертыванию

Continuous Deployment (CD) — автоматическое развертывание изменений в продакшн

По данным DevOps Research and Assessment 2024, команды с зрелыми CI/CD практиками развертывают код в 208 раз чаще и восстанавливаются после сбоев в 106 раз быстрее.


🎯 Зачем нужен CI/CD?

Проблемы традиционной разработки

  • Долгие циклы релизов (месяцы)
  • Высокий риск ошибок при развертывании
  • Сложности интеграции кода разных разработчиков
  • Ручные процессы тестирования и развертывания
  • "Integration Hell" — проблемы при слиянии кода

Преимущества CI/CD

  • Быстрая обратная связь — ошибки обнаруживаются в течение минут
  • Снижение рисков — маленькие изменения легче откатить
  • Повышение качества — автоматические тесты на каждом этапе
  • Ускорение доставки — от идеи до пользователя за часы, а не месяцы

🏗️ Архитектура CI/CD пайплайна

Типичный пайплайн включает этапы:

1. Source        → Разработчик делает commit в Git
2. Build         → Автоматическая сборка приложения
3. Test          → Запуск автоматических тестов
4. Security Scan → Проверка на уязвимости
5. Package       → Создание артефактов (Docker образы)
6. Deploy Staging → Развертывание в тестовую среду
7. Integration Tests → Тестирование в реальной среде
8. Deploy Production → Развертывание в продакшн
9. Monitor       → Мониторинг работы приложения

Стратегии развертывания

Blue-Green — две идентичные среды, переключение между ними
Canary — постепенный перевод трафика на новую версию
Rolling — поэтапная замена экземпляров приложения


🚀 GitHub Actions — современный CI/CD

Основные концепции

Workflow — автоматизированный процесс, описанный в YAML файле
Job — набор шагов, выполняющихся на одном runner
Step — отдельная задача (команда или action)
Runner — сервер, выполняющий workflows

Базовый workflow для Node.js

# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

env:
  NODE_VERSION: '18'
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        node-version: [16.x, 18.x, 20.x]
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
    
    - name: Setup Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v4
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Run linter
      run: npm run lint
    
    - name: Run unit tests
      run: npm test -- --coverage
    
    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage/lcov.info
    
    - name: Run security audit
      run: npm audit --audit-level moderate

  build-and-push:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    outputs:
      image-digest: ${{ steps.build.outputs.digest }}
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
    
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3
    
    - name: Login to Container Registry
      uses: docker/login-action@v3
      with:
        registry: ${{ env.REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}
    
    - name: Extract metadata
      id: meta
      uses: docker/metadata-action@v5
      with:
        images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
        tags: |
          type=ref,event=branch
          type=sha,prefix={{branch}}-
          type=raw,value=latest,enable={{is_default_branch}}
    
    - name: Build and push Docker image
      id: build
      uses: docker/build-push-action@v5
      with:
        context: .
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}
        cache-from: type=gha
        cache-to: type=gha,mode=max

  deploy-staging:
    needs: build-and-push
    runs-on: ubuntu-latest
    environment: staging
    
    steps:
    - name: Deploy to staging
      run: |
        echo "Deploying to staging environment..."
        echo "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}"
        # Здесь команды для развертывания в staging

  integration-tests:
    needs: deploy-staging
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
    
    - name: Run integration tests
      run: |
        echo "Running integration tests against staging..."
        # npm run test:integration

  deploy-production:
    needs: [build-and-push, integration-tests]
    runs-on: ubuntu-latest
    environment: production
    if: github.ref == 'refs/heads/main'
    
    steps:
    - name: Deploy to production
      run: |
        echo "Deploying to production..."
        echo "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}"
        # kubectl set image deployment/myapp myapp=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}

🛠️ Практические задания

Задание 7.1: Создание простого CI пайплайна

# 1. Создание Node.js приложения
mkdir ci-cd-demo
cd ci-cd-demo
npm init -y

# 2. Установка зависимостей
npm install express
npm install --save-dev jest supertest eslint nodemon

# 3. Настройка package.json
cat > package.json << 'EOF'
{
  "name": "ci-cd-demo",
  "version": "1.0.0",
  "description": "Demo app for CI/CD learning",
  "main": "app.js",
  "scripts": {
    "start": "node app.js",
    "dev": "nodemon app.js",
    "test": "jest",
    "test:watch": "jest --watch",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix"
  },
  "dependencies": {
    "express": "^4.18.2"
  },
  "devDependencies": {
    "jest": "^29.7.0",
    "supertest": "^6.3.3",
    "eslint": "^8.57.0",
    "nodemon": "^3.0.2"
  }
}
EOF

Создание приложения

// app.js
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;

app.use(express.json());

// Простая база данных в памяти
let users = [
  { id: 1, name: 'Alice', email: 'alice@example.com' },
  { id: 2, name: 'Bob', email: 'bob@example.com' }
];

app.get('/', (req, res) => {
  res.json({ 
    message: 'Hello CI/CD!', 
    version: '1.0.0',
    timestamp: new Date().toISOString()
  });
});

app.get('/health', (req, res) => {
  res.json({ 
    status: 'OK', 
    uptime: process.uptime(),
    timestamp: new Date().toISOString()
  });
});

app.get('/users', (req, res) => {
  res.json(users);
});

app.post('/users', (req, res) => {
  const { name, email } = req.body;
  
  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email are required' });
  }
  
  const newUser = {
    id: users.length + 1,
    name,
    email
  };
  
  users.push(newUser);
  res.status(201).json(newUser);
});

if (require.main === module) {
  app.listen(port, () => {
    console.log(`Server running on port ${port}`);
  });
}

module.exports = app;

Создание тестов

// app.test.js
const request = require('supertest');
const app = require('./app');

describe('App', () => {
  test('GET / should return hello message', async () => {
    const response = await request(app).get('/');
    expect(response.status).toBe(200);
    expect(response.body.message).toBe('Hello CI/CD!');
    expect(response.body.version).toBe('1.0.0');
  });

  test('GET /health should return OK status', async () => {
    const response = await request(app).get('/health');
    expect(response.status).toBe(200);
    expect(response.body.status).toBe('OK');
    expect(response.body.uptime).toBeGreaterThanOrEqual(0);
  });

  test('GET /users should return users list', async () => {
    const response = await request(app).get('/users');
    expect(response.status).toBe(200);
    expect(Array.isArray(response.body)).toBe(true);
    expect(response.body.length).toBeGreaterThan(0);
  });

  test('POST /users should create new user', async () => {
    const newUser = {
      name: 'Charlie',
      email: 'charlie@example.com'
    };

    const response = await request(app)
      .post('/users')
      .send(newUser);

    expect(response.status).toBe(201);
    expect(response.body.name).toBe(newUser.name);
    expect(response.body.email).toBe(newUser.email);
    expect(response.body.id).toBeDefined();
  });

  test('POST /users should return 400 for invalid data', async () => {
    const response = await request(app)
      .post('/users')
      .send({ name: 'Test' }); // missing email

    expect(response.status).toBe(400);
    expect(response.body.error).toBe('Name and email are required');
  });
});

Задание 7.2: Dockerfile для приложения

# Dockerfile
FROM node:18-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:18-alpine AS runtime

# Создание пользователя
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nextjs -u 1001

WORKDIR /app

# Копирование зависимостей
COPY --from=builder /app/node_modules ./node_modules
COPY --chown=nextjs:nodejs . .

# Переключение на непривилегированного пользователя
USER nextjs

EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3000/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"

CMD ["npm", "start"]

Задание 7.3: GitHub Actions workflow

# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '18'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Run linter
      run: npm run lint
    
    - name: Run tests
      run: npm test
    
    - name: Security audit
      run: npm audit --audit-level moderate

  build:
    needs: test
    runs-on: ubuntu-latest
    if: github.event_name == 'push'
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3
    
    - name: Login to GitHub Container Registry
      uses: docker/login-action@v3
      with:
        registry: ${{ env.REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}
    
    - name: Build and push
      uses: docker/build-push-action@v5
      with:
        context: .
        push: true
        tags: |
          ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
          ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest

🔧 Jenkins — классический CI/CD

Установка Jenkins

# Установка Java
sudo apt update
sudo apt install openjdk-11-jdk

# Добавление репозитория Jenkins
wget -q -O - https://pkg.jenkins.io/debian/jenkins.io.key | sudo apt-key add -
sudo sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list'

# Установка Jenkins
sudo apt update
sudo apt install jenkins

# Запуск Jenkins
sudo systemctl start jenkins
sudo systemctl enable jenkins

# Получение первоначального пароля
sudo cat /var/lib/jenkins/secrets/initialAdminPassword

Jenkinsfile пример

groovy
pipeline {
    agent any
    
    environment {
        DOCKER_IMAGE = "myapp"
        DOCKER_TAG = "${BUILD_NUMBER}"
        DOCKER_REGISTRY = "ghcr.io"
    }
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
        
        stage('Install Dependencies') {
            steps {
                sh 'npm install'
            }
        }
        
        stage('Lint') {
            steps {
                sh 'npm run lint'
            }
        }
        
        stage('Test') {
            steps {
                sh 'npm test'
            }
            post {
                always {
                    publishTestResults testResultsPattern: 'test-results.xml'
                }
            }
        }
        
        stage('Security Scan') {
            steps {
                sh 'npm audit'
            }
        }
        
        stage('Build Docker Image') {
            steps {
                script {
                    def image = docker.build("${DOCKER_IMAGE}:${DOCKER_TAG}")
                    docker.withRegistry("https://${DOCKER_REGISTRY}", 'github-registry-credentials') {
                        image.push()
                        image.push("latest")
                    }
                }
            }
        }
        
        stage('Deploy to Staging') {
            steps {
                sh """
                    docker run -d --name staging-${BUILD_NUMBER} \
                    -p 3001:3000 \
                    ${DOCKER_IMAGE}:${DOCKER_TAG}
                """
            }
        }
        
        stage('Integration Tests') {
            steps {
                sh 'npm run test:integration'
            }
        }
        
        stage('Deploy to Production') {
            when {
                branch 'main'
            }
            steps {
                script {
                    input message: 'Deploy to production?', ok: 'Deploy'
                }
                sh """
                    kubectl set image deployment/myapp-deployment \
                    myapp=${DOCKER_IMAGE}:${DOCKER_TAG}
                """
            }
        }
    }
    
    post {
        always {
            cleanWs()
        }
        success {
            slackSend channel: '#deployments', 
                      message: "✅ Pipeline succeeded for ${env.JOB_NAME} #${env.BUILD_NUMBER}"
        }
        failure {
            slackSend channel: '#deployments',
                      message: "❌ Pipeline failed for ${env.JOB_NAME} #${env.BUILD_NUMBER}"
        }
    }
}

📊 Мониторинг пайплайнов

Метрики CI/CD

Lead Time — время от коммита до развертывания
Deployment Frequency — частота развертываний
Mean Time to Recovery — время восстановления после сбоя
Change Failure Rate — процент неудачных изменений

Уведомления и алерты

# Добавление в GitHub Actions
- name: Notify Slack on failure
  if: failure()
  uses: 8398a7/action-slack@v3
  with:
    status: failure
    webhook_url: ${{ secrets.SLACK_WEBHOOK }}
    message: |
      ❌ Build failed!
      Repository: ${{ github.repository }}
      Branch: ${{ github.ref }}
      Commit: ${{ github.sha }}

📖 Полезные ресурсы


✅ Чек-лист модуля

  • Понимаю концепции CI/CD и их преимущества
  • Создал простое приложение с тестами
  • Настроил GitHub Actions workflow
  • Реализовал автоматическую сборку Docker образов
  • Настроил автоматическое развертывание
  • Изучил стратегии развертывания (Blue-Green, Canary)
  • Добавил проверки безопасности в пайплайн
  • Настроил уведомления о статусе сборки
  • Понимаю метрики эффективности CI/CD
  • Умею отлаживать проблемы в пайплайнах

🚀 Что дальше?

После освоения CI/CD переходите к Модулю 8: Infrastructure as Code для изучения управления инфраструктурой через код и автоматизации развертывания сред.