Лаб. работа "CI/CD с GitLab"

Лабораторная работа по теме “CI/CD с GitLab”

Цель работы

Изучить практические аспекты настройки и использования системы непрерывной интеграции и развертывания (CI/CD) с использованием GitLab. Получить навыки создания пайплайнов, настройки автоматического тестирования, сборки и развертывания приложений.

Краткие теоретические сведения

GitLab CI/CD — это встроенная в GitLab система непрерывной интеграции и развертывания, которая позволяет автоматизировать процессы разработки, тестирования и развертывания приложений.

Ключевые концепции GitLab CI/CD:

  • Runner — агент, который выполняет задачи пайплайна. Может быть общим, групповым или специфичным для проекта.
  • Pipeline — набор задач (jobs), объединенных в стадии (stages), которые выполняются в определенном порядке.
  • Job — минимальная единица выполнения в пайплайне, содержит скрипты для выполнения.
  • Stage — группа jobs, которые выполняются параллельно. Стадии выполняются последовательно.
  • Artifacts — файлы, создаваемые job’ом и передаваемые между стадиями.
  • Variables — переменные окружения для конфигурации пайплайна.
  • Cache — механизм кэширования зависимостей для ускорения сборок.

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

  • Полная интеграция с GitLab
  • Встроенная поддержка Docker
  • Автоматическое обнаружение .gitlab-ci.yml
  • Богатые возможности визуализации
  • Поддержка множественных раннеров
  • Встроенный registry для Docker образов

Краткий справочник по .gitlab-ci.yml

Основная структура

 1stages:                    # Определение стадий
 2  - build
 3  - test
 4  - deploy
 5
 6variables:                 # Глобальные переменные
 7  DOCKER_DRIVER: overlay2
 8  DOCKER_TLS_CERTDIR: "/certs"
 9
10job_name:                  # Имя job'а
11  stage: build            # Стадия выполнения
12  image: node:18          # Docker образ
13  script:                 # Команды для выполнения
14    - npm install
15    - npm run build
16  artifacts:              # Артефакты
17    paths:
18      - dist/
19    expire_in: 1 week
20  only:                   # Условия запуска
21    - main
22    - develop

Основные ключевые слова

  • stages — список стадий пайплайна
  • stage — стадия для конкретного job’а
  • image — Docker образ для выполнения job’а
  • services — дополнительные Docker сервисы
  • script — команды для выполнения
  • before_script — команды перед основным скриптом
  • after_script — команды после основного скрипта
  • artifacts — файлы для сохранения
  • cache — кэширование файлов
  • only/except — условия запуска job’а
  • variables — переменные окружения
  • dependencies — зависимости от других job’ов
  • allow_failure — разрешение на неудачу
  • when — условие выполнения (on_success, on_failure, always)

Типы пайплайнов

  • Branch Pipelines — запускаются при push в ветки
  • Merge Request Pipelines — запускаются при создании MR
  • Scheduled Pipelines — запускаются по расписанию
  • Manual Pipelines — запускаются вручную
  • Triggered Pipelines — запускаются через API

Требования к окружению

  • Доступ к GitLab серверу: it.vstu.by/gitlab
  • Учетная запись в GitLab
  • Git установлен локально
  • Docker установлен локально (для локального тестирования)
  • Базовые знания Git
  • Текстовый редактор

Порядок выполнения работы

Часть 1. Подключение к GitLab

  1. Регистрация/вход в GitLab Перейдите по адресу http://it.vstu.by/gitlab и войдите в систему под своей учетной записью.

  2. Настройка SSH ключей (рекомендуется) Сгенерируйте SSH ключ для безопасной работы с Git:

    1ssh-keygen -t ed25519 -C "your.email@example.com"
    

    Добавьте публичный ключ в GitLab: Profile → SSH Keys → Add new key

  3. Настройка Git локально Настройте Git с вашими данными:

    1git config --global user.name "Ваше Имя"
    2git config --global user.email "your.email@example.com"
    

