Виходить такая собі серія постів про підготовку до використання Terraform на проекті.
Отже, в першій частині думалось про те, як організувати підготовку backend для проекту, тобто виконати його bootstrap, та трохи – як менеджити Dev/Prod оточення в цілому, див. Terraform: початок роботи та планування нового проекту – Dev/Prod та bootsrap. В другій – як налаштувати State Lock та про remote state в цілому, див. Terraform: remote state з AWS S3 та state locking з DynamoDB.
Рішення по типу Terraform Cloud, Terragrunt, Spacelift, Atlantis та Cluster.dev поки лишимо осторонь – проект ще малий, і вносити додаткові утіліти не хочеться. Почнемо з простого, а як воно все взлетить – то вже будемо думати про подібні рішення.
Тепер спробуємо все зібрати в кучу, і набросати план майбутньї автоматизації.
Отже, про що треба подумати:
- керування бекендом, або project bootstrap: бакет(и) для state-файлів та таблицю(і) DynamoDB для state lock:
- можна створювати руками для кожного проекту
- можна створити окремий проект/репозиторій, і в ньому менеджити всі бекенди
- можна створювати в рамках кожного проекту на початку роботи в коді самого проекту
- розділення по Dev/Prod оточенням:
- Terraform Workspaces: built-in фіча Terraform, мінімум дублікації коду, але можуть бути складнощі з навігацією, може використовувати тільки один backend (проте з окремими директоріями в ньому), складноші роботи з модулями
- Git branches: built-in фіча Git, простота навігації по коду, можливість мати окремі бекенди, але багато дублікації коду, морока с переносом коду між оточеннями, складнощі роботи з модулями
- Separate Directories: максимальная ізоляція і можливість мати окремі бекенди та провайдери, але можлива дублікація коду
- Third-party tools: Terragrunt, Spacelif, Atlantis тощо – чудово, але потребує додаткового часу на вивчення інженерами та імплементацію
Сьогодні спробуємо підхід з менеджементом бакету для бекенду з коду самого проекту, а Dev/Prod робити через окремі директорії.
Зміст
Керування бекендом, або project bootstrap
Тут будемо використовувати підхід зі створенням бекенду в рамках кожного проекту на старті.
Тобто:
- спочатку описуємо створення бакету та таблиці Динамо
- створюємо ресурси
- налаштовуємо блок
terraform.backend{} - імпортуємо стейт
- описуємо та створюємо всі інші ресурси
Розділення по Dev/Prod оточенням з окремими директоріями
Як все може виглядати з окремими каталогами?
Можемо створити структуру:
globalmain.tf: створення ресурсів для бекенду – S3, Dynamo
environmentsdevmain.tf: тут включаємо потрібні модулі (дублються з Prod, але відрізняється під час розробки та тестування нового модулю)variables.tf: декларуємо змінні, загальні (дублюються з Prod) та специфічні до оточенняterraform.tfvars: значення змінних, загальні (дублюються з Prod) та специфічні до оточенняproviders.tf: налаштування підключення до AWS/Kubernetes, специфічні до оточення (осолибво корисно, коли Dev/Prod це різні акаунти AWS)backend.tf: налаштування зберігання state-файлів, специфічні до оточення
prod- <аналогічно Dev>
modulesvpcmain.tf– описуємо модулі
backend.hcl– загальні параметри для state backend
Тоді можемо деплоїти окремі оточення або виконуючи cd environments/dev && terraform aplly, або terraform aplly -chdir=environments/dev. Бекенд можемо передавати через terraform init -backend-config=backend.hcl.
Ну і давайте спробуємо, і подивимось, як воно може виглядати в роботі.
Створення бекенду
Тут будемо робити бекенд з коду самого проекту, але мені все ж вважається кращим менеджмент AWS ресурсів для бекендів винести окремим проектом в окремому репозиторії, бо зі схемою наведеною нижче створення нового проекту виглядає трохи complecated – якщо це будуть робити самі девелопери, то їм доведеться робити окремі кроки, і для цього потрібно буде писати окрему доку. Краще нехай при старті проекту передадуть нам його ім’я, “девопси” зроблять корзину та DynamoDB таблицю, а далі девелопери вже просто захардкодять їхні імена в свої конфіги.
Створюємо директорії:
[simterm]
$ mkdir -p envs_management_test/{global,environments/{dev,prod},modules/vpc}
[/simterm]
Получаємо таку структуру:
[simterm]
$ tree envs_management_test/
envs_management_test/
├── environments
│ ├── dev
│ └── prod
├── global
└── modules
└── vpc
[/simterm]
У каталозі envs_management_test/global нам треба описати створення бакету та таблиці для локів.
Тут теж питання: робити одну корзини під кожен енв – чи одну, і стейти в ній розділяти ключами?
Multiple S3 buckets
Якщо робити по корзині на кожен енв, то можна зробити наступним чином:
- створюємо змінну з типом
list, в цей список вносимо імена оточень - потім при створенні ресурсів – використовуємо цей список, щоб в циклі пройтись по кожному індексу в ньому
Тобто, variables.tf може бути таким:
variable "environments" {
description = "Environments names"
type = set(string)
default = ["dev", "prod", "global"]
}
А у файлі main.tf створюємо ресурси так:
resource "aws_kms_key" "state_backend_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" "state_backend_bucket" {
for_each = var.project_names
bucket = "tf-state-backend-${each.value}"
# 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
}
}
# enable S3 bucket versioning
resource "aws_s3_bucket_versioning" "state_backend_versioning" {
for_each = aws_s3_bucket.state_backend_bucket
bucket = each.value.id
versioning_configuration {
status = "Enabled"
}
}
...
Єдиний S3 для оточень
Але щоб не ускладнювати код – поки зробимо одну корзину, а потім для кожного оточення задамо власний key в його backend.
Використаємо змінну для імені:
variable "project_name" {
description = "The project name to be used in global resources names"
type = string
default = "envs-management-test"
}
І в main.tf описуємо самі ресурси – тут код той самий, щоб використовувався в попередньому пості:
resource "aws_kms_key" "state_backend_bucket_kms_key" {
description = "Encrypt the state bucket objects"
deletion_window_in_days = 10
}
# create state-files S3 bukets per each Env
resource "aws_s3_bucket" "state_backend_bucket" {
bucket = "tf-state-bucket-${var.project_name}"
lifecycle {
prevent_destroy = true
}
}
# enable S3 bucket versioning per each Env's bucket
resource "aws_s3_bucket_versioning" "state_backend_bucket_versioning" {
bucket = aws_s3_bucket.state_backend_bucket.id
versioning_configuration {
status = "Enabled"
}
}
# enable S3 bucket encryption per each Env's bucket
resource "aws_s3_bucket_server_side_encryption_configuration" "state_backend_bucket_encryption" {
bucket = aws_s3_bucket.state_backend_bucket.id
rule {
apply_server_side_encryption_by_default {
kms_master_key_id = aws_kms_key.state_backend_bucket_kms_key.arn
sse_algorithm = "aws:kms"
}
bucket_key_enabled = true
}
}
# block S3 bucket public access per each Env's bucket
resource "aws_s3_bucket_public_access_block" "state_backend_bucket_acl" {
bucket = aws_s3_bucket.state_backend_bucket.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# create DynamoDB table per each Env
resource "aws_dynamodb_table" "state_dynamo_table" {
name = "tf-state-lock-${var.project_name}"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
Створюємо ресурси:
[simterm]
$ terraform init && terraform apply
[/simterm]
Налаштування динамічного State Backend
Далі нам треба налаштувати бекенд для global.
Але щоб потім не повторювати один і той самий конфіг для Dev && Prod – загальні параметри бекенду винесемо окремим файлом.
В корні проекту створюємо backend.hcl:
bucket = "tf-state-bucket-envs-management-test" region = "us-east-1" dynamodb_table = "tf-state-lock-envs-management-test" encrypt = true
В директорії global додаємо backend.tf:
terraform {
backend "s3" {
key = "global/terraform.tfstate"
}
}
Виконуємо ініціалізацію ще раз, та через -backend-config передаємо шлях до файлу с параметрами бекенду:
[simterm]
$ terraform init -backend-config=../backend.hcl 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 ... 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]
Перевіряємо корзину:
[simterm]
$ aws s3 ls tf-state-bucket-envs-management-test/global/ 2023-08-30 16:57:10 8662 terraform.tfstate
[/simterm]
Перший стейт-файл є, чудово.
Створення та використання модулів
Додамо власний модуль для VPC. Тут просто для приклада, в продакшені будемо використовувати AWS VPC Terraform module.
В файлі modules/vpc/main.tf описуємо саму VPC:
resource "aws_vpc" "vpc" {
cidr_block = var.vpc_cidr
tags = {
environment = var.environment
created-by = "terraform"
}
}
Там же додаємо файл modules/vpc/variables.tf:
variable "vpc_cidr" {
description = "VPC CIDR"
type = string
}
variable "environment" {
type = string
}
Далі описуємо змінні vpc_cidr та environment в файлах environments/dev/variables.tf та environments/prod/variables.tf:
variable "vpc_cidr" {
description = "VPC CIDR"
type = string
}
variable "environment" {
type = string
}
У файлі environments/dev/terraform.tfvars значення для них:
vpc_cidr = "10.0.1.0/24" environment = "dev"
І в environments/prod/terraform.tfvars інші значення:
vpc_cidr = "10.0.2.0/24" environment = "prod"
В обох environments створюємо main.tf, де включаємо модуль VPC:
module "vpc" {
source = "../../modules/vpc"
vpc_cidr = var.vpc_cidr
environment = var.environment
}
Додаємо providers.tf аналогічний тому, що маємо в global:
[simterm]
$ cp global/providers.tf environments/dev/ $ cp global/providers.tf environments/prod/
[/simterm]
І в кожному створюємо власний backend.tf, але з різними key.
Dev:
terraform {
backend "s3" {
key = "dev/terraform.tfstate"
}
}
Та Prod:
terraform {
backend "s3" {
key = "prod/terraform.tfstate"
}
}
Тепер у нас виходить така структура каталогів та файлів:
І тепер можемо деплоїти ресурси.
Спочатку Dev:
[simterm]
$ cd environments/dev/ $ terraform init -backend-config=../../backend.hcl $ terraform apply
[/simterm]
І повторюємо для Prod:
[simterm]
$ cd ../prod/ $ terraform init -backend-config=../../backend.hcl $ terraform apply
[/simterm]
Перевіряємо бакет стейтів:
[simterm]
$ aws s3 ls tf-state-bucket-envs-management-test/
PRE dev/
PRE global/
PRE prod/
Та самі стейти:
[simterm]
$ aws s3 ls tf-state-bucket-envs-management-test/dev/ 2023-08-30 17:32:07 1840 terraform.tfstate
[/simterm]
І чи створились VPC:
Динамічні оточення
Добре – схема з окремими диреткоріями для Dev/Prod виглядає робочю.
Але як бути для динамічних оточень, тобто коли ми хочемо створити інфрастуктуру проекту під час створення Pull Request в Git, для тестів?
Тут можемо використати такий флоу:
- бранчуємось від мастер-бранчу
- робимо свої зміни в коді
environments/dev/ - ініціалізуємо новий бекенд
- і деплоїмо з
terraform apply -varз новими значеннями змінних
Ініціалізуємо новий стейт. Додаємо -reconfigure, бо робимо локально, і тут вже є .terraform. У випадку, коли це буде виконуватись з GitHub Actions – директорія буде чистою, і можна виконувати просто init.
У другому параметрі -backend-config передаємо ключ для стейту – в якій директорії корзини зберігати файл:
[simterm]
$ terraform init -reconfigure -backend-config=../../backend.hcl -backend-config="key=pr1111/terraform.tfstate"
[/simterm]
Тепер деплоїмо з -var або передаємо через змінні як TF_VAR_vpc_cidr, див. Environment Variables – в пайплайні це можна досить просто зробити:
[simterm]
$ terraform apply -var vpc_cidr=10.0.3.0/24 -var environment=pr1111
[/simterm]
Перевіряємо стейти – маємо новий каталог pr1111:
[simterm]
$ aws s3 ls tf-state-bucket-envs-management-test/
PRE dev/
PRE global/
PRE pr1111/
[/simterm]
Готово.
Корисні посилання
- How to manage Terraform state
- Terraform manage multiple environments
- How to Manage Multiple Terraform Environments Efficiently
- How to manage multiple environments with Terraform using workspaces
- How to manage multiple environments with Terraform using branches
- How to manage multiple environments with Terraform using Terragrunt
- і трохи не по темі, але цікаво – Terraform: Destroy / Replace Buckets

