Виходить такая собі серія постів про підготовку до використання 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 оточенням з окремими директоріями
Як все може виглядати з окремими каталогами?
Можемо створити структуру:
global
main.tf
: створення ресурсів для бекенду – S3, Dynamo
environments
dev
main.tf
: тут включаємо потрібні модулі (дублються з Prod, але відрізняється під час розробки та тестування нового модулю)variables.tf
: декларуємо змінні, загальні (дублюються з Prod) та специфічні до оточенняterraform.tfvars
: значення змінних, загальні (дублюються з Prod) та специфічні до оточенняproviders.tf
: налаштування підключення до AWS/Kubernetes, специфічні до оточення (осолибво корисно, коли Dev/Prod це різні акаунти AWS)backend.tf
: налаштування зберігання state-файлів, специфічні до оточення
prod
- <аналогічно Dev>
modules
vpc
main.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