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

от автора

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

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

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

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


В процессе работы необходимо:

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

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

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

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

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

Поднимаем GitLab


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

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

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

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

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

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

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

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

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

#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: "<domain_name>"     gitlabRootEmail: "<email>"     gitlabRootPassword: " "     gitlabPostgresDB: "gitlab"     gitlabPostgresUser: "gitlab"     gitlabPostgresPassword: "gitlab"     useExternalDB: false 

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

Все действия выше можно описать с использованием 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 можно посмотреть в репозитории.

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

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

У нас теперь есть свой 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> 

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

Для работы с 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 произойдет магия. Учетные записи пользователей созданы, раннер зарегистрирован, осталось его запустить.

Проверяем, появился ли зарегистрированный раннер в панели администратора. Для этого возвращаемся к 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.

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

Раннер запустился и опознался готовым к работе в 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).

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

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

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

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

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

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

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

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

Выводы


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

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/890196/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *