Как развернуть свой GitLab с CI/CD, используя IaC

2ncbcseivpuh8qvkpjj1hfvjmcy.jpeg

Всем привет! Меня зовут Александр, я обучаюсь в магистратуре СПбПУ. А заодно являюсь младшим разработчиком на C++ и стараюсь использовать и внедрять практики DevOps в мою ежедневную разработку. Недавно я получил зачет за то, что развернул собственный GitLab (именно GitLab, а не аналог) на серверах Selectel с CI/CD и Container Registry. Собственно, об этом и расскажу и в статье.

jargq5pdefecou6_ouzhfadcrr8.gifМы в Selectel готовим новый сервис. Если арендуете серверы в рабочих или личных проектах, нам очень поможет ваш опыт — записывайтесь на короткое онлайн-интервью. За участие подарим плюшевого Тирекса и бонусы на услуги Selectel.

Используйте навигацию, если не хотите читать текст полностью:
→ Задача и инструменты
→ Поднимаем GitLab
→ Запуск GitLab Runner
→ Тестируем Франкенштейна
→ Выводы

Задача и инструменты


В процессе работы необходимо:
  1. развернуть GitLab,
  2. развернуть и зарегистрировать GitLab Runner,
  3. создать учетные записи для студентов из списка.

На выходе на руках должны оказаться список созданных учетных записей для потока студентов и полностью функционирующие GitLab и GitLab Runner. Все нужно описать кодом для дальнейшего переиспользования.

Для описания выделения ресурсов будем использовать Terraform. Он позволяет в формате кода разворачивать цифровые ресурсы. С точки зрения переиспользования конфигурации инфраструктуры это может быть очень полезным инструментом.

Для описания конфигурации серверов будем использовать Ansible. С его помощью можно настраивать созданные серверы путем описания конфигурационного файла с описанием необходимых компонентов.

Действия по развертыванию можно повторить самому, используя проект, опубликованный на GitHub.

dieiksvcuar3umm3kjj24s37br8.png

Поднимаем GitLab


Перед этим шагом необходимо выделить публичный IP и зарегистрировать на него домен нашего будущего GitLab.

После того как публичный IP зарегистрирован, заходим в панель управления Selectel → ПродуктыОблачные серверы.

eb40812c0fc78c5d5de5961e77be035c.png

Нажимаем Создать сервер.
5319bb1d501b52872fd21ebdcd03a50c.png

Кликаем по полю Источник, переходим на вкладку Приложения и выбираем Cloud Gitlab 16.11.10 64-bit.
2edeaf9387e108e07a0a46d68b0e930c.png

Использование готового образа поможет нам избежать ручной настройки БД и ручного конфигурирования GitLab. Кроме того, в образ входит конфигурация Container Registry. Она позволит хранить собранные для проекта Docker-образы.

Фактически, установка GitLab и GitLab Runner на серверах автоматизирована, необходимо лишь выделить нужное количество ресурсов под наши нужды. Это делается на той же странице чуть ниже.

Выбираем в конфигурации сервера 8 vCPU в соответствии с техническими требованиями GitLab.

036eb93449bb70261844f9e01d285ce5.png

Еще чуть ниже добавляем загрузочный диск (SSD Быстрый, 50 ГБ) и диск для данных (SSD Универсальный v2, 100 ГБ).
580e224b8cdb7d7c830c6c5200aa88f8.png

Листаем ниже до раздела Автоматизация. Здесь в поле для ввода текста вписываем скрипт cloud-init. Он позволит настроить root-пользователя и корректную временную зону.
9d5ab495e23c3bd1b963ee7f89ab8970.png

#cloud-config
timezone: Europe/Moscow
# Start GitLab Instance
# Configure GitLab root user and DB
write_files:
- path: "/opt/gomplate/values/user-values.yaml"
  permissions: "0644"
  content: |
    gitlabDomain: ""
    gitlabRootEmail: ""
    gitlabRootPassword: "
"
    gitlabPostgresDB: "gitlab"
    gitlabPostgresUser: "gitlab"
    gitlabPostgresPassword: "gitlab"
    useExternalDB: false

