Инструкция описывает, как настроить хранение и блокировку Terraform‑состояния (state) в GitLab, если Terraform запускается из GitLab CI, а не локально.
Terraform хранит текущее состояние инфраструктуры в terraform.tfstate. При каждом plan/apply Terraform:
Читает состояние.
Рассчитывает изменения.
Pаписывает обновленное состояние.
Если два пайплайна или два job’а одновременно работают с одним и тем же state, возможны гонки и порча инфраструктуры. Locking гарантирует, что в один момент времени только один процесс изменяет state, а остальные ждут или падают с ошибкой блокировки.
GitLab умеет выступать как удаленный backend для Terraform и обеспечивать централизованное хранение и блокировку state.
Terraform в GitLab CI использует HTTP-backend, подключенный к GitLab API:
projects/{PROJECT_ID}/terraform/state/{STATE_NAME} └── /lock — блокировка state
Последовательность при terraform plan/apply:
Terraform запрашивает lock (HTTP POST на /lock).
Если lock получен — читает/пишет state.
По завершении снимает lock (HTTP DELETE на /lock). Результат: только один job изменяет state одновременно.
GitLab хранит данные в своем хранилище (БД или object storage), но вы работаете только с HTTP‑API.
Project ID указан в разделе Проект → Settings → General → Project ID (внизу страницы). Вы можете использовать тот же проект, где хранится Terraform-код.
STATE_NAME — это произвольный идентификатор state. Примеры:
dev, stage, prod
network-dev, k8s-prod и т.п.
Формат: латинские буквы, цифры, -, _, (без /).
Вы можете использовать отдельный state на каждый environment/branch, например:
STATE_NAME = "prod"
STATE_NAME = "dev"
или STATE_NAME = "$CI_COMMIT_REF_SLUG" (по ветке).
Создайте (или отредактируйте) файл backend.tf в корне Terraform‑проекта.
Пример (GitLab.com, один state prod):
terraform {backend "http" {address = "https://gitlab.com/api/v4/projects/1234567/terraform/state/prod"lock_address = "https://gitlab.com/api/v4/projects/1234567/terraform/state/prod/lock"unlock_address = "https://gitlab.com/api/v4/projects/1234567/terraform/state/prod/lock"lock_method = "POST"unlock_method = "DELETE"# время ожидания между попытками взять lockretry_wait_min = 5}}
Если GitLab self‑hosted, замените https://gitlab.com на ваш домен, например:
address = "https://gitlab.example.com/api/v4/projects/1234567/terraform/state/prod"
Важно: Не храните в backend‑блоке чувствительные данные (логин/токен). Настройте аутентификацию через переменные окружения в GitLab CI.
Для HTTP‑backend Terraform может использовать следующие переменные окружения:
TF_HTTP_USERNAME
TF_HTTP_PASSWORD
Рекомендованный паттерн для GitLab CI:
TF_HTTP_USERNAME = "gitlab-ci-token"
TF_HTTP_PASSWORD = "$CI_JOB_TOKEN"
GitLab проверяет пару gitlab-ci-token + CI_JOB_TOKEN и разрешает доступ к state этого проекта. Эти переменные необходимо задать в .gitlab-ci.yml в разделе variables.
Ниже минимальный пример пайплайна с тремя стадиями:
validate — проверка синтаксиса;
plan — расчет плана;
apply — применение (ручное, чтобы не применять автоматически каждый push).
stages:- validate- plan- apply# Общие переменныеvariables:TF_ROOT: "." # путь к Terraform-каталогуTF_HTTP_USERNAME: "gitlab-ci-token"TF_HTTP_PASSWORD: "$CI_JOB_TOKEN"TF_IN_AUTOMATION: "true"TF_INPUT: "false"TF_PLAN_FILE: "tfplan.out"terraform:validate:stage: validateimage: hashicorp/terraform:1.9.0script:- cd "$TF_ROOT"- terraform init -input=false- terraform validateonly:- merge_requests- main- masterterraform:plan:stage: planimage: hashicorp/terraform:1.9.0script:- cd "$TF_ROOT"- terraform init -input=false- terraform plan -out="$TF_PLAN_FILE"artifacts:paths:- "$TF_ROOT/$TF_PLAN_FILE"expire_in: 1 dayonly:- merge_requests- main- masterterraform:apply:stage: applyimage: hashicorp/terraform:1.9.0needs:- job: terraform:planartifacts: truescript:- cd "$TF_ROOT"- terraform init -input=false- terraform apply -input=false "$TF_PLAN_FILE"only:- main- masterwhen: manual # применение только вручнуюallow_failure: false
Как работает блокировка:
Каждый job выполняет terraform init и подключается к GitLab state.
Terraform запрашивает lock (HTTP POST на /lock).
Если lock получен — читает/пишет state.
По завершении снимает lock (HTTP DELETE на /lock). Результат: только один job изменяет state одновременно.
Способы разделения:
Отдельный state для каждого окружения — dev, stage, prod.
State по ветке.
Рассмотрим варианты реализации.
Например:
envs/dev/backend.tf → …/state/dev
envs/prod/backend.tf → …/state/prod
В .gitlab-ci.yml необходимо указать TF_ROOT в зависимости от job’а или pipeline‑переменной.
Более продвинутый вариант — формировать STATE_NAME динамически, но стандартный backend "http" не подставляет переменные окружения в address. Для простоты лучше явно указать один state (dev или prod).
Для проверки, что GitLab действительно блокирует state:
Подготовка: a. backend "http" настроен на GitLab. b. Пайплайн успешно прошел хотя бы один раз (terraform init выполнился, state создан).
Тест параллельности: a. Запустите одно и то же terraform:apply два раза почти одновременно. Например, дождитесь, пока первый apply начнет выполняться (и будет ждать подтверждения или долго применять ресурсы). b. Запустите второй terraform:apply job вручную.
Ожидаемый результат: a. Первый job получит lock и выполнится. b. Второй job:
либо будет ожидать (в соответствии с retry_wait_min),
либо упадет с ошибкой Error acquiring the state lock.
Так вы убедитесь, что GitLab‑backend реально предотвращает одновременную запись в один state.
В примере выше использовался CI_JOB_TOKEN — он автоматически создается GitLab для каждого job’а и имеет ограниченные права.
Если по каким‑то причинам вы хотите использовать PAT:
Создайте PAT с минимально возможными правами;
Добавьте его в CI/CD Variables проекта/группы (например, TF_GITLAB_PAT, protected + masked).
Используйте в .gitlab-ci.yml:
variables:TF_HTTP_USERNAME: "oauth2"TF_HTTP_PASSWORD: "$TF_GITLAB_PAT"
Храните токены только в CI/CD Variables, не в репозитории.