Як безболісно додавати, використовувати та оновлювати .terraform.lock.hcl

💡 Усі статті, обговорення, новини про DevOps — в одному місці. Приєднуйтесь до DevOps спільноти!

Дисклеймер від редакції: хоча оновлення Terraform 1.4.0 вийшло вже деякий час тому, ця стаття з певних причин залишалася неопублікованою. Ми вважаємо, що її зміст заслуговує на увагу, навіть із запізненням. Сподіваємося, вам буде цікаво — приємного читання!

Передісторія

Як ви знаєте, у Terraform 1.4.0 запроваджено зміни, які ламають попередню неправильну поведінку. Раніше ви могли ігнорувати лок-файл та використовувати закешовані провайдери так довго, як version constraints в коді були сумісними з ваши локальним кешем. Починаючи з 1.4.0, Terraform дивиться в лок-файл перед тим, як подивитись що лежить в кеші. На практиці це означає, що якщо ви проігноруєте лок-файл або повністю видалите його, Terraform запустить повну ініціалізацію, незалежно від того, що знаходиться у ваших TF_CACHE_DIR та .terraform/.

Отже, є кілька варіантів, щоб вирішити це:

  • Продовжити використовувати Terraform 1.3.x як новий 0.11
  • Додати TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE=true
  • Почати використовувати лок-файл та рухатись далі

Коли зник останній промінь надії на те, що наш чудовий робочий процес із **/.terraform.lock.hcl у .gitignore не буде поламаний взад, я вирішив спробувати додати .terraform.lock.hcl до всіх наших 289 рут модулів. Ви можете запитати:

Навіщо потрібні ці лок-файли

Що ж, окрім «дуже рекомендованого Hashicorp’ом рішення», який фактично змушує вас використовувати лок-файл, існують додаткові причин, чому ви хотіли б їх використовувати — повторюваність і безпека.

Повторюваність

Уявимо що у вас є провайдер aws або kubernetes і ви вірите, що мейнтейнери використовують SemVer правильно. Тож ви вказуєте:

terraform {
  required_version = "~> 1.3"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "~> 2.0"
    }
  }
}

Все працює добре, допоки...

  • Провайдер був зламаний протягом 3 робочих днів через відсутність тестування ¯\_(ツ)_/¯

    3 дні несправного провайдера Kubernetes

  • Мейнтейнери провайдера додають breaking changes в мінорний реліз, оскільки вони забули додати їх в мажорний

    AWS  breaking changes в мінорній версії

І обидві ці ситуації виникли протягом одного місяця.

Звичайно, ви можете пінити версії провайдерів включно з патч-версіями. Наприклад, «5.0.0» або «2.19.0» і використовувати Renovate/dependabot або tfupdate pre-commit хук для контрольованих оновлень. З плюсів, з tfupdate ви зафорсите використання лише однієї версії terraform/провайдера/модуля в усьому коді.

Таким чином ви уникнете проблем, описаних вище. Однак це ще не все.

Безпека

Всередині .terraform.lock.hcl є всі ці h1 і zh хеші:

provider "registry.terraform.io/hashicorp/kubernetes" {
  version = "2.21.1"
  constraints = ">= 2.21.1, < 3.0.0"
  hashes = [
    "h1:2spGoBcGDQ/Csc23bddCfM21zyKx3PONoiqRgmuChnM=",
    "h1:7cCH+Wsg2lFTpsTleJ7MewkrYfFlxU1l4HlLWP8wzFw=",
    "h1:I1qWLUFmB0Z8+3CX+XzJtkgiAOYQ1nHlLN9lFcPf+zc=",
    "h1:gP8IU3gFfXYRfGZr5Qws9JryZsOGsluAVpiAoZW7eo0=",
    "zh:156a437d7edd6813e9cb7bdff16ebce28cec08b07ba1b0f5e9cec029a217bc27",
    "zh:1a21c255d8099e303560e252579c54e99b5f24f2efde772c7e39502c62472605",
    "zh:27b2021f86e5eaf6b9ee7c77d7a9e32bc496e59dd0808fb15a5687879736acf6",
    "zh:31fa284c1c873a85c3b5cfc26cf7e7214d27b3b8ba7ea5134ab7d53800894c42",
    "zh:4be9cc1654e994229c0d598f4e07487fc8b513337de9719d79b45ce07fc4e123",
    "zh:5f684ed161f54213a1414ac71b3971a527c3a6bfbaaf687a7c8cc39dcd68c512",
    "zh:6d58f1832665c256afb68110c99c8112926406ae0b64dd5f250c2954fc26928e",
    "zh:9dadfa4a019d1e90decb1fab14278ee2dbefd42e8f58fe7fa567a9bf51b01e0e",
    "zh:a68ce7208a1ef4502528efb8ce9f774db56c421dcaccd3eb10ae68f1324a6963",
    "zh:acdd5b45a7e80bc9d254ad0c2f9cb4715104117425f0d22409685909a790a6dd",
    "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
    "zh:fb451e882118fe92e1cb2e60ac2d77592f5f7282b3608b878b5bdc38bbe4fd5b",
  ]
}

