Infrastructure as Code: базові принципи vs інструменти, що еволюціонують

Якщо ви тільки починаєте працювати з інструментами для Infrastructure as Code або думаєте, як інтегрувати його у ваш CI/CD-пайплайн — це стаття для вас. Ми з'єднання ясуємо, як побудувати процес автоматизації інфраструктури та втілити Infrastructure as Code.

Стаття дає базовий огляд Infrastructure as Code як концепції і фокусується на методології і принципи її впровадження в щоденній розробці та деплойменті.

Дисклеймер: ця стаття НЕ є серйозною документацією щодо конкретних інструментів і технологій.

Що таке інфраструктура

Інфраструктура — це ресурси, які потрібні для підтримки коду. Водночас дехто може уявити серверні стійки, світчі та зміїне кубло кабелів... Але це вчорашній день. Сьогодні 99% проєктів живе в «хмарах». Тобто ресурси — це віртуальні машини, контейнери, load balancers.

Отже, усі хмарні ресурси — це інше програмне забезпечення, яке виконується на комп'ютерній комп'ютерах нашого хмарного провайдера.

Infrastructure as Code — це спосіб постачання та керування обчислювальними та мережевими ресурсами методом їх опису у вигляді програмного коду, на відміну від налаштовування необхідного обладнання власноруч чи з допомогою інтерактивних інструментів.

Чому варто звернути увагу на Infrastructure as Code

Infrastructure as Code є (вже не таким) новим трендом, який розв'язків язує актуальну проблему автоматизації інфраструктури.

Багатьом із нас доводилося бути в схожій ситуації:

— Слухай, мені треба задеплоїти лоад-балансер...
— Вибач, у нас завал! Будь ласка, створи тікет у JIRA і повертайся за два дні...

Якби інфраструктура була автоматизована, цього діалогу б не відбулося (як і затримки в роботі), бо лоад-балансер автоматично деплоївся б. Тому автоматизація інфраструктури така популярна. Вона розв'язків язує не тільки технічні питання, а й організаційні та комунікаційні. Автоматизація полегшує наше життя і перетворює безлад на передбачуваний процес.

Якщо ви тільки почали ознайомлюватися з цією темою, то можете бути приголомшені кількістю інструментів, які пропонує ринок. Як побудувати процес, який допоможе архітектурі еволюціонуватиі дасть змогу змінити інструментарій?

Проблема масштабу

За моїми спостереженнями, один мікросервіс в середньому потребує 10-12 інфраструктурних ресурсів (a load balancer, RDS instance, security groups тощо). Якщо у нас є три середовища — test, staging, production — то це вже близько 30 ресурсів. А якщо мікросервісів 10-20-100, проблема стає ще більш масштабною.

Проблема передбачуваності

Якщо створювати всі ці ресурси вручну, то питання «Що робити, якщоми припустимося помилки і наші середовища будуть відрізнятися; до яких багів це може призвести?» перетворюється на «Що робити, коли...» Бо вірогідність припуститися помилки в кількох сотнях ручних операцій наближається до 100%.

З урахуванням цих проблем автоматизація інфраструктури стає не просто модним трендом, а необхідністю.

Шляхи розв'язків язання

У нас є перевірені методології роботи з кодом, які ми можемо використати. Ми вже знаємо, як побудувати процес: як код зберігати, тестувати і деплоїти.

Одна з найвідоміших методологій роботи з кодом — The 12 Factor App . Її популяризував один із провайдерів хмарних — Heroku. Серед цілей цієї методології такі:

І якщо ми пригадаємо наші проблеми, то це ті, що лікар прописавши!

З-поміж 12 принципів The 12 Factor App найголовнішими є:

Codebase

Коли ми працюємо з кодом мікросервісів, то не зберігаємо його локально, а користуємося системами контролю версій (Git, Mercurial тощо). І код для інфраструктури не має бути винятком. Так ми не втратимо історію змін і знатимемо причину для кожної з них.

Якщо наш код стає єдиним уніфікованим джерелом істини для всіх середовищ і в ньому немає кастомних латок для окремих середовищ — то ми можемо позбутися проблем з ручним деплойментом, коли кожен реліз — це спроба пригадати де і які граблі приховані. Ми можемо зробити деплоймент інфраструктури повністю автоматизованим.

Configuration

Альо для автоматизованого деплойменту конфігурацію треба зберігати окремо від коду і надавати доступ до неї під час деплойменту. The 12 Factor App рекомендує робити це через змінні середовища. Це універсальний підхід, який працює на будь-який операційній системі. Понад ті, це безпечний підхід, на відміну від аргументів командного рядка, адже змінні середовища не можна отримати з іншого процесу пробачимо 'ps aux'.

Logging

