Вже писав про питання управління бекендами у постах 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 initDev-оточення використовуємо-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.
Готово.