Часть 2. Создание проекта

  1. Создание нового проекта В GitLab создайте новый проект:

    • Нажмите “New project”
    • Выберите “Create blank project”
    • Введите название: cicd-lab-work
    • Описание: Лабораторная работа по CI/CD
    • Установите видимость: Private
    • Нажмите “Create project”
  2. Клонирование репозитория Склонируйте созданный репозиторий:

    1git clone http://it.vstu.by/gitlab/ваш-username/cicd-lab-work.git
    2cd cicd-lab-work
    

Часть 3. Создание простого приложения

  1. Создание Node.js приложения Создайте простое веб-приложение:

     1# package.json
     2{
     3  "name": "cicd-lab-app",
     4  "version": "1.0.0",
     5  "description": "Simple app for CI/CD lab",
     6  "main": "app.js",
     7  "scripts": {
     8    "start": "node app.js",
     9    "test": "jest",
    10    "test:watch": "jest --watch",
    11    "lint": "eslint .",
    12    "build": "npm run lint && npm test"
    13  },
    14  "dependencies": {
    15    "express": "^4.18.2"
    16  },
    17  "devDependencies": {
    18    "jest": "^29.7.0",
    19    "supertest": "^6.3.3",
    20    "eslint": "^8.50.0"
    21  },
    22  "jest": {
    23    "testEnvironment": "node"
    24  }
    25}
    
  2. Создание основного файла приложения Создайте app.js:

     1const express = require('express');
     2const app = express();
     3const PORT = process.env.PORT || 3000;
     4
     5app.use(express.json());
     6
     7// Health check endpoint
     8app.get('/health', (req, res) => {
     9  res.json({ 
    10    status: 'healthy', 
    11    timestamp: new Date().toISOString(),
    12    version: '1.0.0'
    13  });
    14});
    15
    16// Main endpoint
    17app.get('/', (req, res) => {
    18  res.json({
    19    message: 'Hello from CI/CD Lab App!',
    20    environment: process.env.NODE_ENV || 'development',
    21    timestamp: new Date().toISOString()
    22  });
    23});
    24
    25// API endpoint with basic functionality
    26app.get('/api/data', (req, res) => {
    27  res.json({
    28    data: [
    29      { id: 1, name: 'Item 1', status: 'active' },
    30      { id: 2, name: 'Item 2', status: 'inactive' },
    31      { id: 3, name: 'Item 3', status: 'active' }
    32    ],
    33    total: 3,
    34    timestamp: new Date().toISOString()
    35  });
    36});
    37
    38// Error handling middleware
    39app.use((err, req, res, next) => {
    40  console.error(err.stack);
    41  res.status(500).json({ 
    42    error: 'Something went wrong!',
    43    timestamp: new Date().toISOString()
    44  });
    45});
    46
    47app.listen(PORT, () => {
    48  console.log(`Server is running on port ${PORT}`);
    49});
    50
    51module.exports = app;
    
  3. Создание тестов Создайте папку tests и файл app.test.js:

     1const request = require('supertest');
     2const app = require('../app');
     3
     4describe('API Endpoints', () => {
     5  test('GET / should return welcome message', async () => {
     6    const response = await request(app)
     7      .get('/')
     8      .expect('Content-Type', /json/)
     9      .expect(200);
    10
    11    expect(response.body).toHaveProperty('message');
    12    expect(response.body.message).toContain('Hello from CI/CD Lab App!');
    13  });
    14
    15  test('GET /health should return health status', async () => {
    16    const response = await request(app)
    17      .get('/health')
    18      .expect('Content-Type', /json/)
    19      .expect(200);
    20
    21    expect(response.body).toHaveProperty('status', 'healthy');
    22    expect(response.body).toHaveProperty('timestamp');
    23  });
    24
    25  test('GET /api/data should return data array', async () => {
    26    const response = await request(app)
    27      .get('/api/data')
    28      .expect('Content-Type', /json/)
    29      .expect(200);
    30
    31    expect(response.body).toHaveProperty('data');
    32    expect(response.body).toHaveProperty('total', 3);
    33    expect(Array.isArray(response.body.data)).toBe(true);
    34  });
    35
    36  test('GET /nonexistent should return 404', async () => {
    37    await request(app)
    38      .get('/nonexistent')
    39      .expect(404);
    40  });
    41});
    42
    43describe('Application Logic', () => {
    44  test('should handle missing environment variables', () => {
    45    delete process.env.PORT;
    46    const testApp = require('../app');
    47    expect(testApp).toBeDefined();
    48  });
    49});
    
  4. Создание Dockerfile Создайте Dockerfile:

     1# Build stage
     2FROM node:18-alpine AS builder
     3
     4WORKDIR /app
     5
     6# Copy package files
     7COPY package*.json ./
     8
     9# Install dependencies
    10RUN npm ci --only=production
    11
    12# Production stage
    13FROM node:18-alpine
    14
    15WORKDIR /app
    16
    17# Copy built application
    18COPY --from=builder /app/node_modules ./node_modules
    19COPY . .
    20
    21# Create non-root user
    22RUN addgroup -g 1001 -S nodejs
    23RUN adduser -S nodejs -u 1001
    24
    25# Change ownership
    26RUN chown -R nodejs:nodejs /app
    27USER nodejs
    28
    29# Expose port
    30EXPOSE 3000
    31
    32# Health check
    33HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    34  CMD node healthcheck.js
    35
    36# Start application
    37CMD ["node", "app.js"]
    
  5. Создание healthcheck.js Создайте healthcheck.js:

     1const http = require('http');
     2
     3const options = {
     4  hostname: 'localhost',
     5  port: process.env.PORT || 3000,
     6  path: '/health',
     7  method: 'GET',
     8  timeout: 2000
     9};
    10
    11const req = http.request(options, (res) => {
    12  if (res.statusCode === 200) {
    13    process.exit(0);
    14  } else {
    15    process.exit(1);
    16  }
    17});
    18
    19req.on('error', () => {
    20  process.exit(1);
    21});
    22
    23req.on('timeout', () => {
    24  req.destroy();
    25  process.exit(1);
    26});
    27
    28req.end();
    

