Вже писав про питання управління бекендами у постах Terraform: початок роботи та планування нового проекту – Dev/Prod та bootsrap та Terraform: динамічний remote state з AWS S3 та multiple environments по директоріям, повернемось до цієї теми знов.
Отже, вибрав все ж варіант з менеджментом бекендів через окремий проект Terraform, де в змінних маємо список проектів, яким треба мати AWS S3 bucket та таблицю DynamoDB, та їхніх оточень – Dev/Prod.
Потім в циклі for_each
проходимось по елементам списку проектів, і створюємо необхідні ресурси.
В такому випадку девелоперам, щоб запустити новий проект, не треба мати справу зі створенням ресурсів для бекенду state-файлів взагалі – вони або самі можуть просто додати нове значення в змінну і виконати terraform apply
, чи попросити когось з DevOps-тіми, а потім просто додати значення да власного backend.tf
.
Бекенд для самого проекту який менеджить всі бекенди створюється ним же – в перший раз з локальним бекендом, а після створення проекту – його стейт туди імпортується, і надалі вже використовується цей remote state.
Зміст
Providers
Тут у нас буде тільки AWS:
terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 5.14" } } } provider "aws" { region = "us-east-1" profile = "tf-admin" default_tags { tags = { component = "devops" created-by = "terraform" } } }
Файл backend.tf
поки не описуємо – робимо все локально.
Виконуємо terraform init
, і переходимо до змінних.
Variables – список проектів і оточень
Тут нам потрібна по факту одна змінна з типом map(list(string))
, в якій ми описуємо список проектів, для яких будемо створювати ресурси, в тому числі включаємо в неї сам проект, який буде створювати всі ці ресурси.
І для кожного елементу з іменем проекту в значення включаємо список з іменами оточень цього проекту:
variable "projects" { description = "Project names with their environments to be used in S3 and DynamoDB resources" type = map(list(string)) default = { atlas-tf-backends-test = [ "prod" ] atlas-eks-test = [ "dev", "prod" ] } }
Resources
Створення AWS S3 бакетів
Що нам треба, це для кожного проекту створити AWS S3 Bucket, включити йому Versioning, додати Encryption, і заборонити публічний доступ до об’єктів через S3 Bucket ACL.
Щодо Dev/Prod оточень: можна створювати окремі корзини на кожен Env кожного проекту, чи один бакет на проект, а вже в самому проекті використовувати різні ключі, тобто:
- проект atlas-eks-test
- корзина
atlas-eks-test
- при
terraform init
Dev-оточення використовуємо-backend-config="key=dev/atlas-eks.tfstate"
- при
terraform init
для Prod-оточення використовуємо-backend-config=key=prod/atlas-eks.tfstate
- при
Для таблиць DynamoDB створимо окремі таблиці для Dev/Prod, а всякі feature-енви можна буде деплоїти або без State Lock, бо вони тимчасові, і будуть деплоїтись з якогось одного Pull Request з GitHub Actions, або при потребі – створювати таблицю під час деплою проекту командою AWS CLI create-table
.
Отже – в змінних маємо map
зі списком проектів.
Для S3 використовуємо for_each
, з якого отримуємо each.key
, який буде містити ім’я проекту, тобто “atlas-eks-test” або “atlas-tf-backends-test“:
# create state-files S3 buket resource "aws_s3_bucket" "state_backend" { for_each = var.projects bucket = "tf-state-backend-${each.key}" # to drop a bucket, set to `true` force_destroy = false lifecycle { # to drop a bucket, set to `false` prevent_destroy = true } tags = { environment = var.environment } }
Далі, для ресурсів aws_s3_bucket_versioning
, aws_s3_bucket_server_side_encryption_configuration
та aws_s3_bucket_public_access_block
знов використовуємо for_each
, але тепер ітерацію виконуємо по списку ресурсів aws_s3_bucket.state_backend
, тобто весь код буде таким:
# create state-files S3 buket resource "aws_s3_bucket" "state_backend" { for_each = var.projects bucket = "tf-state-backend-${each.key}" # to drop a bucket, set to `true` force_destroy = false lifecycle { # to drop a bucket, set to `false` prevent_destroy = true } tags = { environment = var.environment } } resource "aws_kms_key" "state_backend_kms_key" { description = "This key is used to encrypt bucket objects" deletion_window_in_days = 10 } # enable S3 bucket versioning resource "aws_s3_bucket_versioning" "state_backend_versioning" { for_each = aws_s3_bucket.state_backend bucket = each.value.id versioning_configuration { status = "Enabled" } } # enable S3 bucket encryption resource "aws_s3_bucket_server_side_encryption_configuration" "state_backend_encryption" { for_each = aws_s3_bucket.state_backend bucket = each.value.id rule { apply_server_side_encryption_by_default { kms_master_key_id = aws_kms_key.state_backend_kms_key.arn sse_algorithm = "aws:kms" } bucket_key_enabled = true } } # block S3 bucket public access resource "aws_s3_bucket_public_access_block" "state_backend_acl" { for_each = aws_s3_bucket.state_backend bucket = each.value.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true }
В ouputs
додаємо відображення створених бакетів, використовуючи цикл for
та String Templates:
output "state_backend_bucket_names" { value = "AWS S3 State Buckets:\n%{for name in aws_s3_bucket.state_backend}- ${name.bucket}\n%{endfor}" }
Деплоїмо:
$ terraform apply ... Apply complete! Resources: 9 added, 0 changed, 0 destroyed. Outputs: state_backend_bucket_names = <<EOT AWS S3 State Buckets: - tf-state-backend-atlas-eks-test - tf-state-backend-atlas-tf-backends-test
Міграція власного state-файлу
Далі додаємо параметри до backend.tf
:
terraform { backend "s3" { bucket = "tf-state-backend-atlas-tf-backends-test" key = "atlas-tf-backends.tfstate" region = "us-east-1" profile = "tf-admin" encrypt = true } }
І виконуємо terraform init
ще раз, щоб перенести власний стейт з локального файлу terraform.tfstate
до створеного S3 бакету:
$ terraform init Initializing the backend... Do you want to copy existing state to the new backend? Pre-existing state was found while migrating the previous "local" backend to the newly configured "s3" backend. No existing state was found in the newly configured "s3" backend. Do you want to copy this state to the new "s3" backend? Enter "yes" to copy and "no" to start with an empty state. Enter a value: yes ... Terraform has been successfully initialized!
Створення таблиць DynamdoDB
Якщо для S3 ми робили одну корзину на кожен проект, то для DynamoDB буде окрема таблиця на кожен Env кожного проекту (хоча можна мати і одну – тоді Terraform при створенні ключів сам задасть значення Env, див. Backends S3).
Для цього використаємо locals
та цикли for
, як описано у Nested for loops для map of lists:
... locals { table_names_list = flatten([ # for 'atlas-eks-test["dev", "prod"]: for project, envs in var.projects : [ # for 'dev', 'prod': for env in envs : # create 'atlas-eks-test-dev' && 'atlas-eks-test-prod': "${project}-${env}" ] ]) } # create DynamoDB table resource "aws_dynamodb_table" "state_lock" { for_each = toset(local.table_names_list) name = "tf-state-lock-${each.value}" billing_mode = "PAY_PER_REQUEST" hash_key = "LockID" attribute { name = "LockID" type = "S" } tags = { environment = var.environment } }
Додамо outputs
:
... output "dynamodb_table_names" { value = "DynamoDB tables:\n%{for name in aws_dynamodb_table.state_lock}- ${name.name}\n%{endfor}" }
Та деплоїмо:
$ terraform apply ... Apply complete! Resources: 3 added, 0 changed, 0 destroyed. Outputs: dynamodb_table_names = <<EOT DynamoDB tables: - tf-state-lock-atlas-eks-test-dev - tf-state-lock-atlas-eks-test-prod - tf-state-lock-atlas-tf-backends-test-prod EOT state_backend_bucket_names = <<EOT AWS S3 State Buckets: - tf-state-backend-atlas-eks-test - tf-state-backend-atlas-tf-backends-test EOT
Видалення проекту і його S3 та DynamoDB
Якщо якийсь проект більш не актуальний, і треба видалити його ресурси – то це буде робитись в три етапи apply
:
- міняємо параметри
aws_s3_bucket
:- включаємо
force_destroy
– це потрібно, щоб видалити корзини, в яких включено Versioning і які мають об’єкти - відключаємо
prevent_destroy
– щоб дозволити видалення
- включаємо
- виконуємо
apply
, щоб застосувати зміни- видаляємо проект з
var.projects
- видаляємо проект з
- виконуємо
apply
, щоб видалити корзину та пов’язані ресурси- повертаємо значення параметрів
aws_s3_bucket
–force_destroy
таprevent_destroy
- повертаємо значення параметрів
- виконуємо
apply
, щоб застосувати зміни
Тобто:
# create state-files S3 buket resource "aws_s3_bucket" "state_backend" { for_each = var.projects bucket = "tf-state-backend-${each.key}" # to drop a bucket, set to `true` force_destroy = true lifecycle { # to drop a bucket, set to `false` prevent_destroy = false } tags = { environment = var.environment } } ...
Застосовуємо зміни на всі бакети:
$ terraform apply ... Apply complete! Resources: 0 added, 2 changed, 0 destroyed.
Потім видаляємо ім’я проекту зі значень змінної projects
:
variable "projects" { description = "Project names list with its environments to be used in S3 and DynamoDB nresources" type = map(list(string)) default = { atlas-tf-backends-test = [ "prod" ] } }
І виконуємо apply
ще раз:
... Apply complete! Resources: 0 added, 0 changed, 6 destroyed. Outputs: dynamodb_table_names = <<EOT DynamoDB tables: - tf-state-lock-atlas-tf-backends-test-prod EOT state_backend_bucket_names = <<EOT AWS S3 State Buckets: - tf-state-backend-atlas-tf-backends-test EOT
Після чого повертаємо значення force_destroy
та prevent_destroy
, і виконуємо ще один apply
.
Готово.