Terraform використовує їх для завантаження ідентичних артефактів для вашої платформи, які використовувалися під час останньої terraform init і, як наслідок, terraform apply команди.

Це зменшує ймовірність supply chain attack, коли найслабшою ланкою у вашому ланцюжку залежностей є terraform провайдери.

Підготовка до додавання лок-файлів

Примітка: у всіх наведених нижче прикладах я використовував GitHub Workflows. Однак ви можете портувати їх на будь-який інший CI.

Перш за все, вам потрібно мати валідний terraform код.

І ви не можете просто взяти і пропустити цей крок, якщо у вас багато terraform-коду: майже напевно щось десь буде зламано.

Отже, дозвольте представити вам команду terraform validate! Жартую. Але це саме те, що нам треба. Також для валідація буває потрібно виконати terraform init -backend=false, який потрібно запустити у всіх рут-модулях.

Для цього випадку існує рішення на базі pre-commit, яке інітить ваші модулі (і виправляє існуючі .terraform, якщо вони застарілі/зламані), і запускає валідацію. Для його використання необхідно:

1. Встановити залежності будь-яким способом, описаним у pre-commit-terraform.

2. Створити файл .github/.pre-commit-tf-lockfiles.yaml із наведеним нижче вмістом:

Примітка: пізніше ми будемо використовувати цей файл для автоматичного оновлення лок-файлів в CI. .github/ присутній у шляху до файлу лише для того, щоб приховати його від звичайних користувачів і покласти файл якомога ближче до .github/workflows/
repos:
  - repo: https://github.com/antonbabenko/pre-commit-terraform
    rev: v1.81.0
    hooks:
      - id: terraform_validate
        args:
          - --hook-config=--retry-once-with-cleanup=true
          - --tf-init-args=-upgrade
        # files: '^path/to/your/terraform/root/folder/[a-c]'
        exclude: '(\.)?modules/'
      # - id: terraform_providers_lock
      #   args:
      #   - --hook-config=--mode=always-regenerate-lockfile
      #   - --args=-platform=linux_arm64
      #   - --args=-platform=linux_amd64
      #   - --args=-platform=darwin_amd64
      #   - --args=-platform=darwin_arm64
      #   files: '^path/to/your/terraform/root/folder/[a-c]'
      #   exclude: '(\.)?modules/'

3. Якщо у вас величезний репник, розкоментуйте наступний рядок і вкажіть правильний шлях # files: '^path/to/your/terraform/root/folder/[a-c]'

files і exclude використовує Python re.search синтаксис (docs). Вказавши [a-c] в кінці, ми можемо обмежити кількість директорій, які повинні бути пройдені протягом наступного запуску

4. Запустіть команду та відпочиньте пару хвилин

pre-commit run -a --config .github/.pre-commit-tf-lockfiles.yaml

5. Після завершення команди перевірте щоб всі рут-модулі пройшли валідацію. Якщо ні, виправте помилки та повторно запустіть pre-commit, доки всі модуля не будуть валідними.

6. Відредагуйте свій .gitignore, щоб не ігнорувати файли блокування. Наприклад:

!path/to/your/terraform/root/folder/[a-c]*/.terraform.lock.hcl
!path/to/your/terraform/root/folder/[a-c]*/**/.terraform.lock.hcl

Додайте лок-файли

1. Перейдіть до раніше створеного .github/.pre-commit-tf-lockfiles.yaml і:

    • Розкоментуйте terraform_providers_lock хук
    • Задайте ваші -platform=’и
    • Скопіюйте files і exclude секції з terraform_validate до terraform_providers_lock
    • Закоментуйте terraform_validate хук аби заощадити трохи часу

2. Запустіть команду нижче. Ця займе набагато більше часу ніж перша команда, тож ви можете паралельно робити щось інше: pre-commit run -a --config .github/.pre-commit-tf-lockfiles.yaml

