Інфраструктура як код і доставка
Навіщо це
Section titled “Навіщо це”Ви створили в консолі мережу, кластер і три бакети. За місяць колега додав правило файрвола «тимчасово», ви змінили розмір диска, а хтось третій вимкнув логування, бо воно коштувало грошей. Тепер потрібне те саме середовище для тестів, і ніхто не може сказати, з чого воно складається.
Інфраструктура як код (infrastructure as code, IaC) розв’язує цю ситуацію так само, як система контролю версій розв’язала її для програм: опис середовища лежить у файлах, змінюється через pull request, перевіряється перед застосуванням і відтворюється командою. Друга половина модуля про те, як ці зміни доходять до хмари: хто має право їх застосовувати і як випустити нову версію застосунку, не зупинивши сервіс.
Передумови. Акаунти й ієрархія ресурсів (модуль 2), ролі й федерація (модуль 3), мережа й обчислення (модуль 4, модуль 5), контейнери (модуль 8). Про контейнери й образи в основі є модуль 17 курсу ОС.
Декларативний підхід
Section titled “Декларативний підхід”Скрипт з викликами aws ec2 run-instances описує дії: створити, потім
приєднати, потім увімкнути. Запустіть його вдруге, і він створить дублікат
або впаде на помилці «вже існує».
Декларативний опис фіксує результат: «має існувати мережа з такими параметрами». Інструмент сам обчислює, що треба створити, змінити чи видалити, щоб реальність збіглася з описом. Повторний запуск нічого не змінює, якщо збігається вже й так. Ця властивість називається ідемпотентністю (idempotency), і вона потрібна, щоб застосовувати опис автоматично, без людини.
Головний інструмент курсу, Terraform, працює саме так. Мова конфігурації
називається HCL. Термін «провайдер» тут має два значення, тож розрізняйте їх
за контекстом: хмарний провайдер (AWS, Azure, Google Cloud) і Terraform-провайдер,
плагін, що вміє викликати API конкретної платформи. Ресурси описані в коді,
плагіни завантажуються командою terraform init, а версії фіксуються
у файлі .terraform.lock.hcl, який слід зберігати в репозиторії.
terraform { required_version = ">= 1.11"
required_providers { aws = { source = "hashicorp/aws" version = "~> 6.0" } azurerm = { source = "hashicorp/azurerm" version = "~> 5.0" } google = { source = "hashicorp/google" version = "~> 8.0" } }}Один корінь може використовувати всі три плагіни одночасно, але в реальних
проєктах так роблять рідко: кожен провайдер зазвичай отримує окремий корінь
зі своїм станом. Зверніть увагу на мажорні версії: azurerm 5.x і google
8.x вийшли вже після більшості публікацій в інтернеті, тож приклади
з чужих статей часто містять атрибути, яких у поточній версії немає.
Наприклад, у поточній документації azurerm_storage_container описано лише
з storage_account_id, а не з storage_account_name. terraform validate ловить такі помилки
до того, як вони коштують грошей.
Стан: що Terraform знає про світ
Section titled “Стан: що Terraform знає про світ”Файл конфігурації описує бажане. Але щоб знайти різницю з реальністю,
Terraform має пам’ятати, які саме об’єкти хмари відповідають якому ресурсу
з коду. Ця пам’ять називається станом (state). Стан зіставляє адресу ресурсу
в коді (aws_s3_bucket.reports) з ідентифікатором у хмарі та зберігає
останні відомі значення його атрибутів.
З цього випливає кілька практичних наслідків.
Стан містить секрети. Пароль бази, який ви передали ресурсу, лежить у стані відкритим текстом. Тому стан зберігають у сховищі з шифруванням і жорстким доступом, а не в git. Нові версії Terraform дають ефемерні значення й атрибути лише для запису (write-only), які в стан не потрапляють; корисно користуватися ними для секретів, де провайдер це підтримує.
Втрата стану означає втрату зв’язку з реальністю. Ресурси в хмарі лишаються, але Terraform про них «забув» і при наступному запуску спробує створити дублікати. Тому бакет зі станом вмикає версіонування, щоб можна було повернути попередню версію файлу.
Два одночасні apply псують стан. Якщо двоє запускають зміни, кожен читає однакову початкову версію, а записує свою, і одна зміна затирає іншу. Захищає від цього блокування (locking): перед змінами Terraform отримує ексклюзивне право на стан, а другий запуск чекає або завершується помилкою.
Віддалене зберігання стану
Section titled “Віддалене зберігання стану”За замовчуванням стан лежить у локальному файлі terraform.tfstate. Для
команди це не працює: файл є лише в одного. Тому стан переносять у віддалене
сховище (backend). Кожна хмара має природне місце для нього:
об’єктне сховище, яке і так є в акаунті. Спершу сховище треба створити,
і це єдина частина, яку зазвичай створюють вручну або окремим невеликим коренем.
variable "state_bucket_name" { type = string}
resource "aws_s3_bucket" "state" { bucket = var.state_bucket_name}
resource "aws_s3_bucket_versioning" "state" { bucket = aws_s3_bucket.state.id
versioning_configuration { status = "Enabled" }}
resource "aws_s3_bucket_public_access_block" "state" { bucket = aws_s3_bucket.state.id
block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true}variable "storage_account_name" { type = string}
resource "azurerm_resource_group" "state" { name = "rg-tfstate" location = "northeurope"}
resource "azurerm_storage_account" "state" { name = var.storage_account_name resource_group_name = azurerm_resource_group.state.name location = azurerm_resource_group.state.location account_tier = "Standard" account_replication_type = "ZRS" allow_nested_items_to_be_public = false
blob_properties { versioning_enabled = true }}
resource "azurerm_storage_container" "state" { name = "tfstate" storage_account_id = azurerm_storage_account.state.id container_access_type = "private"}variable "state_bucket_name" { type = string}
resource "google_storage_bucket" "state" { name = var.state_bucket_name location = "europe-west1" uniform_bucket_level_access = true public_access_prevention = "enforced"
versioning { enabled = true }}Після цього в основному корені додається блок backend, і terraform init
пропонує перенести наявний локальний стан у нове сховище.
terraform { backend "s3" { bucket = "acme-tfstate-prod" key = "network/terraform.tfstate" region = "eu-central-1" encrypt = true use_lockfile = true }}terraform { backend "azurerm" { resource_group_name = "rg-tfstate" storage_account_name = "acmetfstateprod" container_name = "tfstate" key = "network.tfstate" use_azuread_auth = true }}terraform { backend "gcs" { bucket = "acme-tfstate-prod" prefix = "network" }}Блокування влаштоване в кожного сховища по-своєму:
- S3. Раніше для блокування потрібна була ще й таблиця DynamoDB. Тепер
use_lockfile = trueзмушує Terraform створювати поряд зі станом файл.tflockумовним записом: S3 приймає запис лише якщо об’єкта ще немає, тож другий запуск отримує відмову. Підтримка з’явилася в Terraform 1.10 як експериментальна і стала стабільною в 1.11; аргументdynamodb_tableпозначено застарілим. Обліковим даним потрібні права на читання, запис і видалення об’єкта.tflock, а не лише самого стану. При міграції спершу вмикають обидва механізми, потім прибирають DynamoDB. - Azure Blob Storage. Terraform бере оренду (lease) на blob зі станом:
поки оренда в одного запуску, другий не запишеться. Окремої таблиці
чи файла не потрібно. Автентифікація
use_azuread_authозначає доступ за ролями Entra ID, без ключа облікового запису сховища. - Google Cloud Storage. Блокування вбудоване, окремих аргументів немає.
Так само вмикають версіонування об’єктів і за потреби ключ Cloud KMS
(
kms_encryption_key).
Plan і apply
Section titled “Plan і apply”Робочий цикл складається з невеликого набору команд:
terraform init # завантажити плагіни, підключити сховище стануterraform fmt -check # єдиний стиль кодуterraform validate # синтаксис і схема ресурсів, без звернення до хмариterraform plan -out=tfplanterraform show tfplan # переглянути збережений планterraform apply tfplan # застосувати саме той план, який ви бачилиplan оновлює стан із хмари, порівнює його з кодом і показує кожну дію
зі знаком: + створити, ~ змінити, - видалити, -/+ замінити (видалити
й створити наново). Останній символ найважливіший для читання: для бази даних
чи диска «замінити» означає втрату даних, і Terraform це показує, але не зупиняє.
Збережений через -out план гарантує, що apply виконає саме те, що затвердила
людина, а не те, що змінилося за годину між рев’ю й запуском.
Для CI корисні два прапорці: -detailed-exitcode повертає код 2, якщо зміни
є, а -lock-timeout=5m змушує чекати на блокування замість негайної помилки.
Якщо запуск обірвався й лишив блокування, його знімає terraform force-unlock
з ідентифікатором блокування, але лише після того, як ви переконалися, що
жоден запуск справді не працює.
До цього ж циклу належать перевірки припущень: блоки precondition
і postcondition у ресурсах та окремі блоки check спрацьовують під час
plan і apply, а команда terraform test запускає тести модулів.
Дрейф (drift) виникає, коли реальна інфраструктура розійшлася з тим, що
знає Terraform: правило файрвола змінили в консолі, вимкнули шифрування
вручну, автомасштабування змінило кількість інстансів. Виявляє його той самий
plan, бо він оновлює стан із хмари. Щоб побачити лише розбіжності, без
пропозицій щодо коду, є режим:
terraform plan -refresh-only # що змінилося поза Terraformterraform apply -refresh-only # прийняти це у стан, не змінюючи хмаруДалі рішення людини: або привести хмару до коду (звичайний apply
скасує ручну зміну), або підправити код під нову реальність. Друге
буває виправданим, наприклад коли автомасштабування змінює desired_count,
і тоді ці атрибути виносять у lifecycle { ignore_changes = [...] }.
Зворотна ситуація: ресурс вже існує, а в коді його немає. Його підключають
до стану блоком import, який працює в межах звичайного plan і apply:
resource "aws_s3_bucket" "legacy" { bucket = "acme-legacy-reports"}
import { to = aws_s3_bucket.legacy id = "acme-legacy-reports"}resource "azurerm_resource_group" "legacy" { name = "rg-legacy" location = "northeurope"}
import { to = azurerm_resource_group.legacy id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-legacy"}resource "google_storage_bucket" "legacy" { name = "acme-legacy-reports" location = "EU"}
import { to = google_storage_bucket.legacy id = "acme-legacy-reports"}Формат id різний: назва в S3 і GCS, повний шлях ARM у Azure. Його завжди
беруть зі сторінки ресурсу в документації Terraform-провайдера, розділ Import.
Для перейменування ресурсу в коді без перестворення служить блок moved:
moved { from = aws_s3_bucket.reports to = module.reports.aws_s3_bucket.this}Модулі
Section titled “Модулі”Модуль є каталогом з .tf-файлами, який викликають з іншого місця з параметрами.
Потрібен він з тієї самої причини, що й функція: у вас десять бакетів з однаковими
правилами приватності й версіонування, і правило треба змінити в одному місці.
Вхідні змінні (variable) є аргументами модуля, вихідні (output) поверненим
значенням. Змінна може мати validation, яка відхилить хибний вхід ще до
звернення до API хмари.
variable "name" { type = string
validation { condition = can(regex("^[a-z0-9][a-z0-9.-]{2,62}$", var.name)) error_message = "Назва бакета: 3-63 символи, малі літери, цифри, крапки й дефіси." }}
resource "aws_s3_bucket" "this" { bucket = var.name}
resource "aws_s3_bucket_versioning" "this" { bucket = aws_s3_bucket.this.id
versioning_configuration { status = "Enabled" }}
output "arn" { value = aws_s3_bucket.this.arn}variable "name" { type = string
validation { condition = can(regex("^[a-z0-9]{3,24}$", var.name)) error_message = "Назва: 3-24 символи, лише малі літери й цифри." }}
variable "resource_group_name" { type = string}
variable "location" { type = string}
resource "azurerm_storage_account" "this" { name = var.name resource_group_name = var.resource_group_name location = var.location account_tier = "Standard" account_replication_type = "ZRS" allow_nested_items_to_be_public = false
blob_properties { versioning_enabled = true }}
output "id" { value = azurerm_storage_account.this.id}variable "name" { type = string
validation { condition = can(regex("^[a-z0-9][a-z0-9._-]{2,62}$", var.name)) error_message = "Назва бакета: 3-63 символи, малі літери, цифри, крапки, дефіси й підкреслення." }}
resource "google_storage_bucket" "this" { name = var.name location = "europe-west1" uniform_bucket_level_access = true public_access_prevention = "enforced"
versioning { enabled = true }}
output "url" { value = google_storage_bucket.this.url}Викликається модуль однаково в усіх трьох випадках; відрізняються лише аргументи, які він оголосив:
module "reports" { source = "./modules/private-store" name = "acme-reports-prod"}Модуль може лежати в сусідньому каталозі, у git-репозиторії з тегом версії або в реєстрі. Готових модулів у Terraform Registry багато, але кожен чужий модуль ви успадковуєте разом з його рішеннями: перш ніж брати, прочитайте, які ресурси він створює і що станеться, коли вийде нова версія. Зручна межа: модуль інкапсулює одне рішення (приватний бакет, мережа з підмережами), а не «весь проєкт» зі сотнею змінних.
Policy-as-code
Section titled “Policy-as-code”Ревʼю коду ловить помилки, які бачить людина. Помилку «бакет відкритий у світ» вона легко пропускає, а перевірка може виявити її щоразу. Політики як код працюють на двох рівнях.
Перед застосуванням політику запускають проти плану. Terraform вміє
вивести його в JSON (terraform show -json tfplan), а інструмент політик
перевіряє цей файл у CI. Поширені інструменти: Open Policy Agent з мовою Rego
(і його обгортка conftest), HashiCorp Sentinel у комерційних продуктах,
Checkov для статичного аналізу. Політика на Rego може, наприклад, вимагати
ручного погодження для кожного видалення бакета:
package terraform.guard
import rego.v1
deny contains msg if { some rc in input.resource_changes rc.type == "aws_s3_bucket" "delete" in rc.change.actions msg := sprintf("видалення бакета %s потребує ручного погодження", [rc.address])}terraform show -json tfplan > plan.jsonconftest test plan.json # код виходу не нуль, якщо є порушенняПісля застосування, на боці хмари. Це останній рубіж: навіть якщо код обійшов перевірку, платформа відмовить у порушенні. Кожен провайдер має власний механізм, і його теж можна описати в Terraform:
variable "target_id" { type = string description = "ID організаційної одиниці або акаунта"}
resource "aws_organizations_policy" "guardrails" { name = "guardrails-baseline" type = "SERVICE_CONTROL_POLICY"
content = jsonencode({ Version = "2012-10-17"
Statement = [{ Sid = "DenyLeavingAndPublicAccessChanges" Effect = "Deny" Action = ["organizations:LeaveOrganization", "s3:PutAccountPublicAccessBlock"] Resource = "*" }] })}
resource "aws_organizations_policy_attachment" "guardrails" { policy_id = aws_organizations_policy.guardrails.id target_id = var.target_id}Політика керування службами (service control policy, SCP) обмежує максимум прав у всіх акаунтах одиниці, навіть для адміністратора акаунта.
variable "subscription_id" { type = string}
resource "azurerm_subscription_policy_assignment" "allowed_locations" { name = "allowed-locations" subscription_id = "/subscriptions/${var.subscription_id}" policy_definition_id = "/providers/Microsoft.Authorization/policyDefinitions/e56962a6-4747-49cd-b67b-bf8b01975c4c"
parameters = jsonencode({ listOfAllowedLocations = { value = ["northeurope", "westeurope"] } })}Azure Policy оцінює кожне створення чи зміну ресурсу. Тут вбудована політика «Allowed locations» забороняє створювати ресурси поза двома регіонами.
variable "project_id" { type = string}
resource "google_org_policy_policy" "no_sa_keys" { name = "projects/${var.project_id}/policies/iam.disableServiceAccountKeyCreation" parent = "projects/${var.project_id}"
spec { rules { enforce = "TRUE" } }}Політика організації (Organization Policy) вимикає створення ключів сервісних акаунтів у проєкті: довгоживучих ключів не буде навіть у того, хто дуже хоче.
Різниця між двома рівнями практична: перевірка плану дає розробнику швидку й зрозумілу відповідь у pull request, а політика платформи діє на всі шляхи до хмари, включно з консоллю й ручними командами.
Ліцензія і OpenTofu
Section titled “Ліцензія і OpenTofu”У серпні 2023 року HashiCorp змінила ліцензію Terraform з MPL 2.0 на Business
Source License (BSL). Вона обмежує використання коду для продуктів, які
конкурують з HashiCorp; звичайне користування Terraform для власної
інфраструктури не заборонене. У відповідь спільнота створила форк OpenTofu
під егідою Linux Foundation, який лишається на відкритій ліцензії MPL.
Мова, провайдери й формат стану в них спільні, тож весь код цього курсу
працює в обох інструментах без змін: замість terraform ви запускаєте tofu.
Надалі ми пишемо «Terraform», маючи на увазі обидва.
Нативні інструменти
Section titled “Нативні інструменти”Кожен провайдер має власну систему опису інфраструктури. Вони глибше інтегровані з платформою, зате прив’язують до неї:
| AWS | Azure | Google Cloud | |
|---|---|---|---|
| Декларативний шаблон | CloudFormation (JSON/YAML), ресурси групуються в стеки | Bicep (компілюється в ARM-шаблони) | Infrastructure Manager, розгортає Terraform-конфігурації |
| Код замість шаблону | AWS CDK: TypeScript, Python та інші, генерує CloudFormation | Bicep є мовою, окремого CDK немає | Terraform як основа |
| Де зберігається стан | Сервіс CloudFormation, окремого файла немає | Azure Resource Manager | Стан Terraform веде сам Infrastructure Manager |
Нативні інструменти мають дві переваги: стан веде сама платформа, а нові функції сервісів зазвичай з’являються в них першими, тоді як Terraform-провайдери іноді відстають. Мінус той самий, що й завжди: шаблон Bicep не працює в AWS. Для команди, яка працює з двома хмарами, один Terraform дає єдину мову й єдиний робочий цикл, хоча самі ресурси різні.
Google Cloud тут відрізняється: його власний Deployment Manager вийшов із підтримки, і рекомендованою заміною став Infrastructure Manager, який керований сервіс над Terraform.
Доставка: від коміту до хмари
Section titled “Доставка: від коміту до хмари”Опис інфраструктури лежить у git. Наступне питання: хто його застосовує.
Відповідь «розробник з ноутбука» має три вади: ключі облікових даних лежать
на ноутбуці, немає журналу, хто що застосував, а результат залежить від
локальної версії плагінів. Тому plan і apply виконує конвеєр CI/CD.
Федерація CI з хмарою через OIDC
Section titled “Федерація CI з хмарою через OIDC”Конвеєру потрібні права на хмару. Найпростіше вписати ключ доступу в секрети репозиторію, і саме цей шлях призводить до витоків: ключ не має терміну дії, лежить у багатьох місцях і його важко відкликати. Чому ролі й тимчасові облікові дані безпечніші, розказано в модулі 3, тут лише механіка для CI.
GitHub Actions видає кожному завданню підписаний токен OpenID Connect (OIDC): JWT, у якому сказано, з якого репозиторію, гілки чи середовища запущене завдання. Хмара налаштована довіряти підписам GitHub (така довіра називається федерацією, federation) і обмінює токен на тимчасові облікові дані. Довгоживучого секрету в GitHub немає.
Схему обміну токена показано в модулі 3.
Найважливіше твердження (claim) у токені називається sub, суб’єкт.
Його формат залежить від того, з чого запущено завдання:
| Запуск | Значення sub |
|---|---|
| Гілка | repo:ORG/REPO:ref:refs/heads/main |
| Pull request | repo:ORG/REPO:pull_request |
| Середовище | repo:ORG/REPO:environment:production |
| Тег | repo:ORG/REPO:ref:refs/tags/v1.2.0 |
Довірчі умови в хмарі перевіряють саме sub. Тому можна видати окремі ролі:
plan у pull request отримує роль лише для читання, а apply у середовищі
production, яке вимагає ручного затвердження, отримує роль із правом запису.
Завдання мусить мати дозвіл id-token: write, інакше токена не буде.
GitHub описує також незмінний формат sub із числовими ідентифікаторами
власника й репозиторію; перевірте в документації, який діє у вашій організації.
on: push: branches: [main]
permissions: id-token: write contents: read
jobs: apply: runs-on: ubuntu-latest environment: production steps: - uses: actions/checkout@v7 - uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: arn:aws:iam::111122223333:role/gh-terraform-apply aws-region: eu-central-1 - uses: hashicorp/setup-terraform@v4 - run: terraform init - run: terraform apply -auto-approveДовірча політика ролі містить умову на
token.actions.githubusercontent.com:sub зі значенням
repo:ORG/REPO:environment:production; аудиторія (aud) дорівнює
sts.amazonaws.com. OIDC-провайдер у IAM створюється один раз на акаунт.
on: push: branches: [main]
permissions: id-token: write contents: read
jobs: apply: runs-on: ubuntu-latest environment: production steps: - uses: actions/checkout@v7 - uses: azure/login@v3 with: client-id: ${{ vars.AZURE_CLIENT_ID }} tenant-id: ${{ vars.AZURE_TENANT_ID }} subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} - uses: hashicorp/setup-terraform@v4 - run: terraform init - run: terraform apply -auto-approveІдентифікатори клієнта, тенанта й підписки не є секретами, тому їх кладуть
у змінні репозиторію. Федеративні облікові дані (federated identity credential)
створюються в застосунку Entra ID або в керованої ідентичності, і саме в них
записано очікуваний subject. Провайдер azurerm підхоплює вхід Azure CLI,
який зробив azure/login.
on: push: branches: [main]
permissions: id-token: write contents: read
jobs: apply: runs-on: ubuntu-latest environment: production steps: - uses: actions/checkout@v7 - uses: google-github-actions/auth@v3 with: workload_identity_provider: projects/123456789/locations/global/workloadIdentityPools/github/providers/my-repo service_account: terraform-apply@acme-prod.iam.gserviceaccount.com - uses: hashicorp/setup-terraform@v4 - run: terraform init - run: terraform apply -auto-approveПул ідентичності робочих навантажень (Workload Identity Pool) з провайдером
GitHub створюється один раз у проєкті, а умова на assertion.sub записується
в атрибутну умову провайдера. Замість сервісного акаунта можна видати роль
безпосередньо ідентичності пулу (direct workload identity federation),
і тоді параметр service_account не потрібен.
Числа після @ є мажорними тегами дій. У виробничих конвеєрах дії краще
фіксувати за хешем коміту: сторонній код виконується з правами вашого завдання.
Стратегії розгортання
Section titled “Стратегії розгортання”Нова версія застосунку має замінити стару. Способів три, і вони різняться тим, скільки користувачів ризикують і що коштує тримати стару версію поруч.
| Стратегія | Як працює | Ціна | Коли обирати |
|---|---|---|---|
| Rolling | Інстанси оновлюються партіями, поки не замінені всі | Зайвих потужностей немає; відкат означає нову викатку | Типовий вибір; зміни, сумісні зі старою версією |
| Blue-green | Поруч із поточним («синім») середовищем піднімають нове («зелене»), потім перемикають трафік | Подвійні потужності на час викатки; відкат миттєвий | Зміни, які не можна змішувати зі старою версією |
| Canary | Нова версія отримує малу частку трафіку (наприклад 5%), потім її збільшують за метриками | Потрібні метрики й автоматична оцінка | Великі зміни, ризик яких видно лише в реальному трафіку |
Вибір визначає схема даних. При rolling і canary дві версії працюють з однією базою одночасно, тому міграції роблять у два кроки: спершу зміна, сумісна з обома версіями, і лише після повної викатки прибирання старого.
Кожна платформа розподіляє трафік між версіями по-своєму. Ось поділ 90 до 10 для канарки:
aws lambda update-alias \ --function-name my-function \ --name live \ --routing-config 'AdditionalVersionWeights={2=0.1}'Псевдонім live вказує на основну версію функції, а 10% викликів
йдуть на версію 2. Для контейнерів у ECS цю роль виконує AWS CodeDeploy
із конфігураціями на кшталт CodeDeployDefault.ECSCanary10Percent5Minutes.
az containerapp ingress traffic set \ -n my-containerapp -g my-rg \ --revision-weight latest=10 my-containerapp--v1=90Azure Container Apps у режимі кількох ревізій (multiple revision mode) робить кожну версію окремою ревізією, а вага визначає частку трафіку.
gcloud run services update-traffic my-service \ --to-revisions=my-service-00002-abc=10,my-service-00001-xyz=90У Cloud Run кожне розгортання створює ревізію, а трафік ділиться між ними
у відсотках. Відкат виглядає як --to-revisions=my-service-00001-xyz=100.
У Kubernetes ті самі стратегії реалізують контролери на кшталт Argo Rollouts і Flagger, а канарку можна вести за метриками з модуля 11: якщо частка помилок нової версії перевищила поріг, відкат виконується автоматично.
Feature flags
Section titled “Feature flags”Feature flag відокремлює розгортання коду від його вмикання. Код нової функції потрапляє в продакшн вимкненим, а вмикається змінною конфігурації для 1% користувачів або для співробітників, без нової викатки. Якщо функція шкодить, її вимикають за секунди.
Прапорці накопичуються, і кожен є гілкою в коді, яку колись треба видалити, тому в нього є власник і дата видалення. Нейтральний інтерфейс для коду дає стандарт OpenFeature: код питає про прапорець, а сховище за ним можна замінити. Керовані сховища є в AWS (AppConfig, розділ feature flags) і Azure (App Configuration з керуванням функціями). Окремого сервісу такого класу в Google Cloud немає, тож команди беруть сторонній продукт.
GitOps
Section titled “GitOps”Для Kubernetes існує інша модель доставки. Конвеєр не «штовхає» зміни в кластер: усередині кластера працює контролер (Argo CD або Flux, обидва є випускниками CNCF), який стежить за git-репозиторієм і сам приводить кластер до описаного там стану.
apiVersion: argoproj.io/v1alpha1kind: Applicationmetadata: name: shop namespace: argocdspec: project: default source: repoURL: https://github.com/acme/deploy targetRevision: main path: apps/shop/prod destination: server: https://kubernetes.default.svc namespace: shop syncPolicy: automated: prune: true selfHeal: trueПриклад однаковий для EKS, AKS і GKE, бо описує Kubernetes, а не хмару.
prune видаляє з кластера те, чого вже немає в git, а selfHeal повертає
ручні зміни назад: це той самий дрейф, виправлений автоматично. Відкат
зводиться до git revert, а конвеєру не потрібні права на кластер. Ціна
цього: репозиторій із конфігурацією стає найпривілейованішим місцем,
і право запису в нього захищають як адміністративне.
Середовища як окремі акаунти
Section titled “Середовища як окремі акаунти”Розділити dev і prod можна каталогами, гілками, просторами імен або воркспейсами Terraform. Найміцніша межа проходить по акаунтах: AWS-акаунт, підписка Azure, проєкт Google Cloud (ієрархію розглянуто в модулі 2). Тоді помилка в dev не зачепить prod просто тому, що в неї немає прав; бюджети, ліміти й журнали теж розділені. Групуються акаунти в організаційні одиниці (AWS), групи керування (Azure) і папки (Google Cloud), а політики на групу задають SCP, Azure Policy і політики організації.
Воркспейси Terraform ділять код і налаштування сховища, а облікові дані лишаються однакові, тож межею безпеки вони не є. Надійніша схема: окремий корінь для кожного середовища зі своїм станом і власною роллю конвеєра, а prod додатково вимагає ручного затвердження.
Як це в трьох хмарах
Section titled “Як це в трьох хмарах”| Задача | AWS | Azure | Google Cloud |
|---|---|---|---|
| Сховище стану Terraform | S3 + use_lockfile |
Blob Storage + оренда blob | Cloud Storage |
| Нативний шаблон | CloudFormation, CDK | Bicep | Infrastructure Manager |
| Вхід CI без ключів | IAM OIDC-провайдер + роль | Федеративні облікові дані Entra ID | Workload Identity Federation |
| Політика на рівні платформи | SCP, AWS Config | Azure Policy | Політика організації |
| Розподіл трафіку між версіями | Псевдоніми Lambda, CodeDeploy | Ревізії Container Apps, слоти App Service | Ревізії Cloud Run |
| Керовані feature flags | AppConfig | App Configuration | немає окремого сервісу |
Чим відрізняється і що з цього випливає. Terraform прибирає різницю в мові, але не в моделі. Один і той самий «приватний бакет» у AWS складається з трьох ресурсів (сам бакет, версіонування, блокування публічного доступу), в Azure це обліковий запис сховища з вкладеними блоками, а в Google Cloud один ресурс. Модуль, який інкапсулює таку різницю, для кожної хмари виглядає по-своєму; переносити його між хмарами без переписування не вийде. Друга відмінність у розподілі трафіку між версіями: у AWS він розкиданий по кількох сервісах (псевдоніми Lambda, CodeDeploy, зважені цільові групи ALB), а в Azure Container Apps і Cloud Run це властивість самої платформи.
Розбір: довіра OIDC-ролі, яка надто довірлива
Section titled “Розбір: довіра OIDC-ролі, яка надто довірлива”Це типова конфігураційна помилка, а не один конкретний інцидент, тому
подаємо її як сценарій. Команда налаштувала OIDC-довіру AWS для репозиторію
й, щоб «все працювало», записала умову як repo:acme/*:*. Це означає:
роль може отримати кожен репозиторій організації, у будь-якій гілці,
навіть pull request.
Механізм такий. Токен підписаний GitHub і справжній, тож хмара його приймає.
Але хмара перевіряє лише те, що вказано в умові. Розробник, який відкрив
pull request у будь-якому репозиторії організації, змінює workflow: замість
terraform plan додає крок, який виводить облікові дані. Завдання запускається
з токеном, sub якого підпадає під шаблон, і отримує роль із правами
запису в prod. Довгоживучого ключа не було, а доступ усе одно отримано,
бо довіра описана надто широко.
Виправлення складається з трьох частин. Умова в довірчій політиці має бути
точною: repo:acme/infra:environment:production, без символів підстановки
по репозиторію й гілці. Роль для pull request має лише читати. Середовище
production у GitHub вимагає ручного затвердження, тож навіть змінений workflow
не запустить apply без людини. Ці правила ви перевіряєте так само, як
політики Terraform: як код, у репозиторії, з ревʼю.
Типові помилки розуміння
Section titled “Типові помилки розуміння”«Стан — це кеш, його можна видалити й відновити». Стан є єдиним зв’язком між ресурсами в коді й об’єктами в хмарі. Без нього Terraform про наявні ресурси не знає й створить дублікати.
«Воркспейси Terraform ізолюють dev і prod». Вони ділять сховище й облікові дані. Реальну межу дає окремий акаунт чи підписка з власною ролею.
«plan показав усе, apply не може здивувати». Між plan і apply
хмара змінюється. Тому застосовують збережений через -out план, а для
небезпечних змін (-/+ для баз і дисків) додають політику й ручне затвердження.
«OIDC безпечний сам по собі». Безпека залежить від умови на sub.
Надто широка умова дає доступ будь-кому, хто може запустити завдання
у вашій організації.
«Дрейф — це помилка інструмента». Дрейф є наслідком змін поза кодом.
Лікується він процесом: заборона ручних змін у prod, selfHeal у GitOps
або регулярний plan -refresh-only за розкладом.
«Canary і blue-green — те саме». Blue-green перемикає весь трафік одразу й дає швидкий відкат за подвійну ціну. Canary поступово збільшує частку і виявляє проблеми на невеликій групі користувачів, але потребує метрик.
Перевір себе
Джерела
Section titled “Джерела”- Terraform: backend s3, azurerm, gcs
- Terraform CHANGELOG 1.11:
use_lockfileі застаріванняdynamodb_table - OpenTofu: S3 backend
- GitHub Docs: OpenID Connect reference
- aws-actions/configure-aws-credentials, azure/login, google-github-actions/auth, hashicorp/setup-terraform
- Реєстр Terraform: aws, azurerm, google
- Azure CLI: az containerapp ingress traffic
- AWS CLI: lambda update-alias
- Google Cloud: gcloud run services update-traffic
- Google Cloud: Infrastructure Manager, Deployment Manager deprecation
- Argo CD: automated sync policy