Часть 4. Создание базового .gitlab-ci.yml

  1. Создание простого пайплайна Создайте файл .gitlab-ci.yml в корне проекта:

     1stages:
     2  - install
     3  - test
     4  - build
     5  - deploy
     6
     7variables:
     8  NODE_VERSION: "18"
     9  DOCKER_DRIVER: overlay2
    10
    11# Install dependencies
    12install_dependencies:
    13  stage: install
    14  image: node:${NODE_VERSION}
    15  script:
    16    - npm ci
    17  cache:
    18    paths:
    19      - node_modules/
    20  artifacts:
    21    paths:
    22      - node_modules/
    23    expire_in: 1 hour
    24
    25# Run tests
    26test:
    27  stage: test
    28  image: node:${NODE_VERSION}
    29  dependencies:
    30    - install_dependencies
    31  script:
    32    - npm test
    33  coverage: '/Coverage: \d+\.\d+%/'
    34  artifacts:
    35    reports:
    36      junit: junit.xml
    37    paths:
    38      - coverage/
    39    expire_in: 1 week
    40
    41# Build Docker image
    42build_docker:
    43  stage: build
    44  image: docker:latest
    45  services:
    46    - docker:dind
    47  dependencies:
    48    - install_dependencies
    49  script:
    50    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    51    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    52  only:
    53    - main
    54    - develop
    55    - merge_requests
    56
    57# Deploy to staging (manual)
    58deploy_staging:
    59  stage: deploy
    60  image: alpine:latest
    61  script:
    62    - echo "Deploying to staging environment..."
    63    - echo "Application version: $CI_COMMIT_SHA"
    64    - echo "Deployed at: $(date)"
    65  environment:
    66    name: staging
    67    url: https://staging.example.com
    68  when: manual
    69  only:
    70    - main
    
  2. Первый коммит и push Добавьте файлы в Git и отправьте в GitLab:

    1git add .
    2git commit -m "Initial commit: Add basic application and CI/CD pipeline"
    3git push origin main
    
  3. Проверка пайплайна Перейдите в GitLab → CI/CD → Pipelines и посмотрите на выполнение пайплайна.

Часть 5. Расширение пайплайна

  1. Добавление линтинга Обновите .gitlab-ci.yml, добавив стадию линтинга:

     1stages:
     2  - install
     3  - lint
     4  - test
     5  - build
     6  - deploy
     7
     8# Lint code
     9lint:
    10  stage: lint
    11  image: node:${NODE_VERSION}
    12  dependencies:
    13    - install_dependencies
    14  script:
    15    - npm run lint
    16  allow_failure: false
    
  2. Добавление security сканирования Добавьте job для проверки безопасности:

     1# Security scan
     2security_scan:
     3  stage: test
     4  image: node:${NODE_VERSION}
     5  dependencies:
     6    - install_dependencies
     7  script:
     8    - npm audit --audit-level moderate
     9  allow_failure: true
    10  only:
    11    - main
    12    - merge_requests
    
  3. Добавление интеграционных тестов Создайте интеграционные тесты в файле tests/integration.test.js:

     1const request = require('supertest');
     2const app = require('../app');
     3
     4describe('Integration Tests', () => {
     5  let server;
     6
     7  beforeAll((done) => {
     8    server = app.listen(0, () => {
     9      done();
    10    });
    11  });
    12
    13  afterAll((done) => {
    14    server.close(done);
    15  });
    16
    17  test('should handle concurrent requests', async () => {
    18    const promises = [];
    19    for (let i = 0; i < 10; i++) {
    20      promises.push(request(app).get('/api/data'));
    21    }
    22
    23    const responses = await Promise.all(promises);
    24
    25    responses.forEach(response => {
    26      expect(response.status).toBe(200);
    27      expect(response.body).toHaveProperty('data');
    28    });
    29  });
    30
    31  test('should maintain data consistency', async () => {
    32    const response1 = await request(app).get('/api/data');
    33    const response2 = await request(app).get('/api/data');
    34
    35    expect(response1.body.total).toBe(response2.body.total);
    36    expect(response1.body.data.length).toBe(response2.body.data.length);
    37  });
    38});
    
  4. Обновление package.json Добавьте скрипт для интеграционных тестов:

    1{
    2  "scripts": {
    3    "test": "jest",
    4    "test:integration": "jest --testPathPattern=integration",
    5    "test:unit": "jest --testPathPattern=unit"
    6  }
    7}
    
  5. Обновление .gitlab-ci.yml Добавьте job для интеграционных тестов:

     1# Integration tests
     2integration_tests:
     3  stage: test
     4  image: node:${NODE_VERSION}
     5  dependencies:
     6    - install_dependencies
     7  script:
     8    - npm run test:integration
     9  artifacts:
    10    reports:
    11      junit: junit-integration.xml
    12    paths:
    13      - coverage-integration/
    14    expire_in: 1 week
    15  only:
    16    - main
    17    - merge_requests
    

Часть 6. Работа с артефактами и кэшем

  1. Улучшение кэширования Обновите настройки кэша в .gitlab-ci.yml:

    1cache:
    2  paths:
    3    - node_modules/
    4    - .npm/
    5  key: 
    6    files:
    7      - package-lock.json
    8  policy: pull-push
    
  2. Настройка артефактов Добавьте более детальные настройки артефактов:

     1test:
     2  stage: test
     3  image: node:${NODE_VERSION}
     4  dependencies:
     5    - install_dependencies
     6  script:
     7    - npm test
     8  coverage: '/Coverage: \d+\.\d+%/'
     9  artifacts:
    10    when: always
    11    reports:
    12      junit: junit.xml
    13      coverage_report:
    14        coverage_format: cobertura
    15        path: coverage/cobertura-coverage.xml
    16    paths:
    17      - coverage/
    18      - junit.xml
    19    expire_in: 30 days
    20  coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
    