3. У моїх тестах це зайняло близько 2,5 с на платформу на провайдера на рут-модуль. Тож, щоб створити лок-файл для модуля з 6 провайдерами і з 4 платформами, вам знадобитися приблизно 1 хвилина.

4. Переконайтеся, що всі лок-файли мають zh хеші для кожного провайдера.
Не забудьте видалити порожні файли, створені в розділі «Підготовка» для директорій без terraform коду.
Якщо деякі лок-файли не мають усіх необхідних хешів, перевірте логи. У більшості випадків це означає, що ви все ще використовуєте щось старе із Terraform 0.11, яке не підтримує одну з указаних платформ (у моєму випадку -platform=darwin_arm64 для hashicorp/template і mumoshu/helmfile)

5. Якщо ви також зіткнулися з цими проблемами, змініть .github/.pre-commit-tf-lockfiles.yaml і перезапустіть pre-commit:

- id: terraform_providers_lock
    args:
      - --hook-config=--mode=always-regenerate-lockfile
      - --args=-platform=linux_arm64
      - --args=-platform=linux_amd64
      - --args=-platform=darwin_amd64
      - --args=-platform=darwin_arm64
    exclude: |
      (?x)
        (/(\.)?modules/
        # hashicorp/template 2.2.0 is not available for darwin_arm64
        |^terraform/bootstrap/
        # mumoshu/helmfile 0.14.0 is not available for darwin_arm64.
        |^terraform/helmfiles/
      )
  # TODO: Rewrite these modules to newer providers
  - id: terraform_providers_lock
    name: Lock terraform provider versions w/o darwin_arm64
    args:
      - --hook-config=--mode=always-regenerate-lockfile
      - --args=-platform=linux_arm64
      - --args=-platform=linux_amd64
      - --args=-platform=darwin_amd64
    files: |
      (?x)
        # hashicorp/template 2.2.0 is not available for darwin_arm64
        (^terraform/bootstrap/
        # mumoshu/helmfile 0.14.0 is not available for darwin_arm64.
        |^terraform/helmfiles/
      )
Примітка: щоб заощадити час, ви можете закоментувати останній хук для генерації наступних групи лок-файлів.

Автоматизація оновлення лок-файлів у CI

Як ви матимете всі лок-файли, настане час автоматизувати їх оновлення.

1. Перейдіть до .github/.pre-commit-tf-lockfiles.yaml і:

    • Змініть terraform_validate files секції на:
    • files: '\.terraform\.lock\.hcl$'
    • щоб обмежити запуск terraform init лише для директорій з лок-файлами.
    • Видаліть files у terraform_providers_lock хуках

У підсумку ви отримаєте щось на зразок цього:

repos:
  - repo: https://github.com/antonbabenko/pre-commit-terraform
    rev: v1.81.0
    hooks:
      - id: terraform_validate
        args:
          - --hook-config=--retry-once-with-cleanup=true
          - --tf-init-args=-upgrade
        files: '\.terraform\.lock\.hcl$'
    - id: terraform_providers_lock
        args:
          - --hook-config=--mode=always-regenerate-lockfile
          - --args=-platform=linux_arm64
          - --args=-platform=linux_amd64
          - --args=-platform=darwin_amd64
          - --args=-platform=darwin_arm64
        exclude: |
          (?x)
            (/(\.)?modules/
            # hashicorp/template 2.2.0 is not available for darwin_arm64
            |^terraform/bootstrap/
            # mumoshu/helmfile 0.14.0 is not available for darwin_arm64.
            |^terraform/helmfiles/
          )
      # TODO: Rewrite these modules to newer providers
      - id: terraform_providers_lock
        name: Lock terraform provider versions w/o darwin_arm64
        args:
          - --hook-config=--mode=always-regenerate-lockfile
          - --args=-platform=linux_arm64
          - --args=-platform=linux_amd64
          - --args=-platform=darwin_amd64
        files: |
          (?x)
            # hashicorp/template 2.2.0 is not available for darwin_arm64
            (^terraform/bootstrap/
            # mumoshu/helmfile 0.14.0 is not available for darwin_arm64.
            |^terraform/helmfiles/
          )

2. Додайте GitHub workflow, який встановлює всі залежності та запускає pre-commit run щопонеділка і створюватиме новий PR в Renovate-стилі:

