Модуль 7: CI/CD — Непрерывная интеграция и развертывание
🔄 Что такое 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 }}
📖 Полезные ресурсы
- GitHub Actions Documentation — официальная документация
- Jenkins Handbook — полное руководство по Jenkins
- GitLab CI/CD — альтернативная платформа CI/CD
- YouTube: "CI/CD с нуля до продакшена" — практический курс
- The DevOps Handbook — книга о DevOps практиках
✅ Чек-лист модуля
- Понимаю концепции CI/CD и их преимущества
- Создал простое приложение с тестами
- Настроил GitHub Actions workflow
- Реализовал автоматическую сборку Docker образов
- Настроил автоматическое развертывание
- Изучил стратегии развертывания (Blue-Green, Canary)
- Добавил проверки безопасности в пайплайн
- Настроил уведомления о статусе сборки
- Понимаю метрики эффективности CI/CD
- Умею отлаживать проблемы в пайплайнах
🚀 Что дальше?
После освоения CI/CD переходите к Модулю 8: Infrastructure as Code для изучения управления инфраструктурой через код и автоматизации развертывания сред.