Теперь нажимаем Создать сервер и ждем, когда он запустится. GitLab на это нужно около пяти минут, иногда меньше. Если статус сменился на ACTIVE, сервер готов и GitLab запущен.
c9bb65f0cba8c3a75beda2f08c5a5919.png

Все действия выше можно описать с использованием Terraform.
# Создание ключевой пары для доступа к ВМ
module "keypair" {
  source         = "../modules/keypair"
  keypair_name       = "ssh_key_ed"
  keypair_public_key = file("${var.ssh_key_file}.pub")
  region         = var.region
}
# Создание приватной сети для ВМ
module "nat" {
  source = "../modules/nat"
}
# Создание GitLab-сервера.
module "gitlab_server" {
  source = "../modules/server_gitlab"
  server_name         = "gitlab"
  server_zone         = var.server_zone
  server_vcpus        = var.gitlab_vcpus
  server_ram_mb       = var.gitlab_ram_mb
  server_root_disk_gb     = var.gitlab_root_disk_gb
  server_boot_volume_type = var.gitlab_boot_volume_type
  server_volume_type      = var.server_volume_type
  server_image_name       = var.gitlab_image_name
  server_ssh_key      = module.keypair.keypair_name
  region              = var.region
  network_id          = module.nat.network_id
  subnet_id           = module.nat.subnet_id
  attached_disk_gb    = var.gitlab_attached_disk_gb
  public_ip           = var.gitlab_public_ip
  user_data           = file(var.gitlab_user_data_path)
  server_preemptible_tag = var.server_no_preemptible_tag
}
# Создание inventory файла для ansible
resource "local_file" "ansible_inventory" {
  content = templatefile("../resources/inventory.tmpl",
    {
        gitlab_public_ip  = module.gitlab_server.floating_ip
        ssh_key_file      = var.ssh_key_file
    }
  )
  filename = "../../ansible/resources/inventory.ini"
}

Как можно заметить, мы также добавили создание inventory-файла, который содержит публичный IP создаваемого сервера, путь до SSH-ключа и SSH-порт для доступа к серверу:
[gitlab]
${gitlab_public_ip} ansible_ssh_private_key_file=${ssh_key_file} ansible_port=22022

Inventory-файл пригодится нам на следующем этапе для настройки GitLab с помощью Ansible. Пока что будем держать в голове, что он есть.
Все файлы с конфигурацией Terraform и Ansible можно посмотреть в репозитории.

Запускаем создание инфраструктуры и переходим по указанному домену. Нас встретит страница входа.
53397efdcec3680e58fe6b63490aeabb.png

Заходим с данными root-пользователя, которые мы указали ранее, и радуемся жизни. Первый этап завершен.
2a9ef96f3297ed9b2669c53df3cb5b4c.png

У нас теперь есть свой GitLab!

Конфигурируем GitLab


При конфигурации необходимо создать группу пользователей, учетные записи пользователей и зарегистрировать раннер для группы. Для этого воспользуемся сервисом GitLab Rails, который позволяет управлять GitLab с помощью Ruby-скриптов.

Передача требуемых данных, скриптов, а также их запуск производятся посредствам Ansible.

Конфигурация группы и пользователей


Пользователей нужно создать внутри одной группы, чтобы в дальнейшем они могли использовать раннеры, привязанные к группе, а не к каждому пользователю.
def is_invalid_username(username);
    is_invalid = false;
    if User.find_by_username(username);
        is_invalid = true;
    end;
    is_invalid;
End;
# Читаем список пользователей
users_list = File.read('/tmp/users.txt').split(/\n/);
unique_users = users_list.group_by { |item| item.downcase };
users_creds = {'users' => Array.new};
# Создаем группу
group_name = 'Devops' + Date.today.cwyear.to_s;
unless Group.find_by_path_or_name(group_name);
    puts 'Creating group';
    group = Group.create;
    group.name = group_name;
    group.path = group.name.downcase;
    group.lfs_enabled = false;
    group.add_owner(User.first);
    group.save!;
