Готуємось переводити управління інфрастуктурою з AWS CDK на Terraform.
Про планування того, як воно все може виглядати писав у Terraform: початок роботи та планування нового проекту – Dev/Prod та bootsrap, але тоді оминув одну досить важливу опцію – створення lock для state-файлів.
Блокування стейт-файлів використовується для того, щоб уникнути ситуацій, коли запускається кілька інстансів Terraform одночасно – інженерами або автоматично в CI/CD, і вони одночасно будуть намагатись внести зміни в один стейт-файл: при використанні lock, Terraform заблокує запуск іншого інстансу допоки перший інстанс не завершить свою роботу і не звільнить блокування.
У нашому випадку інфрастуктура вся в AWS, тому в ролі бекенду для зберігання стейтів буде використовуватись AWS S3, а для створення lock-файлів – таблиця в DynamoDB.
Документація – State Locking.
Отже, що ми зробимо:
- таблиця DynamoDB та S3 бакет будуть менеджитись самим Терраформом
- Terraform буде авторизуватись в AWS з AssumeRole
- опишемо створення S3 bucket та таблиці DynamoDB
- створимо ресурси використовуючи локальний стейт
- імпортуємо локальний стейт в створений бакет
- протестуємо, як працює State Lock
Зміст
IAM Role
Terraform буде працювати через окрему IAM Role, див. Use AssumeRole to provision AWS resources across accounts.
Переходимо в IAM > Create Role, вибираємо Custom Trust Policy, і описуємо її:
{ "Version": "2012-10-17", "Statement": [ { "Sid": "Statement1", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::123456789012:root" }, "Action": "sts:AssumeRole" } ] }
Замість 123456789012 вказуємо ID аккаунту, а в root
позначаємо, що будь-який аутентифікований IAM User цього аккаунта зможете виконати sts:AssumeRole
цієї ролі.
Поки задаємо AdministratorAccess, пізніше можна буде налаштувати права більш детально:
Зберігаємо роль:
Перевіримо, що вона працює.
В свій ~/.aws/config
додаємо новий профайл:
[profile tf-admin] role_arn = arn:aws:iam::492***148:role/tf-admin source_profile = default
І виконуємо sts get-caller-identity
з цим профайлом:
[simterm]
$ aws --profile tf-admin sts get-caller-identity { "UserId": "ARO***ZEF:botocore-session-1693297579", "Account": "492***148", "Arn": "arn:aws:sts::492***148:assumed-role/tf-admin/botocore-session-1693297579" }
[/simterm]
Окей, тепер можемо переходити до самого Terraform.
Налаштування Terraform-проекту
Додаємо версії модулів, котрі будемо використовувати.
Останню версію провайдеру AWS можна взяти тут>>>, а версію самого Terraform – тут>>>.
Створюємо файл versions.tf
:
terraform { required_version = ">= 1.5" required_providers { aws = { source = "hashicorp/aws" version = ">= 5.14.0" } } }
Додаємо файл providers.tf
, де описуємо параметри підключення до AWS:
provider "aws" { region = "us-east-1" assume_role { role_arn = "arn:aws:iam::492***148:role/tf-admin" } }
Створюємо файл main.tf
, поки що пустий, і перевіряємо, що Terraform може виконати AssumeRole.
Виконуємо ініціалізцію:
[simterm]
$ terraform init Initializing the backend... Initializing provider plugins... - Finding hashicorp/aws versions matching ">= 5.14.0"... - Installing hashicorp/aws v5.14.0... - Installed hashicorp/aws v5.14.0 (signed by HashiCorp) ...
[/simterm]
То робимо terraform plan
:
[simterm]
$ terraform plan No changes. Your infrastructure matches the configuration. Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.
[/simterm]
Все добре – Terraform підключився до нашого AWS-аккаунту.
Створення AWS S3 для бекенду
Для корзини, де будуть зберігатись state-файли, потрібно мати:
- encryption: для AWS S3 включено по дефолту, але можна налаштувати з власним ключем з AWS KMS
- access control: закрити публічний доступ до об’єктів в корзині
- versioning: налаштувати версіонування, щоб мати історію змін в стейт-файлах
Створюємо файл backend.tf
, і описуємо створення KMS ключа та корзини:
resource "aws_kms_key" "tf_lock_testing_state_kms_key" { description = "This key is used to encrypt bucket objects" deletion_window_in_days = 10 } # create state-files S3 buket resource "aws_s3_bucket" "tf_lock_testing_state_bucket" { bucket = "tf-lock-testing-state-bucket" lifecycle { prevent_destroy = true } } # enable S3 bucket versioning resource "aws_s3_bucket_versioning" "tf_lock_testing_state_versioning" { bucket = aws_s3_bucket.tf_lock_testing_state_bucket.id versioning_configuration { status = "Enabled" } } # enable S3 bucket encryption resource "aws_s3_bucket_server_side_encryption_configuration" "tf_lock_testing_state_encryption" { bucket = aws_s3_bucket.tf_lock_testing_state_bucket.id rule { apply_server_side_encryption_by_default { kms_master_key_id = aws_kms_key.tf_lock_testing_state_kms_key.arn sse_algorithm = "aws:kms" } bucket_key_enabled = true } } # block S3 bucket public access resource "aws_s3_bucket_public_access_block" "tf_lock_testing_state_acl" { bucket = aws_s3_bucket.tf_lock_testing_state_bucket.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true }
Далі, там же додаємо створення DynamoDB таблиці для state lock:
... # create DynamoDB table resource "aws_dynamodb_table" "tf_lock_testing_state_ddb_table" { name = "tf-lock-testing-state-ddb-table" billing_mode = "PAY_PER_REQUEST" hash_key = "LockID" attribute { name = "LockID" type = "S" } }
Перевіряємо, чи все правильно описали:
[simterm]
$ terraform plan Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # aws_dynamodb_table.tf_lock_testing_state_ddb_table will be created + resource "aws_dynamodb_table" "tf_lock_testing_state_ddb_table" { + arn = (known after apply) + billing_mode = "PAY_PER_REQUEST" + hash_key = "LockID" ... Plan: 5 to add, 0 to change, 0 to destroy.
[/simterm]
І виконуємо terraform apply
, щоб створити ресурси:
[simterm]
$ terraform apply ... Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes ... Apply complete! Resources: 5 added, 0 changed, 0 destroyed.
[/simterm]
Перевіряємо корзину:
Та таблицю DynamoDB:
Налаштування Terraform Backend та State Lock
Тепер можемо додати бекенд з параметром dynamodb_table
для створення lock.
До файлу backend.tf
додаємо блок terraform.backend.s3
:
terraform { backend "s3" { bucket = "tf-lock-testing-state-bucket" key = "tf-lock-testing-state-bucket.tfstate" region = "us-east-1" dynamodb_table = "tf-lock-testing-state-ddb-table" encrypt = true } } ...
Виконуємо terraform init
ще раз, та імпортуємо локальний state в корзину:
[simterm]
$ terraform init Initializing the backend... Acquiring state lock. This may take a few moments... Do you want to copy existing state to the new backend? ... Enter a value: yes Releasing state lock. This may take a few moments... Successfully configured the backend "s3"! Terraform will automatically use this backend unless the backend configuration changes. Initializing provider plugins... - Reusing previous version of hashicorp/aws from the dependency lock file - Using previously-installed hashicorp/aws v5.14.0 Terraform has been successfully initialized!
[/simterm]
Перевіряємо DynamoDB тепер – маємо ключ:
І стейт в S3:
Якщо переглянути таблицю DynamoDB під час виконання plan
чи apply
– можна побачити сам lock з полями Operation та хто саме виконує операцію:
Тестування State Lock
Додаємо файл main.tf
с ресурсом EC2:
resource "aws_instance" "ec2_lock_test" { ami = "ami-0d2fcfe4f5c4c5b56" instance_type = "t2.micro" tags = { Name = "EC2 Instance with remote state" } }
Копіюємо всі файли проекту в новий каталог:
[simterm]
$ mkdir test-lock $ cp -r * test-lock/ cp: cannot copy a directory, 'test-lock', into itself, 'test-lock/test-lock'
[simterm]
В поточному каталозі запускаємо terraform apply
, але не відповідаємо yes, щоб створений в DynamoDB lock залишався:
[simterm]
$ terraform apply Acquiring state lock. This may take a few moments... ...
[/simterm]
Переходимо в другий каталог, і там запускаємо init
та apply
ще раз:
[simterm]
$ cd test-lock/ $ terraform init && terraform apply ... Acquiring state lock. This may take a few moments... ╷ │ Error: Error acquiring the state lock │ │ Error message: ConditionalCheckFailedException: The conditional request failed │ Lock Info: │ ID: 98dd894b-065f-8f63-f695-d4dcea702807 │ Path: tf-lock-testing-state-bucket/tf-lock-testing-state-bucket.tfstate │ Operation: OperationTypeApply ...
[/simterm]
Та маємо помилку створення блокування, бо вже є процесс, який користується нашим state-файлом.
Terraform State Lock trics
force-unlock
Іноді буває, що Terraform не звільняє lock, наприклад, якщо при виконанні операції відвалився інтернет.
Тоді можемо звільти стейт за допомогою force-unlock
, якому передаємо Lock ID:
[simterm]
$ terraform force-unlock 98dd894b-065f-8f63-f695-d4dcea702807 Do you really want to force-unlock? Terraform will remove the lock on the remote state. ... Enter a value: yes Terraform state has been successfully unlocked!
[/simterm]
lock-timeout
Іноді треба, щоб Terraform не зупиняв роботу, як тільки побачить, що lock-запис вже є. Наприклад, в CI-пайплайні можуть бути одночасно запущені дві джоби, і тоді друга запиниться з полмилкою.
В такому випадку можемо додати lock-timeout
– тоді Terraform зачекає заданий період часу, і спробує виконати lock ще раз:
[simterm]
$ terraform apply -lock-timeout=180s
[/simterm]
Готово.