Під час деплойменту інфрастурктури нам треба контролювати стан деплойменту і стан системи загалом. І зробити це можна за допомогою логів. Щоб вони працювали будь-де, The 12 Factor App пропонує розглядати логи як потік евентів без кінця та початку, які відсортовані хронологічно і виведені в stdout.

Логування — це окрема проблема, яку можна розв'язків язувати за допомогою Fluentd, Elasticsearch або скеруванням в інший файл чи процес. Найголовніше, що логи у stdout можуть інтегруватися з будь-якою системою або працювати локально, коли ви займаєтесь дебагінгом.

Development/Production Parity

І найголовніший принцип — це Development/Production Parity (еквівалентність середовищ). Якщо ми зберігаємо код у системі контролю версій і використовуємо його як універсальне джерело істини, а в самому коді немає спеціальних кейсів для окремих середовищ, усі унікальні налаштування зберігаються окремо і доступні під час деплойменту як змінні середовища — ми отримаємо систему, де немає розбіжностей між середовищами (test vs staging vs production).

The 12-Factor App FTW

Так, у нас може виникнути ситуація на кшталт «для тесту мені потрібні два інстанси, а для продакшену — 50». Альо тут буде різниця в налаштуваннях, а не в коді. І це відкриває нам шлях до Continuous Deployment. Якщо ми можемо автоматично задеплоїти та протестувати наші зміни, то можемо автоматично це робити в будь-якому середовищі.

Якщо наші логи доступні у stdout, а наша конфігурація доступна як змінні середовища, то в нас немає жодних проблем з інтеграцією із сучасними CI/CD-рішеннями. Travis CI, Gitlab CI, Github Actions, Jenkins та інші інструменти можуть читати код із системи контролю версій, давати доступ до конфігурації через змінні середовища та працювати з логами в stdout.

«Hello, World!», або З чого почати

Якщо ми почнемо гуглити «infrastructure tools», то можемо здивуватися з вибором, який є на ринку. Нам треба обрати інструмент, з яким не просто вдасться написати аналог «Hello, World!», а з яким буде зручно підтримувати реальну систему.

Першим вибором (якщо ви користуєтеся AWS) може стати AWS CLI. З його допомогою можна створювати, змінювати та видаляти хмарні ресурси:

aws elb create-load-balancer
 --load-balancer-name myELB
--listeners
"Protocol=HTTP,
LoadBalancerPort=80,
InstanceProtocol=HTTP,
InstancePort=80"
 --subnets subnet-15aaab61

Вісь приклад команди, яка створює load balancer за допомогою AWS CLI. На перший погляд досить прозоро, ця команда буде працювати, як заплановано, і створить load balancer... Але чи існують інші ресурси (subnets, security groups), на які ця команда посилається. Якщо ні, їх треба створити (а це ще кілька команд). Якщо ресурси існують, але у них інші ідентифікатори — треба знайті ці правильні ідентифікатори і підставити їх у команду.

А що, як load balancer вже існує? Значить, треба додати команду, яка перевірить його існування. А що, як у нього інші параметри, не такі, як потрібно? Отже, доведеться перевірити його стан — і загорнути нашу команду в if-else-statement: «Якщо ресурсу немає — створи, якщо є — зміни його параметри».

Це занадто багато команд! І проблема навіть не в розмірі скрипта, а в його нестабільності: для кожної банальної операції нам треба обробити безліч винятків. І якщо ми пропустимо один, є шанс зруйнувати всю інфраструктуру.

Декларативні vs імперативні інструменти

Проблема нестабільності скрипта спричинена тим, що AWS CLI — імперативний інструмент. Імперативніінструменти працюють за схемою «я хочу змінити світ, для цього зроблю X». Але якщо світ не є в тому стані, який ми очікуємо, то в найкращому разі інструмент нам поверне помилку і нічого не зробить, у найгіршому — зробить не ті, що ми очікуємо.

Для інфраструктури більше підходять декларативні інструменти. Вони працюють за схемою «я хочу змінити світ і залишити цей світ у стані Y». Замість того, щоб окремо описувати кожен крок, який нам треба зробити, щоб досягнути мети, декларативні інструменти описують саму мету, кінцевий стан. А які саме кроки — декларативні інструменти вирішують самостійно, користувач не повинний описувати кожну дію і кожну умову.

Серед декларативних інструментів для AWS є AWS CloudFormation і Terraform. Для нашого прикладу оберемо Terraform. У ньому load balancer матиме такий вигляд:

resource "aws_elb" "myELB" {
 name = "myELB"

 listener {
 instance_port = 8000
 instance_protocol = "http"
 lb_port = 80
 lb_protocol = "http"
}

 subnets = [...]
 security_groups = [...]
}

...

