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 і повертайся за два дні...
Якби інфраструктура була автоматизована, цього діалогу б не відбулося (як і затримки в роботі), бо лоад-балансер автоматично деплоївся б. Тому автоматизація інфраструктури така популярна. Вона розв’язує не тільки технічні питання, а й організаційні та комунікаційні. Автоматизація полегшує наше життя і перетворює безлад на передбачуваний процес.
Якщо ви тільки почали ознайомлюватися з цією темою, то можете бути приголомшені кількістю інструментів, які пропонує ринок. Як побудувати процес, який допоможе архітектурі еволюціонувати і дасть змогу змінити інструментарій?
Проблема масштабу
За моїми спостереженнями, один мікросервіс в середньому потребує
Проблема передбачуваності
Якщо створювати всі ці ресурси вручну, то питання «Що робити, якщо ми припустимося помилки і наші середовища будуть відрізнятися; до яких багів це може призвести?» перетворюється на «Що робити, коли...» Бо вірогідність припуститися помилки в кількох сотнях ручних операцій наближається до 100%.
З урахуванням цих проблем автоматизація інфраструктури стає не просто модним трендом, а необхідністю.
Шляхи розв’язання
У нас є перевірені методології роботи з кодом, які ми можемо використати. Ми вже знаємо, як побудувати процес: як код зберігати, тестувати і деплоїти.
Одна з найвідоміших методологій роботи з кодом — The 12 Factor App. Її популяризував один із хмарних провайдерів — Heroku. Серед цілей цієї методології такі:
- забезпечити максимальну портабельність між середовищами, зменшуючи ризик відмінностей та багів. Таким чином ми робимо можливим Continuous Deployment;
- зробити автоматизацію максимально простою. Щоб розробники не витрачали багато часу на початок роботи над проєктом.
І якщо ми пригадаємо наші проблеми, то це те, що лікар прописав!
З-поміж 12 принципів The 12 Factor App найголовнішими є:
- Codebase
- Configuration
- Logging
- Development/Production Parity
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. Може постати питання: а чому б не зберегти цей файл і в системі контролю версій? Є дві причини, чому цього не варто робити:
- По-перше, цей файл де-факто містить у собі конфігурацію. А ми намагаємося розділити код і конфігурацію. Тому зберігати їх разом не варто.
- По-друге, цей файл може містити конфіденційну інформацію. Наприклад, пароль бази даних, яку ви створили в AWS.
Нам треба зберігати конфігурацію в окремому безпечному місці. На щастя, в 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, оскільки цей механізм:
- підтримує шифрування, чим розв’язує проблему зберігання конфіденційних даних;
- підтримує версіонування — у разі несправностей ви завжди зможете знайти останній коректний варіант стану вашого світу;
- більшість команд може використовувати його безкоштовно протягом року (AWS free tier).
Але навіть розв’язавши цю проблему, ми одразу бачимо наступну. Так, ми створили 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)
Тестування та димове тестування заслуговують на окрему статтю, тому наразі зупинимося на валідації та деплойменті.
Валідація інфраструктури — особливо ключової — дуже важлива. Нам треба переконатися, що зміни в інфраструктурі, які будуть деплоїтися — насправді ті, що нам потрібні і що в них немає непередбачуваних побічних ефектів.
У 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, і з іншим
Чи є життя за межами 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 -
Також саме ми описуємо наші ресурси як код, зберігаємо їх у системі контролю версій. Перевіряємо зміни перед тим, як деплоїти, і можемо інтегрувати цей процес з
А що в майбутньому
І навіть якщо в майбутньому з’явиться принципово новий декларативний інструмент для керування (наприклад!) нейромережами, то всі ці методи можна буде застосувати й для нього! Сподіваюсь, тепер вам буде легше починати експерименти з інфраструктурою. Якщо хочете дізнатися більше, рекомендую послухати ці доповіді про Infrastructure-as-code: evolving tools vs core principles: російською чи англійською, Automated Testing for Terraform, Docker, Packer, Kubernetes, and More.
Також раджу прочитати книги
- Infrastructure as Code: Managing Servers in the Cloud by Kief Morris
- Terraform: Up and Running: Writing Infrastructure as Code by Yevgeniy Brikman
GitHub-репозиторій для Core infrastructure та GitHub-репозиторій окремо для сервіса.
Дякую за увагу та бажаю всім легких деплойментів і 100% аптайму!
24 коментарі
Підписатись на коментаріВідписатись від коментарів Коментарі можуть залишати тільки користувачі з підтвердженими акаунтами.