Треба запланувати використання Terraform у новому проекті, а це включає в себе і планування структри файлів для проекті, і як створити бекенд (тобто bootstrap) і інші потрібні для початку роботи ресурси, і подумати на тему роботи з кількома оточеннями і AWS-аккаунтами.
Взагалі, цей пост спочатку писався чисто про створення AWS SES, але я почав додавати багато деталей по тому, як жеж саме створити та планувати новий проект з Terraform, тож вирішив винести окремим постом. Про SES пізніше теж допишу, бо там досить цікаво саме по самому SES та пошті в цілому.
Наприкінці цього поста, як завжди, буде багато цікавих посилань, але особливо хочу відмітити Terraform Best Practices від Anton Babenko.
Зміст
Планування Terraform для нового проекту
Про що треба буде подумати?
- структура файлів проектів
- бекенд – AWS S3, як робити корзину для першого запуску?
- гарно б DynamoDB для State Locking, але то іншим разом
- dev/prod оточення та aws multi account – як робитимо?
Структура файлів Terraform
У проекті для AWS SES спочатку зробив все у одному файлі, але давайте зробимо “як треба”, див. наприклад How to Structure Your Terraform Projects (там ще багато чього).
Отже, як організуємо:
main.tf
– виклики модулівterraform.tf
– параметри backend-у, провайдери, версіїproviders.tf
– тут сам провайдер AWS, аутентифікаця, регіонvariables.tf
– тут декларуємо змінніterraform.tfvars
– значення для змінних
У проекті SES ще будуть окремі файли ses.tf
та route53.tf
для всього, що пов’язано з ними.
Multiple environments з Terraform
Окей, а що маємо по роботі з декількома оточеннями типу Dev/Prod, або взагалі різними AWS-аккаунтами?
Можемо зробити через Terraform Workspaces, але щось я не пам’ятаю, щоб багато чув про них в плані використання для Dev/Prod та ще й з CI/CD пайплайнами. Хоча – як варіант, і може має сенс якось спробувати. Див. How to manage multiple environments with Terraform using workspaces).
Взагалі-то не знайшов якось “золотої кулі”, і варіантів прям купа. Хтось використовує git-бранчі або Terragrunt (How to manage multiple environments with Terraform), хтось – різні директорії (How to Create Terraform Multiple Environments), хтось – рішення типу Spacelift.
Dev та Production по каталогах
Як на мене, для невеликого проекту найбільш привабливим виглядає варіант з використанням декільких калалогів для оточень і Terraform modules для ресурсів.
Спробуємо, щоб побачити, як воно все виглядатиме та працюватиме.
У каталозі проекту створимо структуру директорій:
[simterm]
$ mkdir -p environments/{dev,prod} $ mkdir -p modules/vpc
[/simterm]
В результаті маємо таке:
[simterm]
$ tree . |-- environments | |-- dev | `-- prod `-- modules `-- vpc
[/simterm]
Тут у нас environments/dev/
та prod/
будуть незалежними проектами з власними параметрами та будуть використовувати загальні модулі з каталогу modules
. Таким чином процес розробки чогось нового для інфрастуктури можна спочатку протестити у окремому файлі в каталозі environments/dev
, потім перенести його до modules
, додати до dev
вже у вигляді модулю, і після повторного тестування там – додати до production
.
Крім того, так як будемо мати власні файли параметрів для AWS, то зможемо використовувати окремі AWS-аккаунти.
Поки що руками створимо корзину для стейтів – дійдемо до цього пізніше, як будемо говорити про Bootstrap:
[simterm]
$ aws s3 mb s3://tfvars-envs make_bucket: tfvars-envs
[/simterm]
Створення shared-модулю
Переходимо до каталога modules/vpc/
і у файлі main.tf
описуємо VPC (втім, якщо вже дотримуватися best practicies, то краще використовувати модуль VPC, також від Anton Babenko):
resource "aws_vpc" "env_vpc" { cidr_block = var.vpc_cidr tags = { environment = var.environment } }
В тому ж каталозі створюємо файл variables.tf
зі змінними, але без значень – тільки декларуємо їх:
variable "vpc_cidr" { type = string } variable "environment" { type = string }
Створення Dev/Prod оточень
Переходимо до environments/dev
і готуємо файли. Почнемо з параметрів – terraform.tf
та provider.tf
.
У terraform.tf
описуємо потрібні провайдери, версії та бекенд.
У бекенді у key
вказуємо шлях до стейт-файлу в директорії dev/
– її буде створено при деплої. А для Prod – вкажемо prod/
(хоча можна взагалі різні корзини):
terraform { required_providers { aws = { source = "hashicorp/aws" version = ">= 4.6.0" } } required_version = ">= 1.4" backend "s3" { bucket = "tfvars-envs" region = "eu-central-1" key = "dev/terraform.tfstate" } }
У provider.tf
параметри провайдера AWS – регіон та AWS profile з ~/.aws/config
, який буде використовуватись:
provider "aws" { region = var.region profile = "default" }
Хоча можна зараз було б об’єднати їх у terraform.tf
, але на майбутнє – нехай буде так.
Створюємо main.tf
, в якому використовуємо наш модуль з каталогу modules
, якому передаємо змінні:
module "vpc" { source = "../../modules/vpc" vpc_cidr = var.vpc_cidr environment = var.environment }
Додаємо файл variables.tf
, в якому також тільки декларуємо змінні, тут нова змінна region
для terraform.tf
та providers.tf
:
variable "vpc_cidr" { type = string } variable "environment" { type = string } variable "region" { type = string }
І нарешті самі значення змінних описуємо у файлі terraform.tfvars
:
vpc_cidr = "10.0.0.0/24" environment = "dev" region = "eu-central-1"
Аналогічно робимо для environments/prod/
, тільки з каталогом prod
/ у бекенді та іншими значеннями у terraform.tfvars
:
vpc_cidr = "10.0.1.0/24" environment = "prod" region = "eu-central-1"
Отримуємо таку структуру:
[simterm]
$ tree . |-- environments | |-- dev | | |-- main.tf | | |-- provider.tf | | |-- terraform.tf | | |-- terraform.tfvars | | `-- variables.tf | `-- prod | |-- main.tf | |-- provider.tf | |-- terraform.tf | |-- terraform.tfvars | `-- variables.tf `-- modules `-- vpc |-- main.tf `-- variables.tf
[/simterm]
Перевіримо наш Dev – виконуємо init
:
[simterm]
$ terraform init Initializing the backend... Successfully configured the backend "s3"! Terraform will automatically use this backend unless the backend configuration changes. Initializing modules... Initializing provider plugins... - Reusing previous version of hashicorp/aws from the dependency lock file - Installing hashicorp/aws v4.67.0... - Installed hashicorp/aws v4.67.0 (signed by HashiCorp) Terraform has been successfully initialized!
[/simterm]
І plan
:
[simterm]
$ terraform plan ... Terraform will perform the following actions: # module.vpc.aws_vpc.env_vpc will be created + resource "aws_vpc" "env_vpc" { + arn = (known after apply) + cidr_block = "10.0.0.0/24" ... + tags = { + "environment" = "dev" } + tags_all = { + "environment" = "dev" } } Plan: 1 to add, 0 to change, 0 to destroy.
[/simterm]
Окей – можна створювати ресурси, але як щодо корзини tfvars-envs
, яку ми вказали у backend
? Якщо ми спробуємо виконати apply
зараз, то деплой сфейлиться, бо бакету для бекенду нема.
Тобто – як взагалі підготувати AWS аккаунт до використання Terraform, виконати його bootstraping?
Terraform Backend Bootstrap
Тобто, маємо новий проект, новий аккаунт, і нам десь треба зберігати state-файли. Ми будемо використовувати AWS S3, а потім ще додамо DynamoDB для state-lock, але й корзина, і таблиця в DynamoDB мають бути створені до деплою нового проекту.
Поки бачу три основних варіанти:
- “clickops”: все створюємо руками через AWS Console
- скриптом або вручну створювати через AWS CLI
- рішення по типу Terragrunt або Terraform Cloud, але це поки овер-інжинірінг для такого маленького проекту
- мати окремий проект з Terraform, назвемо його bootstrap, в якому створюються ресурси і стейт-файл, який потім імпортуємо в новий бекенд
Ще з найдених варіантів – використовувати CloudFormation для цього – How to bootstrap an AWS account with Terraform state backend, але таке собі – не хочеться змішувати кілька orchestration management тулів.
Ще нагуглив рішення з Makefile – Terrastrap – Bootstrap a S3 and DynamoDB backend for Terraform, цікава реалізація.
До речі, якщо маєте GitLab – то в нього є свій бекенд для Terraform-state, див. GitLab-managed Terraform state, і в такому випадку нічого створювати не потрібно (хіба що DynamoDB та AIM, але питання зі стейтами вирішується).
В принципі якщо питання просто створити корзину, то можна й AWS CLI, але як бути, коли планується і S3, і DynamoDB, та ще й окремий IAM юзер для Terraform з власною IAM Policy? Все робити через AWS CLI? І це повторювати для всіх нових проектів вручну? Ну, таке собі.
Перше рішення, яке придумалось – це мати єдиний bootstrap-проект, в якому ми будемо створювати ресурси для всіх іншних проектів, тобто – всі корзини/Dynamo/IAM, просто через різні tfvars
– можна було б організувати щось накташлт рішення з Dev/Prod оточеннями, як робили вище. Тобто у репозиторії з bootstrap-проектом мати окремі директорії з власними файлами terraform.tf
, provider.tf
та terraform.tfvars
під кожен новий проект.
В такому випадку можна руками з AWS CLI створити перший бакет для самого проекту bootstrap, і вже в цьому проекті описуємо створення DynamoDB, S3-бакетів, IAM-ресурсів для інших проектів.
Для проекту bootstrap для аутентифікації можна взяти якісь існуючі ACCESS/SECRET ключі, а інші проекти вже зсможуть використовувати IAM юзера або роль, яку ми створимо у бутстрапі.
Виглядає наче робочою ідею, але є ще один варіант – використовувати каталог/репозиторій bootstrap як модуль в кожному проекті, і створювати ресурси перед запуском проекту.
Тобто:
- модуль bootsrap – його зберігаємо в репозиторії для доступу з інших проектів
- потім при створенні нового проекту – включаємо цей модуль в код, за його допомогою створюємо S3-бакет, AIM та DynamoDB
- після створення – імпортуємо state-файл, який отримали після бутстрапу, в нову корзину
- і вже тоді починаємо роботу з оточеннями
Спробуємо – як на мене, то цей варіант виглядає непогано.
Видаляємо корзину, яку створили на початку, вона має бути порожня, бо з terraform apply
ми нічого не створювали:
[simterm]
$ aws s3 rb s3://tfvars-envs remove_bucket: tfvars-envs
[/simterm]
Створення Bootstrap модулю
Створюємо репозиторій, і в ньому файл s3.tf
з aws_s3_bucket
– поки обійдемось без IAM/Dynamo, тут чисто для прикладу і перевірки плану:
resource "aws_s3_bucket" "project_tfstates_bucket" { bucket = var.tfstates_s3_bucket_name tags = { environment = "ops" } } resource "aws_s3_bucket_versioning" "project_tfstates_bucket_versioning" { bucket = aws_s3_bucket.project_tfstates_bucket.id versioning_configuration { status = "Enabled" } }
Додаємо variables.tf
з ім’ям корзини:
variable "tfstates_s3_bucket_name" { type = string }
Тепер повертаємось до нашого проекту, і в корні створюємо файл main.tf
, в якому використовєємо bootstrap модуль з Github:
module "bootstrap" { source = "[email protected]:setevoy2/terraform-bootsrap.git" tfstates_s3_bucket_name = var.tfstates_s3_bucket_name }
У source
, до речі, можемо задати бранч або версію, наприклад:
source = "[email protected]:setevoy2/terraform-bootsrap.git?ref=main
Далі, додаємо файл variables.tf
:
variable "tfstates_s3_bucket_name" { type = string } variable "region" { type = string }
Файл provider.tf
:
provider "aws" { region = var.region profile = "default" }
І terraform.tf
:
terraform { required_providers { aws = { source = "hashicorp/aws" version = ">= 4.6.0" } } required_version = ">= 1.4" # backend "s3" { # bucket = "tfvars-envs" # region = "eu-central-1" # key = "bootstrap/terraform.tfstate" # } }
Тут блок backend
поки закоментований – повернемось до нього, як створимо корзину, поки що state file буде згенеровано локально. У key
вказуємо шлях bootstrap/terraform.tfstate
– саме туди буде імпортовано наш стейт.
Додаємо файл terraform.tfvars
:
tfstates_s3_bucket_name = "tfvars-envs" region = "eu-central-1"
Тепер структура виходить така:
[simterm]
$ tree . |-- environments | |-- dev | | |-- main.tf | | |-- provider.tf | | |-- terraform.tf | | |-- terraform.tfvars | | `-- variables.tf | `-- prod | |-- main.tf | |-- provider.tf | |-- terraform.tf | |-- terraform.tfvars | `-- variables.tf |-- main.tf |-- modules | `-- vpc | |-- main.tf | `-- variables.tf |-- provider.tf |-- terraform.tf |-- terraform.tfvars `-- variables.tf
[/simterm]
Тобто, в корні проекту у main.tf
ми виконуємо тільки бутстрап для створення корзини, а потім вже з каталогів environments/{dev,prod}
створюємо ресурси інфрастуктури.
Створення Bootstrap S3-корзини
У корні виконуємо terraform init
:
[simterm]
$ terraform init Initializing the backend... Initializing modules... Downloading git::ssh://[email protected]/setevoy2/terraform-bootsrap.git for bootstrap... - bootstrap in .terraform/modules/bootstrap Initializing provider plugins... - Finding hashicorp/aws versions matching ">= 4.6.0"... - Installing hashicorp/aws v4.67.0... - Installed hashicorp/aws v4.67.0 (signed by HashiCorp) ...
[/simterm]
Перевіряємо з terraform plan
, і як все гаразд – то запускаємо створення корзини:
[simterm]
$ terraform apply ... # module.bootstrap.aws_s3_bucket.project_tfstates_bucket will be created + resource "aws_s3_bucket" "project_tfstates_bucket" { ... module.bootstrap.aws_s3_bucket_versioning.project_tfstates_bucket_versioning: Creation complete after 2s [id=tfvars-envs] Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
[/simterm]
Тепер наступний крок – імпортувати локальний state-файл:
[simterm]
$ head -5 terraform.tfstate { "version": 4, "terraform_version": "1.4.6", "serial": 4, "lineage": "d34da6b7-08f4-6444-1941-2336f5988447",
[/simterm]
Розкоментуємо блок backend
у terraform.tf
рутового модулю:
terraform { required_providers { aws = { source = "hashicorp/aws" version = ">= 4.6.0" } } required_version = ">= 1.4" backend "s3" { bucket = "tfvars-envs" region = "eu-central-1" key = "bootstrap/terraform.tfstate" } }
І визиваємо terraform init
ще раз – тепер він бачить, що замість local backend має s3 backend, і запропонує мігрувати terraform.tfstate
туди – відповідаємо yes:
[simterm]
$ 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 Successfully configured the backend "s3"! Terraform will automatically use this backend unless the backend configuration changes. Initializing modules... Initializing provider plugins... - Reusing previous version of hashicorp/aws from the dependency lock file - Using previously-installed hashicorp/aws v4.67.0 Terraform has been successfully initialized!
[/simterm]
Тепер маємо налаштований бекенд, котрий можемо використовувати для проекту.
Повертаємось до environments/dev/
, перевіряємо ще раз з plan
, і нарешті створимо наше Dev-оточення:
[simterm]
$ terraform apply ... module.vpc.aws_vpc.env_vpc: Creating... module.vpc.aws_vpc.env_vpc: Creation complete after 2s [id=vpc-0e9bb9408db6a2968] Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
[/simterm]
Перевіряємо корзину:
[simterm]
$ aws s3 ls s3://tfvars-envs PRE bootstrap/ PRE dev/
[/simterm]
І файл стейту:
[simterm]
$ aws s3 ls s3://tfvars-envs/dev/ 2023-05-14 13:37:27 1859 terraform.tfstate
[/simterm]
Все є.
Отже, процесс створення нового проекту буде таким:
- у корні проекту створюємо
main.tf
, в якому описуємо використання модулю bootstrap зsource = "[email protected]:setevoy2/terraform-bootsrap.git
- у файлі
terraform.tf
описуємо бекенд, але коментуємо його - створюємо корзину з модулю
bootsrap
- розкоментуємо бекенд, і через
terrafrom init
імпортуємо локальний state file
Після цього проект готовий до створення dev/prod оточень з бекендом для стейт-файлів у новій корзині.
І да, таки цікаво які тули/підходи використовуєте, тож велкам у коменти або чатик RTFM у Telegram.
Посилання по темі
- Terraform Best Practices
- 20 Terraform Best Practices to Improve your TF workflow
- How to manage multiple environments with Terraform using workspaces
- How to Create Terraform Multiple Environments
- How to manage multiple environments with terraform with the use of modules?
- An Intro to Bootstrapping AWS to Your Terraform CI/CD
- Terraform .tfvars files: Variables Management with Examples