Тут ми бачимо ще одну проблему — посилання одних ресурсів на інші. Щоб наш псевдокод ставши реальним, треба додати дефініції для security groups (subnets тощо). Наприклад, посилання на security group може мати такий вигляд:

resource "aws_elb" "myELB" {
 name = "myELB"
...
 security_groups = ["${aws_security_group.elb.id}"]
}

resource "aws_security_group" "elb" {
 name = "web_alb"
 description = "Allow incoming HTTP connections to ALB."

 ingress {
 from_port = 80
 to_port = 80
 protocol = "tcp"
 cidr_blocks = ["0.0.0.0/0"]
}

 egress {
 from_port = 0
 to_port = 0
 protocol = "-1"
 cidr_blocks = ["0.0.0.0/0"]
}
}

Ми визначаємо правила для цієї security group — приймати трафік на порт 80 — і деталі деплоймента цієї групи стають неважливими. Terraform сам задеплоїть ресурс, отримає його ID і підставить у параметри load balancer.

Ми можемо додати декларації Terraform до системи контролю версій і використовувати їх як джерело істини для різних середовищ. І тут ми бачимо наступну проблему.

Стан світу

Як Terraform дізнається, що треба деплоїти, а що ні? Для цього йому потрібно десь зберігати поточний стан усіх описуваних ресурсів. За замовчуванням цей стан зберігається у файлі terraform.tfstate. Може постати питання: а чому б не зберегти цей файл і в системі контролю версій? Є дві причини, чому цього не варто робити:

Нам треба зберігати конфігурацію в окремому безпечному місці. На щастя, в Terraform є концепція remote state; ми можемо зберегти стан світу окремо від коду і навіть експортувати його атрибути, які можуть бути корисними для інших розробників.

output "web_alb_sg_id" {
 value = aws_security_group.web_alb.id
}

terraform {
 backend "s3" {
 key = "iacdemo.tfstate"
 region = "us-west-2"
 bucket = "demobucket"
}
}

Terraform підтримує багато механізмів remote state, але якщо ви працюєте з AWS, то рекомендую дивитися на AWS S3, оскільки цей механізм:

Альо навіть розв'язків язавши цю проблему, ми одразу бачимо наступну. Так, ми витворили load balancer та інші ресурси, але...

Не всі однакові ресурси

Так, load balancer важливий для нашого мікросервіса. Альо security group, на яку він посилається, важлива для всіх мікросервісів. Зламати security group — це набагато гірше, ніж зламати один load balancer.

Саме тут пролягає межа конфлікту між розробниками та DevOps. Розробникам треба якомога швидше описати та задеплоїти ресурси для своїх сервісів. DevOps потрібно, щоб уся система була стабільною. Однак розробникі не хочуть чекати, поки кожна зміна в інфраструктуру буде ретельно перевірена вручну перед релізом. Вони прагнуть якнайшвидше задеплоїти свої зміни в production.

Єдиний спосіб уникнути цього конфлікту — розділити інфраструктуру на ключову (яка забезпечує базовими ресурсами всю систему) та сервісну (яка забезпечує ресурси для конкретного сервісу). Якщо ключова інфраструктура підтримується окремо, то розробникі можуть працювати над інфраструктурою своїх мікросервисів без страху, що зламають щось, про що навіть не мають уявлення. Усі їхні помилки та невдачі будуть лише в межах їхніх сервісів.

Як розділити інфраструктуру

Альо розробникам все одне треба посилатися на ключові ресурси. Як це зробити?

Розгляньмо на прикладі Terraform. Ми зберігаємо стан світу окремо від коду. Це дає можливість імпортувати його і під годину декларації ресурсів посилатися на ті його параметри, які були експортовані:

data "terraform_remote_state" "core" {
 backend = "s3"
 config = {
 key = "iacdemo.tfstate"
 region = "us-west-2"
 bucket = "demobucket"
}
}


resource "aws_elb" "myELB" {
 name = "myELB"
...
 security_groups = "${terraform_remote_state.core.web_alb_sg_id}"
}

Тепер ключова інфраструктура може бути задеплоєна окремо від інфраструктури мікросервісів.

І ми переходимо до найважливішого питання...

Як організувати процес деплойменту інфраструктури

Як і під час роботи зі звичайним кодом, ми можемо розбити процес деплойменту на частини:

  1. Валідація
  2. Тестування
  3. Деплоймент
  4. Димове тестування
  5. (і потім усе повторюється у наступному оточенні, починаючи з п. 1)

Тестування та димове тестування заслуговують на окрему статтю, тому наразі зупинимося на валідації та деплойменті.

Валідація інфраструктури — особливо ключової — дуже важлива. Нам треба переконатися, що зміни в інфраструктурі, які будуть деплоїтися — насправді ті, що нам потрібні і що у них немає непередбачуваних побічних ефектів.

