Тепер, як маємо готовий код для розгортання кластеру AWS Elastic Kubernetes Service (див. Terraform: створення EKS, частина 1 – VPC, Subnets та Endpoints і наступні частини), прийшов час подумати про автоматизацію, тобто – про створення пайплайнів в CI/CD, які би виконували створення нових енвів для тестування фіч, або деплоїли апдейти на Dev/Prod оточення Kubernetes.
І тут знову поговоримо про менеджмент Dev/Prod оточень з Terraform.
В попередніх записах я перебирав варіанти з Terraform Workspaces, Git-бранчами та розділенням по директоріям (див. Terraform: початок роботи та планування нового проекту – Dev/Prod та bootsrap та Terraform: динамічний remote state з AWS S3 та multiple environments по директоріям), але поки писав код Terraform, то подумав про те, як зазвичай робиться з Helm-чартами: весь код чарту зберігається в корні репозиторію або окремій директорії, а значення для Dev/Prod передаються з різних файлів values.yaml
.
То чому б не спробувати аналогічний підхід з Terraform?
В будь-якому разі те, що описано в цьому пості не треба сприймати як приклад “це робиться саме так”: тут я більше експерементую і пробую можливості GitHub Actions та Terraform. А в якому вигляді воно піде в продакшен – подивимось.
Проте вийшло доволі цікаво.
Зміст
Terraform Dev/Prod – Helm-like “flat” approach
Отже, конфігурація для Terraform може бути такою:
- аутентифікацію та атворизацію робимо локально зі змінною
AWS_PROFILE
, в якому виконується AssumeRole, а в GitHub Actions – через OIDC і AssumeRole – таким чином зможемо мати різні параметри для AWS Provider для Dev та Prod оточень - весь код Terraform зберігається в корні
tfvars
для Dev/Prod в окремих директоріяхenvs/dev
таenvs/prod
- загальні параметри
backend
описуємо у файліbackend.hcl
, а ключі і таблицю Дінамо передаємо через-backend-config
А флоу розробки та деплою – таким:
- бранчуємось від мастер-бранчу
- робимо зміни в коді, тестуємо локально або через GitHub Actions, передаючи потрібні параметри через
-var
(ім’я енву, якісь версії і т.д.) - як закінчили розробку і тести – пушимо в репозиторій, і робимо Pull Request
- при створенні Pull Request можемо задати спеціиальну лейблу, і тоді GitHub Actions виконують деплой на feature-енв, тобто створюється тимчасова інфрастуктура в AWS
- Dev-оточення, щоб перевірити, що все працює
- після мержу Pull Request можемо додатково задеплоїти на Dev-оточення, щоб перевірити, що зміни між строю версією та новою працюють (такий собі staging env)
- якщо треба задеплоїти на Прод – створюємо Git Release
- вручну або автоматично тригерим GitHub Actions на деплой з цим релізом
- …
- profit!
А що там Terragrunt?
Звісно, для управління оточеннями і бекендами можна було б просто взяти Terragrunt, але я поки не впевнений в тому, яка модель управління інфрастуктурою з Terraform складеться на проекті: якщо цим будуть займатись виключно девопси – то, скоріш за все, візьмемо Terragrunt. Якщо діло зайде і девелоперам, і вони захочуть самі щось менеджити – то, можливо, для управління візьмемо якесь рішення на кштал Atlantis або Gaia.
Тому поки це все тільки починається – робимо все максимально просто і “ванільним” Terraform, а далі подивимось, що і як буде краще.
Terraform layout for multiple environments
Зараз структура файлів і каталогів виглядає так – в environments/dev
весь код для Dev, в environments/prod
– весь код для Prod (поки пусто):
$ tree terraform/ terraform/ └── environments ├── dev │ ├── backend.tf │ ├── configs │ │ └── karpenter-provisioner.yaml.tmpl │ ├── controllers.tf │ ├── eks.tf │ ├── iam.tf │ ├── karpenter.tf │ ├── main.tf │ ├── outputs.tf │ ├── providers.tf │ ├── terraform.tfvars │ ├── variables.tf │ └── vpc.tf └── prod
Однак так як ми тут не використвуємо модулі, то код дуже дублюється, і менеджити його буде важко і як то кажусь, “error prone”.
Можна було б створити модулі, але тоді були б модулі в модулях, бо весь EKS-стек створюється через модулі – VPC, EKS, Subnets, IAM, etc, і ускладнювати код, виносячи код в модулі поки що не хочеться, бо знов-таки – подивимось, як підуть процеси. Поки це все на самому початку – то маємо змогу досить швидко переробити так, як потім буде краще в залежності від того, яку модель і систему управління оточеннями оберемо.
Тож що нам треба зробити:
- перенести файли
.tf
в корень директоріїterraform
- оновити файл
providers.tf
– видалити з ньогоassume_role
та прибрати--profile
вargs
провайдерів Kubernetes, Helm та kubectl - створити файл
backend.hcl
для common-конфігурації - у файлі
backend.tf
лишити тількиbackend "s3"
environments
перейменуємо вenvs
- всередені будуть каталоги
dev
таprod
, в яких будуть зберігатисьterraform.tfvars
для кожного енва
- всередені будуть каталоги
Providers
providers.tf
зараз виглядає так:
provider "aws" { region = var.aws_region assume_role { role_arn = "arn:aws:iam::492***148:role/tf-admin" } default_tags { tags = { component = var.component created-by = "terraform" environment = var.environment } } }
Прибираємо з нього assume_role
:
provider "aws" { region = var.aws_region default_tags { tags = { component = var.component created-by = "terraform" environment = var.environment } } }
В інші провайдерах – Kubernetes, Helm та Kubectl – в args
прибираємо --profile
:
... provider "kubernetes" { host = module.eks.cluster_endpoint cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data) exec { api_version = "client.authentication.k8s.io/v1beta1" command = "aws" args = ["eks", "get-token", "--cluster-name", module.eks.cluster_name] } } ...
Versions
Версії виносимо в окремий файл vesrions.tf
:
terraform { required_version = "~> 1.5" required_providers { aws = { source = "hashicorp/aws" version = "~> 5.14" } kubernetes = { source = "hashicorp/kubernetes" version = "~> 2.23" } helm = { source = "hashicorp/helm" version = "~> 2.11" } kubectl = { source = "gavinbunney/kubectl" version = "~> 1.14" } } }
Переносимо файли, і тепер структура виглядає так:
$ tree . ├── backend.tf ├── configs │ └── karpenter-provisioner.yaml.tmpl ├── controllers.tf ├── eks.tf ├── iam.tf ├── karpenter.tf ├── main.tf ├── outputs.tf ├── providers.tf ├── variables.tf ├── envs │ ├── dev │ │ └── dev.tfvars │ └── prod ├── versions.tf └── vpc.tf
Backend
В корні створюємо файл backend.hcl
:
bucket = "tf-state-backend-atlas-eks" region = "us-east-1" dynamodb_table = "tf-state-lock-atlas-eks" encrypt = true
У файлі backend.tf
залишаємо тільки s3
:
terraform { backend "s3" {} }
Окей – наче все готово? Давайте тестити.
Тестування з terraform init
&& plan
для різних оточень
Маємо AWS Profile в ~/.aws/config
:
[profile work] region = us-east-1 output = json [profile tf-assume] role_arn = arn:aws:iam::492***148:role/tf-admin source_profile = work
Перевіряємо існуючий ключ в S3 – бо вже маємо задеплоїний Dev:
$ aws s3 ls tf-state-backend-atlas-eks/dev/ 2023-09-14 15:49:15 450817 atlas-eks.tfstate
Задаємо змінну AWS_PROFILE
, і пробуємо terraform init
з backend-config
:
$ export AWS_PROFILE=tf-admin $ terraform init -backend-config=backend.hcl -backend-config="key=dev/atlas-eks.tfstate" Initializing the backend... Successfully configured the backend "s3"! Terraform will automatically ... Terraform has been successfully initialized!
Наче ОК – модулі загрузились, існуючий стейт-файл знайшло, все гуд.
Пробуємо terraform plan
– в коді нічого не мінялось, тож нічого не має змінитись і в AWS:
$ terraform plan -var-file=envs/dev/dev.tfvars ... Terraform will perform the following actions: # helm_release.karpenter will be updated in-place ~ resource "helm_release" "karpenter" { id = "karpenter" name = "karpenter" ~ repository_password = (sensitive value) # (28 unchanged attributes hidden) # (6 unchanged blocks hidden) } Plan: 0 to add, 1 to change, 0 to destroy. ...
Найс! Змінить тільки пароль для AWS ECR, це ОК, бо там токен, що міняється.
Спробуємо зробити теж саме, але для Prod – копіюємо файл dev.tfvars
як prod.tfvars
в каталог envs/prod
, міняємо в ньому данні – тільки VPC CIDR та environment
:
project_name = "atlas-eks" environment = "prod" component = "devops" eks_version = "1.27" vpc_params = { vpc_cidr = "10.2.0.0/16" enable_nat_gateway = true ...
І пробуємо новий terraform init
з -reconfigure
, бо робимо це все локально. В backend-config
передаємо новий ключ для стейт-файлу в S3:
$ terraform init -reconfigure -backend-config=backend.hcl -backend-config="key=prod/atlas-eks.tfstate" Initializing the backend... Successfully configured the backend "s3"! Terraform will automatically ... Terraform has been successfully initialized!
Перевіряємо з terraform plan
:
$ terraform plan -var-file=envs/prod/prod.tfvars ... Plan: 109 to add, 0 to change, 0 to destroy. ...
Добре! Збирається створити купу нових ресурсів, бо Prod ще не робив, тобто все, як задумано.
Для feature-енвів все теж саме, тільки в -var
передаємо ще environment
і в backend-config
задаємо новий ключ по імені енва:
$ terraform init -reconfigure -backend-config=backend.hcl -backend-config="key=feat111/atlas-eks.tfstate" -var="environment=feat111" ...
Для plan
та apply
використовуємо параметри з файлу dev.tfvars
, бо фіча-енви в принципі більш-менш ~= dev-оточенню:
$ terraform plan -var-file=envs/dev/dev.tfvars -var="environment=feat111" ...
Якщо потрібно задати наприклад нову VPC, яка у нас object, в якому є string vpc_cidr
:
... variable "vpc_params" { description = "AWS VPC for the EKS cluster parameters. Note, that this VPC will be environment-wide used, e.g. all Dev/Prod AWS resources must be placed here" type = object({ vpc_cidr = string enable_nat_gateway = bool one_nat_gateway_per_az = bool single_nat_gateway = bool enable_vpn_gateway = bool enable_flow_log = bool }) } ...
Тож її передаємо як -var='vpc_params={vpc_cidr="10.3.0.0/16"}'
. Але в такому випадку потрібно передавати всі значення об’єкту vpc_params
, тобто:
$ terraform plan -var-file=envs/dev/dev.tfvars -var="environment=feat111" \ > -var='vpc_params={vpc_cidr="10.3.0.0/16",enable_nat_gateway=true,one_nat_gateway_per_az=true,single_nat_gateway=false,enable_vpn_gateway=false,enable_flow_log=false}'
Окей – з цим розібрались, можна переходити до GitHub Actions.
Авторизація і аутентифікація – OIDC та IAM
Для доступу з GitHub Actions до AWS ми використовуємо OpenID Connect – тут GitHub виступає в ролі Identity Provider (IDP), а AWS – Service Prodider (SP), тобто на GitHub ми проходимо аутентифікацю, після чого GitHub “передає” нас до AWS, кажучи, що “це дійсно Вася Пупкін”, а вже AWS виконує авторизацію – тобто, AWS перевіряє, чи може цей Вася Пупкін створювати нові ресурси.
Для авторизації в AWS ми в Terraform виконуємо IAM Role Assume, і вже від імені цієї ролі і IAM Policy, які підключені до неї, ми проходимо авторизацію.
Щоб мати можливість виконувати AsuumeRole цієї ролі використовуючи GitHub Identity Provider – треба змінити її Trusted Policy, бо зараз вона дозволяє AssumeRole тільки для юзерів з AWS акаунту:
{ "Version": "2012-10-17", "Statement": [ { "Sid": "Statement1", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::492***148:root" }, "Action": "sts:AssumeRole" } ] }
Документація – Configuring the role and trust policy.
Додаємо token.actions.githubusercontent.com
IDP з нашого AWS АІМ:
{ "Version": "2012-10-17", "Statement": [ { "Sid": "AccountAllow", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::492***148:root" }, "Action": "sts:AssumeRole" }, { "Sid": "GitHubAllow", "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::492***148:oidc-provider/token.actions.githubusercontent.com" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" } } } ] }
GitHub Actions: recall the moment!
Колись писав про них – Github: обзор Github Actions и деплой с ArgoCD, але то було давно, та й сам я Actions майже не користувався, то треба трохи згадати що до чього.
Див. також Understanding GitHub Actions.
Отже, що маємо:
- є
events
, як тригерятьjobs
- в
jobs
маємоsteps
з одним чи кількомаactions
actions
виконують конкретні дії, іactions
однієїjob
мають запускатись на одному GitHub Actions Runner
При цьому в Actions можемо використовувати готові “бібліотеки” з Github Actions Marketplace.
GitHub Environments
GitHub дозволяє налаштувати декілька оточень для роботи, і в кожному мати свій набір Variables та Secrets. Крім того, для них можна налаштувати правила, згідно з якими має відбуватись деплой. наприклад – дозволити деплой на Prod тільки з master-гілки.
Див. Using environments for deployment.
Тож що ми можемо зробити:
- Dev env зі своїми змінними
- Prod env зі свіоми змінними
- і створювати feature-енви динамічно під час деплою
GitHub Actions Reusable Workflow та Composite Actions
З Reusable Workflow ми можемо використати вже описаний Workflow в іншому workflow, а з Composite Actions – створити власну Action, яку потім включимо в steps наших workflow-файлів.
Детальніше про різницю між ними можна почитати тут – GitHub: Composite Actions vs Reusable Workflows [Updated 2023] і чудовий пост Github Not-So-Reusable Actions, але якщо кратко, то:
- при створенні Workflow ви групуєте декілька Jobs в єдиний файл і використовувати тільки в Jobs, тоді як в Actions будуть тільки steps, які групуються в єдиний Action, який потім можна використовувати тільки як step
- Workflow не можуть викликати інші Workflow, тоді як в Actions ви можете викликати інші Actions
Крім того, замість Reusable Workflow можна використати Running a workflow based on the conclusion of another workflow – тобто тригерити запуск воркфлоу після завершення роботи іншого воркфлоу.
Створення тестового workflow для Terraform validate && lint
Спочатку давайте зробимо якийсь мінімальний workflow, щоб подивитись як воно все взагалі працює.
Див. Quickstart for GitHub Actions, документація по сінтаксісу – Workflow syntax for GitHub Actions, і по permissions нашої джоби – Assigning permissions to jobs.
Для Terraform будемо використовувати setup-terraform
Action.
Для логіну в AWS – configure-aws-credentials
Action.
В репозиторії створюємо директорію:
$ mkdir -p .github/workflows
І в ній створюємо файл воркфлоу – testing-terraform.yaml
.
Так як код Terraform лежить в каталозі terraform
, то в workflow додамо defaults.run.working-directory
та умову on.push.paths
, тобто трігерити білд тільки якщо зміни відбулися в каталозі terraform
, див. paths
.
Описуємо флоу:
name: Test Terraform changes defaults: run: working-directory: terraform on: workflow_dispatch: push: paths: - terraform/** env: ENVIRONMENT : "dev" AWS_REGION : "us-east-1" permissions: id-token: write contents: read
Тут:
- в
defaults
задаємо з якої директорії будуть виконуватись всі степи - в
on
– умови, по пушу і тільки для директоріїterraform
і додатковоworkflow_dispatch
для можливості ручного запуску - в
env
– значення змінних оточення - в
permissions
– права джоби на створення токену.
В джобах ми будемо виконувати AssumeRole, давайте її занесемо в змінні оточення репозиторію.
Переходимо в Setting > Secrets and variables > Actions > Variables, і додаємо нову змінну:
Далі, подумаємо які степи в джобі нам треба виконати:
git checkout
- логін в AWS
terraform fmt -check
для перевірки “красоти коду” (див.fmt
)terraform init
з Dev-оточенням – загрузити модулі і підключитись до state-бекендуterraform validate
для перевірки синтаксису коду (див.validate
)
Додаємо джобу з цими степами, тепер файл повністю буде таким:
name: Test Terraform changes defaults: run: working-directory: terraform on: workflow_dispatch: push: paths: - terraform/** env: ENVIRONMENT : "dev" AWS_REGION : "us-east-1" permissions: id-token: write contents: read jobs: test-terraform: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: hashicorp/setup-terraform@v2 - name: Terraform fmt run: terraform fmt -check continue-on-error: true - name: "Setup: Configure AWS credentials" # this step adds `env.AWS_*` variables uses: aws-actions/configure-aws-credentials@v2 with: role-to-assume: ${{ vars.TF_AWS_ROLE }} role-session-name: github-actions-terraform role-duration-seconds: 7200 aws-region: ${{ env.AWS_REGION }} - name: Terraform Init run: terraform init -backend-config=backend.hcl -backend-config="key=${{ env.ENVIRONMENT }}/atlas-eks.tfstate" - name: Terraform Validate run: terraform validate
Пушимо зміни в репозиторій, запускаємо вручну, бо змін в коді Terraform не було:
І маємо наш перший ран:
Note: аби з’явилась кнопка для ручного запуску (workflow_dispatch
) – зміни в workflow-файлі мають бути змержені з дефолтним бранчем репозиторія
GitHub Actions TFLint step
Тепер давайте додамо степ з Terraform linter – TFLint.
Спочатку глянемо, як воно працює локально:
$ yay -S tflint $ tflint 3 issue(s) found: Warning: module "ebs_csi_irsa_role" should specify a version (terraform_module_version) on iam.tf line 1: 1: module "ebs_csi_irsa_role" { Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.1.1/docs/rules/terraform_module_version.md Warning: module "karpenter" should specify a version (terraform_module_version) on karpenter.tf line 1: 1: module "karpenter" { Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.1.1/docs/rules/terraform_module_version.md
Слушні зауваження – забув додати версії модулів, треба пофіксити.
Далі, додамо його на нашого воркфлоу – використаємо marketplace/actions/setup-tflint
, версію беремо на сторінці релізів TFLint:
... - name: Setup TFLint uses: terraform-linters/setup-tflint@v3 with: tflint_version: v0.48.0 - name: Terraform lint run: tflint -f compact
Запускаємо, і маємо сфейлений білд:
Добре.
Фіксимо проблеми, і йдемо далі до деплоїв.
Планування деплоїв з GitHub Actions
Тепер можна подумати про те, як нам побудувати workflow.
Які ми маємо дії в GitHub-репозиторії, і які дії з Terraform нам можуть знадобитись?
В GitHub:
- Push в будь-який бранч
- створення Pull Request на merge в master
- закриття Pull Request на merge в master
З Terraform:
- тестування:
fmt
,validate
,lint
- деплой:
plan
таapply
- дестрой:
plan
таdestroy
І як ми можемо це все скомбінувати в різні workflow?
- при Push – виконувати тест
- при створенні PR в мастер з лейблою “deploy” – деплоїти feature-енв (тест + деплой)
- при закритті PR – видаляти feature-енв (дестрой)
- вручну мати можливість:
- задеплоїти на Dev будь-який бранч або реліз (тест + деплой)
- задеплоїти на Prod реліз з мастер-бранчу (тест + деплой)
Тобто виглядає, як п’ять воркфлоу, при чому майже в усіх маємо дії, які повторюються – логін, init та test, тож їх має сенс винести в окремі “функції” – використати Reusable Workflow або Composite Actions.
AWS Login та init
можемо об’єднати, і додатково створити “функцію” test. Крім того – додати “функції” “deploy” та “destroy”, і потім “модульно” їх використовувати у наших workflow:
- terraform-init:
- AWS Login
- terraform init
- test:
-
terraform validate
-
terraform fmt
-
tflint
-
- deploy:
- terraform plan
- terraform apply
- destroy:
- terraform plan
- terraform destroy
Тож якщо планувати з п’ятьма окремими воркфлоу з використанням GitHub Environments та “функціями”, то це будуть:
- тест-он-пуш:
- запускається на push event
- запускається з environment Dev, де маємо змінні для Dev-оточення
- викликає “функції” terraform-init та test
- деплой-дев:
- запускається вручну
- запускається з environment Dev, де маємо змінні для Dev-оточення
- викликає “функції” terraform-init, test та deploy
- деплой-прод:
- запускається вручну
- запускається з environment Prod, де маємо змінні для Prod-оточення та protection rules
- викликає “функції” terraform-init, test та deploy
- деплой-фіча-енв:
- запускається при створенні ПР з лейблою “деплой”
- запускається з динамічним оточенням (ім’я генерується з Pull Request ID і створюється під час деплою)
- викликає “функції” terraform-init, test та deploy
- дестрой-фіча-енв:
- запускається при закритті ПР з лейблою “деплой”
- запускається з динамічним оточенням (ім’я генерується з Pull Request ID і під час деплою оточення вибирається з існуючих)
- викликає “функції” terraform-init, test та destroy
Як саме робити ці “функції” – з Reusable Workflow чи Composite Actions? В прицнипі, в данному випадку мабуть різниці великої нема, бо будемо мати один рівень вкладеності, але тут є нюанс з GitHub Runners: якщо в Workflow робити задачі “terraform-init”, “test” та “deploy” в різних jobs
(а Reusable Workflow мають бути тільки в jobs
, більш того – ці jobs
не можуть мати інших steps
) – то вони скоріш за все будуть запускатись на різних Runners, і в такому випадку для виконання terraform plan
&& apply
&& destroy
нам доведеться кілька раз виконувати step
з AWS Login та terraform init
.
Тому виглядає більш правильним запускати всі задачі в рамках однієї job
, тобто для створення наших “функцій” використати Composite Actions.
Єдине, що в цьому пості я все ж не буду описувати деплой feature-енвів, бо по-перше – пост і так вже досить довгий і містить багато нвоої (принаймні для мене) інформації, по-друге – створення додаткових тестових кластерів AWS EKS буде скоріш виключенням, і буде робитись мною, а я поки що це можу зробити і без додаткової автоматизації. Втім, скоріш за все опишу створення динамічних оточень, коли буду робити GitHub Actions Workflow для деплоїв Helm-чартів.
Окей – давайте пробувати деплоїти Dev та Prod.
Підготовка деплоїв
Створення Actions secrets and variables
У нас буде декілька змінних, які будуть однакові для всіх оточень – AWS_REGION
та TF_AWS_PROFILE
, тож їх задамо на рівні всього репозиторію.
Переходимо в Settings – Actions secrets and variables > Variables, і додаємо Repository variables:
Створення GitHub Environments
Додаємо два оточення – Dev та Prod:
Аналогічно для Prod.
Створення test-on-push Workflow
Створення Composite Action “terraform-init”
В директорії .github
додаємо каталог actions
, в якому створюємо каталог terraform-init
з файлом action.yaml
, в якому описуємо сам Action.
Див. Creating a composite action.
В inputs
приймаємо три параметри, які потім передамо з Workflow:
name: "AWS Login and Terraform Init" description: "Combining AWS Login && terraform init actions into one" inputs: environment: type: string required: true region: type: string required: true iam-role: type: string required: true runs: using: "composite" steps: - uses: hashicorp/setup-terraform@v2 - name: "Setup: Configure AWS credentials" # this step adds `env.AWS_*` variables uses: aws-actions/configure-aws-credentials@v2 with: role-to-assume: ${{ inputs.iam-role }} role-session-name: github-actions-terraform role-duration-seconds: 7200 aws-region: ${{ inputs.region }} - name: Terraform Init run: terraform init -backend-config=backend.hcl -backend-config="key=${{ inputs.environment }}/atlas-eks.tfstate" shell: bash working-directory: terraform
Тут є нюанс з working-directory
, який в Composite Action треба задавати окремо для кожного step
, бо сам Action в workflow запускається в корні репозиторія, а використати defaults
, як це можна зробити в workflow не можна, хоча feature request є давно.
Створення Composite Action “test”
В директорії actions
створюємо ще один каталог – terraform-test
з файлом action.yaml
, в якому описуємо другий Action:
name: "Test and verify Terraform code" description: "Combining all test actions into one" inputs: environment: type: string required: true region: type: string required: true iam-role: type: string required: true runs: using: "composite" steps: - uses: hashicorp/setup-terraform@v2 - name: Terraform Validate run: terraform validate shell: bash working-directory: terraform - name: Terraform fmt run: terraform fmt -check shell: bash working-directory: terraform continue-on-error: false - name: Setup TFLint uses: terraform-linters/setup-tflint@v3 with: tflint_version: v0.48.0 - name: Terraform lint run: tflint -f compact shell: bash working-directory: terraform
Створення workflow
Далі, в директорії .github/workflows
створюємо файл test-on-push.yaml
з новим флоу:
name: Test Terraform code on: workflow_dispatch: push: paths: - terraform/** permissions: id-token: write contents: read jobs: test-terraform: environment: 'dev' runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: 'Setup: AWS Login and Terraform init' uses: ./.github/actions/terraform-init with: environment: ${{ vars.ENVIRONMENT }} region: ${{ vars.AWS_REGION }} iam-role: ${{ vars.TF_AWS_ROLE }} - name: 'Test: check and validate' uses: ./.github/actions/terraform-test with: environment: ${{ vars.ENVIRONMENT }} region: ${{ vars.AWS_REGION }} iam-role: ${{ vars.TF_AWS_ROLE }}
В job: test-terraform
задаємо використання environment: 'dev'
і його змінних (у нас там вона одна – ENVIRONMENT=dev
, а в steps
“AWS Login and Terraform init” і “Test and validate” викликаємо наші файли з Actions, які через with
передаємо параметри зі значеннями із змінних.
Пушимо зміни в репозиторій, і маємо наш білд:
Можемо переходити до деплою.
Створення deploy-dev Workflow
Отже, що ми хочемо?
- деплоїти на Дев з бранчу або тегу
- деплоїти на Прод вручну з релізу
Давайте почнемо з Дев-деплою.
Створення Composite Action “deploy”
Створюємо ще один каталог для нового Action – actions/terraform-apply
, в ньому у файлі action.yaml
описуємо сам Action:
name: "Plan and Apply Terraform code" description: "Combining plan && apply actions into one" inputs: environment: type: string required: true region: type: string required: true iam-role: type: string required: true runs: using: "composite" steps: - uses: hashicorp/setup-terraform@v2 - name: Terraform plan run: terraform plan -var-file=envs/${{ inputs.environment }}/${{ inputs.environment }}.tfvars shell: bash working-directory: terraform - name: Terraform apply run: terraform apply -var-file=envs/${{ inputs.environment }}/${{ inputs.environment }}.tfvars -auto-approve shell: bash working-directory: terraform
Створення workflow
Додаємо новий файл workflow – deploy-dev.yaml
:
name: Deploy Dev environment concurrency: group: deploy-${{ vars.ENVIRONMENT }} cancel-in-progress: false on: workflow_dispatch: push: paths: - terraform/** permissions: id-token: write contents: read jobs: deploy-to-dev: environment: dev runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: 'Setup: AWS Login and Terraform init' uses: ./.github/actions/terraform-init with: environment: ${{ vars.ENVIRONMENT }} region: ${{ vars.AWS_REGION }} iam-role: ${{ vars.TF_AWS_ROLE }} - name: 'Test: check and validate' uses: ./.github/actions/terraform-test with: environment: ${{ vars.ENVIRONMENT }} region: ${{ vars.AWS_REGION }} iam-role: ${{ vars.TF_AWS_ROLE }} - name: 'Deploy: to ${{ vars.ENVIRONMENT }}' uses: ./.github/actions/terraform-apply with: environment: ${{ vars.ENVIRONMENT }} region: ${{ vars.AWS_REGION }} iam-role: ${{ vars.TF_AWS_ROLE }}
В умовах on.workflow_dispatch
можна додати ще inputs
, див. on.workflow_dispatch.inputs
.
Усі умови, якими можна тригерити воркфлоу – див. у Events that trigger workflows.
В concurrency
задаємо умову “ставити в чергу, а не виконувати паралельно, якщо запущено іншу job
, яка використовує такий же concurrency.group
“, див. Using concurrency.
Тобто, якщо ми робимо перевірку по group: deploy-${{ vars.ENVIRONMENT }}
– то якщо після запуску Dev запустити деплой на Prod – то він буде очікувати, поки не завершиться деплой на Dev:
Пушимо, мержимо в master, щоб з’вилась кнопка “Run workflow“, і перевіряємо:
І після деплою:
Створення deploy-prod Workflow та захист від випадкового запуску
Composite Action ми вже маємо, тож створювати її не треба.
В принципі, тут маємо все аналогчно до деплою на Dev – concurrency, ручний запуск, jobs
, едине, що тут має бути інша умова запуску – по тегам замість бранчів і тегів.
Тобто на Дев можемо деплоїти будь-який бранч або тег, а на Прод – тільки тег.
Взагалі я тут намагаюсь уникнути автоматичного деплою, бо це все ж інфрастуктура і все ще в процессі роботи напильником, хоча якщо б у нас був GitHub Enterprise – то можна було б використати Deployment protection rules і Required reviewers в них, а в умові on для запуску воркфлоу деплою на Прод використати створення релізів:
on: release: types: [published]
Тобто – деплоїти на Prod тільки коли створено новий GitHub Release, і тільки після того, як, наприклад, деплой апрувнуто кимось з DevOps-тіми.
Ще один варіант забезпечення безпеки Prod-оточення – це дозволяти мерж в master тільки від Code Owners в Branch protection rules:
А Code Owners описати у файлі CODEOWNERS, і дозволити деплой на Prod тільки з мастер-гілки:
Але я хочу тут використати флоу з деплояіми тільки з релізів або тегів, щоб мати щось на кшталт версіонювання.
Тож єдиний варіант – це використати умову if
в самій job
, див. Using conditions to control job execution:
... jobs: deploy-to-prod: if: github.ref_type == 'tag' environment: prod ...
Ну і можна комбінувати умови, наприклад – додатково через github.actor
або github.triggering_actor
перевіряти хто саме запускає деплой, і дозволити тільки певним юзерам. Див. всі типи в github context.
Як додатковий захист від випадкового запуску – можна додати input, до треба явно вказати “yes”:
... on: workflow_dispatch: inputs: confirm-deploy: type: boolean required: true description: "Set to confirm deploy to the PRODUCTION environment" ...
А потім перевірити в if обидві умови – github.ref_type
та github.event.inputs.confirm-deploy
.
Тож поки вокрфлоу для Проду буде таким:
name: Deploy Prod environment concurrency: group: deploy-${{ vars.ENVIRONMENT }} cancel-in-progress: false on: workflow_dispatch: inputs: confirm-deploy: type: boolean required: true description: "Set to confirm deploy to the PRODUCTION environment" permissions: id-token: write contents: read jobs: deploy-to-prod: if: ${{ github.ref_type == 'tag' && github.event.inputs.confirm-deploy == 'true' }} environment: prod runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: 'Setup: AWS Login and Terraform init' uses: ./.github/actions/terraform-init with: environment: ${{ vars.ENVIRONMENT }} region: ${{ vars.AWS_REGION }} iam-role: ${{ vars.TF_AWS_ROLE }} - name: 'Test: check and validate' uses: ./.github/actions/terraform-test with: environment: ${{ vars.ENVIRONMENT }} region: ${{ vars.AWS_REGION }} iam-role: ${{ vars.TF_AWS_ROLE }} - name: 'Deploy: to ${{ vars.ENVIRONMENT }}' uses: ./.github/actions/terraform-apply with: environment: ${{ vars.ENVIRONMENT }} region: ${{ vars.AWS_REGION }} iam-role: ${{ vars.TF_AWS_ROLE }}
І при деплої – треба по-перше вибрати тег, а не бранч, по-друге – поставити відмітку “Confirm”:
А вже по ходу діла подивимось, як все буде складатись.
Ну і поки що на цьому все.
В цілому – в GitHub Actions зробили дуже багато всього, і коли починав робити ці деплої – прям не очікував, що буде стільки цікавих можливостей.
Так що – далі буде ще.
Пару моментів на додачу, доробити потім:
- Action
setup-terraform
приймає інпутterraform_version
, але не вміє це робити з versions-файлу; поки варіант робити через міні-костиль з додатковимstep
, див. цей коммент - в
jobs
має сенс додати параметрtimeout-minutes
, щоб джоби при проблемах не зависали на довгий час, і не вичерпували Runners time