Terraform: початок роботи та планування нового проекту – Dev/Prod та bootsrap

Автор |  14/05/2023
 

Треба запланувати використання 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]

Все є.

Отже, процесс створення нового проекту буде таким:

  1. у корні проекту створюємо main.tf, в якому описуємо використання модулю bootstrap з source = "[email protected]:setevoy2/terraform-bootsrap.git
  2. у файлі terraform.tf описуємо бекенд, але коментуємо його
  3. створюємо корзину з модулю bootsrap
  4. розкоментуємо бекенд, і через terrafrom init імпортуємо локальний state file

Після цього проект готовий до створення dev/prod оточень з бекендом для стейт-файлів у новій корзині.

І да, таки цікаво які тули/підходи використовуєте, тож велкам у коменти або чатик RTFM у Telegram.

Посилання по темі