Часть 7. Настройка окружений

  1. Добавление окружений Обновите .gitlab-ci.yml с настройками окружений:

     1# Deploy to staging
     2deploy_staging:
     3  stage: deploy
     4  image: alpine:latest
     5  script:
     6    - echo "Deploying to staging..."
     7    - echo "Environment: staging"
     8    - echo "Version: $CI_COMMIT_SHA"
     9    - echo "Branch: $CI_COMMIT_REF_NAME"
    10    - echo "Author: $GITLAB_USER_NAME"
    11  environment:
    12    name: staging
    13    url: https://staging.example.com
    14    on_stop: stop_staging
    15  when: manual
    16  only:
    17    - main
    18
    19# Stop staging environment
    20stop_staging:
    21  stage: deploy
    22  image: alpine:latest
    23  script:
    24    - echo "Stopping staging environment..."
    25  environment:
    26    name: staging
    27    action: stop
    28  when: manual
    29  only:
    30    - main
    31
    32# Deploy to production
    33deploy_production:
    34  stage: deploy
    35  image: alpine:latest
    36  script:
    37    - echo "Deploying to production..."
    38    - echo "Environment: production"
    39    - echo "Version: $CI_COMMIT_SHA"
    40  environment:
    41    name: production
    42    url: https://production.example.com
    43  when: manual
    44  only:
    45    - tags
    46  dependencies:
    47    - build_docker
    

Часть 8. Создание merge request

  1. Создание feature ветки Создайте новую ветку для добавления функции:

    1git checkout -b feature/add-user-endpoint
    
  2. Добавление нового endpoint Обновите app.js, добавив новый endpoint:

     1// Add user endpoint
     2app.post('/api/users', (req, res) => {
     3  const { name, email } = req.body;
     4
     5  if (!name || !email) {
     6    return res.status(400).json({
     7      error: 'Name and email are required'
     8    });
     9  }
    10
    11  const newUser = {
    12    id: Date.now(),
    13    name,
    14    email,
    15    createdAt: new Date().toISOString()
    16  };
    17
    18  res.status(201).json({
    19    message: 'User created successfully',
    20    user: newUser
    21  });
    22});
    
  3. Добавление тестов Создайте тесты для нового endpoint в tests/users.test.js:

     1const request = require('supertest');
     2const app = require('../app');
     3
     4describe('User Endpoints', () => {
     5  test('POST /api/users should create a new user', async () => {
     6    const userData = {
     7      name: 'John Doe',
     8      email: 'john@example.com'
     9    };
    10
    11    const response = await request(app)
    12      .post('/api/users')
    13      .send(userData)
    14      .expect('Content-Type', /json/)
    15      .expect(201);
    16
    17    expect(response.body).toHaveProperty('message', 'User created successfully');
    18    expect(response.body).toHaveProperty('user');
    19    expect(response.body.user).toHaveProperty('name', userData.name);
    20    expect(response.body.user).toHaveProperty('email', userData.email);
    21  });
    22
    23  test('POST /api/users should return 400 for missing data', async () => {
    24    const response = await request(app)
    25      .post('/api/users')
    26      .send({ name: 'John Doe' })
    27      .expect(400);
    28
    29    expect(response.body).toHaveProperty('error');
    30  });
    31});
    
  4. Коммит и push

    1git add .
    2git commit -m "feat: Add user creation endpoint"
    3git push origin feature/add-user-endpoint
    
  5. Создание Merge Request В GitLab создайте Merge Request:

    • Нажмите “Create merge request”
    • Выберите source branch: feature/add-user-endpoint
    • Target branch: main
    • Добавьте описание изменений
    • Нажмите “Create merge request”
  6. Проверка MR пайплайна Посмотрите, как запустился пайплайн для Merge Request.