У Terraform це можна зробити за допомогою команд:

terraform init
terraform plan -input=false

Перша команда ініціалізує Terraform і створює remote state або синхронізується з ним.

Друга команда повертає нам перелік ресурсів, які будуть створюватися або змінюватися. Щоб ми могли перевірити, чи насправді заплановані зміни — ті, які ми очікуємо. (Параметр «-input=false» потрібен для того, щоб усі змінні бралися зі змінних оточень, не чекаючи на введення з консолі. Це дуже корисно, коли команди виконуються в headless оточенні, наприклад у Jenkins, де консолі немає).

Обережно з видаленням

Під час перегляду переліку змін треба звернути особливу увагу на видалення ресурсів: без знання специфіки їхньої роботи можна помилитися. Наприклад, зробити на перший погляд тривіальну зміну — в імені load balancer — яка може призвести до того, що наявний load balancer буде видалено, а новий створен за хвилину опісля.

Якщо ваша інфраструктура не потребує 99.9% аптайму, це можна пережити, якщо ні — можливо, вам треба застосувати налаштування create_before_destroy . Але для цього потрібно розуміти, як це відобразиться на ресурсах, які залежать від того, в якому виникла проблема.

А тепер деплоїмо

Якщо всі зміни є такими, як нам треба, можна сміливо переходіті до деплойменту. Подивімось, який вигляд матиме фінальний скрипт, потрібний для повної автоматизації:

# initializing configuration!
export TF_VAR_<your variable>=... 

# setting up or syncing with a remote state
terraform init


# reviewing a list of changes
terraform plan -input=false 

# deploying our infrastructure changes
terraform apply -input=false -auto-approve

Як бачимо, це банальний shell-скрипт, який можна інтегрувати і з Jenkins, і з Gitlab CI, і з іншим CI-CD рішенням, зокрема з нашим CI-CD pipeline. Це розв'язків язує проблему з неавтоматизованим деплоєм лоад-балансера, описану на початку статті. Тепер ми можемо описати нашу інфраструктуру як код, зберігати зміни в системі контролю версій, робити їм code review та автоматично деплоїти за хвилини, а не за дні чі тижню. Але усе це стосується лише Terraform...

Чи є життя за межами Terraform

Так! Розглянємо приклад з Kubernetes, який дає змогу описувати інфраструктуру для контейнерів та мережних ресурсів декларативно. Можемо загорнути цю декларацію в шаблон, використовуючи банальний shell-скрипт (або більш просунуті інструменти — від YTT до Helm. Але це я залишу як домашнє завдання охочим :)

#!/bin/bash
cat <<YAML
apiVersion: apps/v1beta1
kind: Deployment
...
spec:
 replicas: 1
template:
spec:
containers:
 - name: $SERVICE_NAME
 image: $DOCKER_IMAGE
 imagePullPolicy: Always
ports:
 - containerPort: 8090
...
YAML

Цей скрипт так само можна конфігурувати за допомогою змінних середовища. Якщо ми подивимося на процес деплойменту kubernetes-ресурсів, то побачимо, що принципової різниці немає:

# initializing configuration!
export DOCKER_IMAGE=hello:latest
export SERVICE_NAME=helloworld

# reviewing a list of changes
k8s_template.yaml.sh | kubectl apply --dry-run -f -

# deploying our infrastructure changes
k8s_template.yaml.sh | kubectl apply -f -

Також саме ми описуємо наші ресурси як код, зберігаємо їх у системі контролю версій. Перевіряємо зміни перед тім, як деплоїти, і можемо інтегрувати цей процес з CI-CD pipeline.

А що у майбутньому

І навіть якщо в майбутньому з'явиться принципово новий декларативний інструмент для керування (наприклад!) нейромережами, то всі ці методи можна буде застосувати й для нього! Сподіваюсь, тепер вам буде легше починати експерименти з інфраструктурою. Якщо хочете дізнатися більше, рекомендую послухати ці доповіді про Infrastructure-as-code: evolving tools vs core principles: російською чи англійською , .

Також раджу прочитати книги

GitHub-репозиторій для Core infrastructure та GitHub-репозиторій окремо для сервісу.

Дякую за увагу та бажаю всім легких деплойментів і 100% аптайму!

Опубліковано: 06/05/20 @ 10:04
Розділ Різне

Рекомендуємо:

Понад 57 млн грн. Як IT-компанії та спеціалісти допомагають боротися з епідемією COVID-19
Front-end дайджест #39: COVID-19 у світі розробки інтерфейсів
gRPC-автогенерація Front-end-у
"Ми можемо бути не лише масажистами". Як люди з порушеннями зору вчаться робити сайти доступними
Не ставте питання «чому». Як менеджера спілкуватися з командою правильно