end;
group = Group.find_by_path_or_name(group_name);
# Создаем пользователей
puts 'Creating users';
unique_users.each do |key, names|;
    names.each_index do |index|;
        name = names[index];
        t_username = name.gsub(/[[:space:]]/, '').downcase;
        username = t_username;
        password = SecureRandom.hex(12);
        if is_invalid_username(username)
            i = 1;
            while not is_invalid_username(username);
                username = t_username + i.to_s;
                i += 1;
            end;
        end;
    user = User.new(username: "#{username}", email: "#{username}@devops-spbstu.ru", name: "#{name}", password: "#{password}", password_confirmation: "#{password}", admin: false)
    user.assign_personal_namespace(Organizations::Organization.default_organization)
    user.skip_confirmation! # Пропускаем авторизацию пользователя
    user.save!; # Сохраняем пользователя
    users_creds['users'].append({"#{user.name}" => {'login'=>"#{user.username}", "email"=>"#{user.email}", "password"=>"#{user.password}"}}) # Сохраняем данные о пользователе
    group.add_developer(user) # Добавляем пользователя в группу как разработчика
    end;
end;
File.write("/tmp/users-creds.yaml", users_creds.to_yaml); # Сохраняем данные о пользователях в файл

В данном случае имена пользователей читаются из файла /tmp/users.txt. В результате выполнения скрипта данные будут выглядеть следующим образом:
---
users:
- Ivan Ivanov:
    login: ivanivanov
    email: ivanivanov@devops-spbstu.ru
    password: <секретный пароль 1>
- John Cane:
    login: johncane
    email: johncane@devops-spbstu.ru
    password: <секретный пароль 2>

36571c34c5040bbdc44410deac19b7f9.png

Конфигурация раннера


Для работы с CI/CD в GitLab необходимо веб-приложение (агент) — GitLab Runner. Оно интерпретирует конфигурационный файл CI/CD и автоматически выполняет описанные задачи.

Прежде чем создавать инфраструктуру под веб-приложение через Terraform, необходимо зарегистрировать новую запись об агенте в GitLab и получить ключ доступа.

GitLab Runner регистрируется для всех пользователей и может быть использован для запуска через него CI/CD конвейеров. В данном случае создаем раннер типа Docker с тегом docker:

# Создаем раннер
runner = Ci::Runner.new(description: 'My Shared Runner', active: true, name: 'my-runner' + SecureRandom.hex(4), token: SecureRandom.hex(20), runner_type: Ci::Runner::runner_types["instance_type"]);
runner.docker_executor_type!;
runner.tag_list = ['docker'];
runner.save!; # Сохраняем раннера
runner_cred = {"#{runner.name}" => {"token" => "#{runner.token}"}};
reg_data = {"url"=>"Gitlab.config.gitlab.url", "token"=>"#{runner.token}"}
# Создаем cloud-init файл с токеном раннера
data = '#cloud-config
timezone: Europe/Moscow
write_files:
- path: "/opt/gomplate/values/user-values.yaml"
  permissions: "0644"
  content: |
'
data += "    gitlabURL: \"#{Gitlab.config.gitlab.url}\"\n"
data += "    token: \"#{runner.token}\"\n"
File.write("/tmp/runner-metadata.cfg", data);
File.write("/tmp/runner-creds.yaml", runner_cred.to_yaml);

И вуаля, раннер зарегистрирован. На выходе получаем файл конфигурации для cloud-init.
#cloud-config
timezone: Europe/Moscow
write_files:
- path: "/opt/gomplate/values/user-values.yaml"
 permissions: "0644"
 content: |
   gitlabURL: "https://gitlab.devops-spbstu.ru"
   token: <супер секретный ключ 1>

А также yaml-файл с данными о ранере: название и ключ доступа.
my-runnere8d7802a:
 token: <супер секретный ключ 1>

Кастуем всю магию за раз