Часть 9. Настройка scheduled пайплайнов

  1. Добавление scheduled job Добавьте в .gitlab-ci.yml:

     1# Nightly security scan
     2security_scan_nightly:
     3  stage: test
     4  image: node:${NODE_VERSION}
     5  script:
     6    - npm audit --audit-level high
     7    - echo "Running security scan..."
     8  only:
     9    - schedules
    10  allow_failure: true
    
  2. Создание расписания В GitLab: Settings → CI/CD → Schedules → New Schedule

    • Description: Nightly Security Scan
    • Cron: 0 2 * * * (каждый день в 2:00)
    • Target branch: main
    • Нажмите “Add pipeline schedule”

Часть 10. Мониторинг и отладка

  1. Просмотр логов пайплайна В GitLab перейдите в CI/CD → Pipelines → выберите пайплайн → Jobs → конкретный job

  2. Использование переменных Добавьте debug информацию:

     1debug_info:
     2  stage: test
     3  image: alpine:latest
     4  script:
     5    - echo "=== Debug Information ==="
     6    - echo "Commit SHA: $CI_COMMIT_SHA"
     7    - echo "Branch: $CI_COMMIT_REF_NAME"
     8    - echo "User: $GITLAB_USER_NAME"
     9    - echo "Pipeline ID: $CI_PIPELINE_ID"
    10    - echo "Job ID: $CI_JOB_ID"
    11    - echo "Environment: $CI_ENVIRONMENT_NAME"
    12    - echo "========================"
    13  when: manual
    14  only:
    15    - main
    
  3. Настройка уведомлений В GitLab: Settings → Integrations → настройте Slack, email или другие уведомления

Задания для самостоятельного выполнения

  1. Создание multi-stage Docker build Оптимизируйте Dockerfile для использования multi-stage build и уменьшения размера образа.

  2. Добавление performance тестов Создайте performance тесты и добавьте их в пайплайн с отдельной стадией.

  3. Настройка blue-green deployment Реализуйте стратегию blue-green deployment для production окружения.

  4. Создание API документации Добавьте автоматическую генерацию API документации и включите ее в артефакты.

  5. Настройка мониторинга Интегрируйте мониторинг приложения с Prometheus и добавьте соответствующие проверки в пайплайн.

  6. Создание feature flags Реализуйте систему feature flags и добавьте соответствующие тесты.

  7. Настройка rollback стратегии Создайте автоматический rollback при неудачном деплое.

  8. Добавление code quality метрик Интегрируйте SonarQube или другие инструменты анализа качества кода.

Контрольные вопросы

  1. Что такое GitLab Runner и какие типы раннеров существуют?
  2. В чем разница между stages и jobs в GitLab CI/CD?
  3. Как настроить кэширование зависимостей в пайплайне?
  4. Что такое артефакты и как они используются между job’ами?
  5. Как настроить различные окружения (staging, production)?
  6. В чем преимущество Merge Request пайплайнов?
  7. Как настроить scheduled пайплайны?
  8. Что такое variables в GitLab CI/CD и как их использовать?
  9. Как настроить автоматический rollback при неудачном деплое?
  10. Какие best practices следует соблюдать при создании .gitlab-ci.yml?

Требования к отчету

Отчет по лабораторной работе должен содержать:

  1. Титульный лист
  2. Цель работы
  3. Пошаговое описание выполнения работы с приведением всех вводимых команд и их выводов
  4. Содержимое созданного .gitlab-ci.yml файла с подробными комментариями
  5. Скриншоты выполнения пайплайнов в GitLab
  6. Результаты тестирования всех стадий пайплайна
  7. Описание созданного Merge Request и его обработки
  8. Письменные ответы на контрольные вопросы
  9. Вывод, в котором необходимо кратко описать полученные знания и навыки, а также привести примеры использования GitLab CI/CD в реальных проектах
← Лаб. работа "Docker Compose: Управление …