name: Maintain Terraform lockfile up-to-date
# It is required at least Renovate fixes https://github.com/renovatebot/renovate/issues/22417
on:
  workflow_dispatch: {}
  schedule:
    - cron: '0 4 * * 1' # Execute every Monday at 04:00
permissions:
  contents: write
  pull-requests: write
env:
  # Prevent GH API rate-limit issue
  GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
jobs:
  pre-commit-tf-lockfile:
    runs-on: ubuntu-latest
    container: python:3.11-slim
    steps:
    - name: Install container pre-requirements
      run: |
        apt update
        apt install -y \
            git \
            curl \
            unzip \
            jq \
            nodejs # Needed for Terraform installation
        curl -L https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 > /usr/bin/yq &&\
          chmod +x /usr/bin/yq
    - name: Checkout
      uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
      with:
        ref: ${{ github.base_ref }}
    - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
    - run: |
        git config --global --add safe.directory /__w/infrastructure/infrastructure
        git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/*
    - uses: hashicorp/setup-terraform@633666f66e0061ca3b725c73b2ec20cd13a8fdd1 # v2.0.3
      with:
        terraform_version: ~1.3
    - name: Execute pre-commit
      uses: pre-commit/action@646c83fcd040023954eafda54b4db0192ce70507 # v3.0.0
      with:
        extra_args: >
          --all-files
          --config .github/.pre-commit-tf-lockfiles.yaml
          --color=always --show-diff-on-failure
    - name: Create Pull Request
      if: failure()
      id: cpr
      uses: peter-evans/create-pull-request@284f54f989303d2699d373481a0cfa13ad5a6666 # v5.0.1
      with:
        commit-message: 'chore(deps): Update terraform lockfiles'
        branch: pre-commit/update-tf-lockfiles
        delete-branch: true
        title: 'chore(deps): Update terraform lockfiles'
        body: >
          This PR update provider versions in Terraform lockfiles to their most resent values
          > **Warning**: Before merge, please, make sure that all Terraform CI runs pass successfully.
        labels: auto-update
        branch-suffix: timestamp
    - name: Pull Request number and link
      if: failure() && steps.cpr.outputs.pull-request-number > 0
      run: |
        echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
        echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"

Autoоновлення PR для лок-файлів

Для 289 рут-модулів із 1180 секціями провайдерів в лок-файлах, це займає 2 години 40 хвилин або ~2,288 с на платформу на провайдера, що приблизно на 0,2 с швидше, ніж локально.

І це все з технічної точки зору. Настав час відповісти на запитання: чому б тобі просто не використати для цього Renovate, чуваче?

Чому не Renovate?

Так, я чув і про Renovate і про dependabot. Напр, я розказував про Renovate на HUG Kyiv і на стрімі Антона Бабенка(Eng).

Ми не використовуємо dependabot для інфра репи, оскільки він має забагато проблем із монорепами; ви не можете просто форсувати використання dependabot.yml для всієї організації; він менш конфігурабельний, ніж Renovate тощо.

Сам GitHub неправильно використовує dependabot у своїх репозиторіях...
  • Щойно створений репозиторій із actions/typescript-action містить
    Нещодавно створений репозиторій dependabot PR
  • Dependabot PR живе тижнями!
    Як довго може прожити dependabot PR без уваги?

Я підтримую Renovate для своєї організації тут: Sharable Config Presets for Renovatebot, especially useful for DevOps folks. Крім того, Renovate має опцію lockFileMaintenance, але...

  • Наразі Renovate не сприймає != version constrain (renovate/#22417), тому він просто не може створити PR, якщо принаймні один version constrain з != потрапить в «список оновлення».
  • Якщо у вас немає !=, Renovate створить красиві PRи, але не врахує обмеження провайдерів вказані всередині child-модулів використані у вашому рут-модулі.

Таким чином, ви отримаєте щось на кшталт:

provider "registry.terraform.io/hashicorp/aws" {
version     = "5.2.0"
constraints = "~> 5.0"

У випадках, коли команда terraform providers lock створить щось на кшталт

provider "registry.terraform.io/hashicorp/aws" {
version     = "5.2.0"
constraints = ">= 2.0.0, >= 3.0.0, >= 3.64.0, >= 4.0.0, >= 4.9.0, >= 4.18.0, >= 4.22.0, >= 4.23.0, >= 4.49.0, ~> 5.0"

І воно працюватиме добре, доки хтось всередині цих модулів не вкаже != 5.2.0 або < 5.2.0.

Renovate визначає всі доступні h1 хеші (усі доступні платформи провайдерів), що чудово. Тим не менш, він не вказує «базові» zh хеші, які, на мою думку, є більш суворими. Тому я віддаю перевагу наявності zh хешів, коли це можливо.

І ще одне:

Додавайте лок-файли до всіх нових рут-модулів

Просто введіть правило для ваших тераформ-конфігурацій:

Виконайте terraform init, при додаванні нового рут-модуля.

Це додасть додає базовий .terraform.lock.hcl, який ви можете закомітити як є та почекати наступної CI-джоби з оновлення лок-файлів.

Або, ви можете додати .pre-commit-config.yaml з:

repos:
  - repo: https://github.com/antonbabenko/pre-commit-terraform
    rev: v1.81.0
    hooks:
      # Validate and run `terraform init` which needed for terraform_providers_lock
      - id: terraform_validate
        args:
          - --hook-config=--retry-once-with-cleanup=true
      - id: terraform_providers_lock
        args:
          - --hook-config=--mode=only-check-is-current-lockfile-cross-platform
          - --args=-platform=linux_arm64
          - --args=-platform=linux_amd64
          - --args=-platform=darwin_amd64
          - --args=-platform=darwin_arm64
        ## TODO: Rewrite these modules to newer providers
        # exclude: |
        #   (?x)
        #     (/(\.)?modules/
        #     # hashicorp/template 2.2.0 is not available for darwin_arm64
        #     |^terraform/bootstrap/
        #     # mumoshu/helmfile 0.14.0 is not available for darwin_arm64.
        #     |^terraform/helmfiles/
        #   )

І автоматизувати запуск pre-commit в PR-ах, як тут або тут.

Висновки

Ось кілька висновків:

  • Краще мати лок-файли, ніж не мати (повторюваність, безпека)
  • Має сенс автоматизувати всі ці оновлення та підтримувати однакові версії по всьому коду, щоб мати останні стабільні версії без пірових перемог (Renovate, dependabot, tfupdate via pre-commit)
  • Для кращого UX, вам потрібний source of truth (автоматизований terraform plan в CI, Terratest’и, тощо), яке показуватиме, що зміни нічого не поламали. Це може бути Atlantis, Spacelift, Terraform Cloud, або ви можете зробити це у своєму власному CI
👍ПодобаєтьсяСподобалось4
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Як на мене, краще не мати лок-файлів, і вказувати в модулях єдину версію терраформ.

Якщо-ж використовується кілька різних модулів — це можна розрулити за допомогою terragrunt та розділення проекту на частини.

Бо якщо кожен модуль буде зі своєю версією, рано чи пізно це завершиться тим, що десь буде не сумісність. І розплутувати це клубок буде вкрай важко. Причому це не залежить від мови — я маю на увазі, звичайний, не IaC код.

Ну і якщо вже казати про безпеку — краще переходити на нові версії провайдерів, ніж зберігати залежність від старих. І рано чи пізно це все одно прийдеться робити, тільки чим більше відтягуємо, тим складнішим буде цей процес.

Працювати з локами чи ні це ще можна пережити, але не пінити версію модуля — дуже небезпечно. (для прикладу — прям вже відчувається, що aws збирається дропнути eks aws_auth і залишити лише AccessEntry. Ну і звісно aws auth з eks модуля пропаде. А рішення це оновлювати версії модулів. І це частина роботи, тому без цього ніяк. Так само як і девам треба версії депенденсі оновлювати

Як на мене, краще не мати лок-файлів

Дуже невдалий жарт.

краще переходити на нові версії провайдерів, ніж зберігати залежність від старих

Як зібрались трекать версії й тестувати проблеми сумісності без версіонованих локфайлів ?...

На semver може бути декілька git хешів...

terraform {
required_providers {
mycloud = {
source = “mycorp/mycloud”
version = “1.0.0”
}
}
}

There are two ways for you to manage provider versions in your configuration.

— Specify provider version constraints in your configuration’s terraform block.
— Use the dependency lock file

чим вас такий варіант не задовольняє?

На semver може бути декілька git хешів...

якщо хтось змінює поведінку провайдера не оновлюючи жодного компоненту версії — ну, вибачте, я таке в прод юзати не буду. краще вже своє накостилити, якось більше передбачувано та підконтрольно.

так то можуть і дропнути ту версію з серверу, і наявність хешу вас не дуже врятує.

Підписатись на коментарі