Чтобы выполнить это не ручками, а автоматически, повторим все через Ansible.
- name: Configure Gitlab
  hosts: gitlab
  tasks:
    # Меняем временную зону (исправляем 500 код)
    - name: Changing systems timezone
      community.docker.docker_container_exec:
    container: gitlab
    command: ln -s -f /usr/share/zoneinfo/Europe/Moscow /etc/localtime
    - name: Changing gitlab timezone
      community.docker.docker_container_exec:
    container: gitlab
    command: echo "gitlab_rails['time_zone'] = 'Europe/Moscow'" >> /etc/gitlab/gitlab.rb && gitlab-ctl reconfigure && gitlab-ctl restart
    # Создаем пользователей
    - name: Copy users-list to server
      ansible.builtin.copy:
    src: ./resources/users.txt
    dest: /tmp/users.txt
    - name: Copy users-list to gitlab
      community.docker.docker_container_copy_into:
    container: gitlab
    path: /tmp/users.txt
    container_path: /tmp/users.txt
    #
    - name: Copy users-create script to server
      ansible.builtin.copy:
    src: ./scripts/create_users.rb
    dest: /tmp/users.rb
    - name: Copy users-create script to gitlab
      community.docker.docker_container_copy_into:
    container: gitlab
    path: /tmp/users.rb
    container_path: /tmp/users.rb
    - name: Create users
      community.docker.docker_container_exec:
    container: gitlab
    command: gitlab-rails runner /tmp/users.rb
    # Выгружаем данные о пользователях с сервера
    - name: Load users creds to server
      ansible.builtin.shell: docker cp gitlab:/tmp/users-creds.yaml /tmp/users-creds.yaml
    - name: Load users creds locally
      ansible.builtin.fetch:
    src: /tmp/users-creds.yaml
    dest: ./resources/user-creds.yaml
    flat: true
    # Создаем раннер для группы
    - name: Copy runner-create script to server
      ansible.builtin.copy:
    src: ./scripts/create_runner.rb
    dest: /tmp/runner.rb
    - name: Copy runner-create script to gitlab
      community.docker.docker_container_copy_into:
    container: gitlab
    path: /tmp/runner.rb
    container_path: /tmp/runner.rb
    - name: Create group-runner
      community.docker.docker_container_exec:
    container: gitlab
    command: gitlab-rails runner /tmp/runner.rb
    # Выгружаем данные о раннере с сервера
    - name: Load runner creds from gitlab to server
      ansible.builtin.shell: docker cp gitlab:/tmp/runner-creds.yaml /tmp/runner-creds.yaml
    - name: Load runner creds locally
      ansible.builtin.fetch:
    src: /tmp/runner-creds.yaml
    dest: ./resources/runner-creds.yaml
    flat: true
    # Выгружаем cloud-init конфиг раннера с сервера
    - name: Load runner config from gitlab to server
      ansible.builtin.shell: docker cp gitlab:/tmp/runner-metadata.cfg /tmp/runner-metadata.cfg
    - name: Load runner config locally to terraform
      ansible.builtin.fetch:
    src: /tmp/runner-metadata.cfg
    dest: ../terraform/resources/runner_metadata.cfg
    flat: true
    # Перезагружаем Gitlab
    - name: Restarting gitlab
      community.docker.docker_container_exec:
    container: gitlab
    command: gitlab-ctl restart

После запуска плейбука через Ansible произойдет магия. Учетные записи пользователей созданы, раннер зарегистрирован, осталось его запустить.
cc0933491c7517a2bcf1a7f611abdff3.png

Проверяем, появился ли зарегистрированный раннер в панели администратора. Для этого возвращаемся к Terraform.

Запуск GitLab Runner


Ну что ж, мы на финишной прямой. После всех финтов ушами мы имеем файл конфигурации для раннера. Нам остается лишь установить GitLab Runner.

Рекомендации по установке GitLab Runner советуют ставить GitLab и Runner на отдельных серверах. Поэтому поднимем инфраструктуру для него через Terraform.

Возьмем за основу образ Cloud GitLab Runner 17.5.4 64-bit. Развернем его на базе сервера с 2 CPU, 4 Gb RAM и загрузочным диском (SSD).

Описание через Terraform имеет следующий вид:

# Создание ключевой пары для доступа к ВМ
module "keypair" {
  source         = "../modules/keypair"
  keypair_name       = "ssh_runner_key_ed"
  keypair_public_key = file("${var.ssh_key_file}.pub")
  region         = var.region
}
# Создание приватной сети для ВМ
module "nat" {
  source = "../modules/nat"
}
# Создание GitLab-runner сервера.
module "gitlab_runner_server" {
  source = "../modules/server_gitlab_runner"
  server_name         = "runner"
  server_zone         = var.server_zone
  server_vcpus        = var.runner_vcpus
  server_ram_mb       = var.runner_ram_mb
  server_root_disk_gb     = var.runner_root_disk_gb
  server_boot_volume_type = var.server_volume_type
  server_image_name       = var.runner_image_name
  server_ssh_key      = module.keypair.keypair_name
  region              = var.region
  network_id          = module.nat.network_id
  subnet_id           = module.nat.subnet_id
  user_data           = file(var.runner_user_data_path)
  server_preemptible_tag = var.server_no_preemptible_tag
}

Запустим развертывание через Terraform. Результатом наших действий будет появившийся в панели облачной платформы сервер с GitLab Runner.
5cb36fe5259abc85c66d94dd0aeafcf1.png

Кроме того, состояние нашего раннера должно измениться на активное в панели CI/CD в GItLab.
7eb567c86b6da19329e322c4ddfda604.png

Раннер запустился и опознался готовым к работе в GitLab. Можно радоваться, жизнь удалась, GitLab и GitLab Runner работают!

Тестируем Франкенштейна


Запустим простой пайплайн со сборкой Python-приложения в Docker-образ через kaniko и выгрузкой в Container Registry. Также заодно запустим тесты.

Описание тестового конвейера:

stages:
 - build
 - test
variables:
 IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
build app:
 stage: build
 image:
   name: gcr.io/kaniko-project/executor:v1.23.2-debug
   entrypoint: [""]
 script:
   - /kaniko/executor
     --context "${CI_PROJECT_DIR}"
     --dockerfile "${CI_PROJECT_DIR}/Dockerfile"
     --destination "$IMAGE_TAG"
 tags:
   - docker
unit tests:
 image: python:3.12-alpine
 stage: test
 before_script:
   - python --version
   - pip install pytest
   - ls
   - ls tests
 script:
   - python -m pytest tests/unit
 needs: [ build app ]
 tags:
   - docker

Структура проекта содержит библиотеку (lib) и тесты на pytest (tests/unit).
8f031126245a95ffdec17fac07973e0d.png

Запускаем пайплайн и видим, что он успешно завершился.
0f307836b4c0d12e405af8282540b1d6.png

Проверяем работу Container Registry и видим, что образ приложения появился в проекте.
c616a42e1226382c7487db892240d1e9.png

Какие были трудности


А теперь небольшой «нытинг». Для меня задача по развертыванию GitLab оказалась новой и была неким вызовом, чтобы попробовать свои силы в области, отличной от моей основной деятельности.

Корявые временные настройки


При запуске образа были проблемы с рассинхронизацией времени сервера и контейнера с GitLab. При любой аутентификации вылетал код ошибки 500 — и живи с этим, как хочешь. Лишь после настройки через Ansible времени у GitLab получилось с этим справиться.

Отсутствие документации GitLab Rails


GitLab Rails открылся для меня очень удобным инструментом для конфигурации каждого из компонент GitLab. Однако каким бы он ни был удобным и крутым, я для него не смог найти подробной документации, описывающей все классы и модули. Справиться с выявлением нужных модулей, классов и методов помог «О, Великий гуглинг».

Выводы


На этом этапе, можно сказать, что работа по развертыванию GitLab и GitLab Runner была выполнена. Задача была интересной и, надеюсь, ее результат пригодится кому-нибудь, кто будет жалеть свое время, развертывая инфраструктуру ручками.

Проект не идеален. В будущем хотелось бы улучшить создание и связь публичного IP-адреса с доменом, а также масштабирование количества раннеров.

Всем спасибо!

© Habrahabr.ru