Terraform позволяет хранить описание облачной инфраструктуры в виде кода. Мы можем получить готовую к работе инфраструктуру со всеми зависимостями, выполнив всего одну команду — terraform apply.
В этом уроке мы попрактикуемся с Terraform и попробуем с его помощью развернуть с нуля готовую к работе инфраструктуру: создадим базу и сервер, свяжем их друг с другом, поставим на сервер приложение и подключим его к базе.
Подключение Terraform к облаку
Для начала организуем в проекте доступ к нашему облаку. Создадим папку с проектом и добавим в него файл .gitignore, в котором исключим рабочие файлы Terraform и секреты:
secret.*
.terraform
*.backup
Добавим файл providers.tf, в котором опишем нужный нам провайдер и его параметры подключения:
Объявим используемые там переменные в variables.tf.
Рекомендуем настроить консольную утилиту для облака. В этом случае через нее можно получить токен и сразу сконвертировать его в нужную нам переменную:
Команда выполнит запрос к облаку, извлечет из ответа строку с токеном и сохранит ее в файл secret.auto.tfvars.
Можно получить токен и другим способом. Главное сохранить его в секретный *.auto.tfvars.
Допишем к токену значения переменных yc_cloud_id и yc_folder_id — это id облака и каталога, в которых будем работать.
Теперь перейдем к описанию ресурсов. Поскольку в примере мы используем облако Yandex, нам нужно будет развернуть базовую сетевую инфраструктуру, в которой будут размещаться виртуальные машины и кластеры баз данных.
Сетевая связность
Для нашей инфраструктуры будет достаточно одной виртуальной сети yandex_vpc_network и подсети yandex_vpc_subnet:
Мы описали сетевые ресурсы и дали им одинаковые имена. Поскольку это разные ресурсы, конфликтов из-за одинакового имени не будет, а в интерфейсе облака мы явно увидим, что управляется Terraform.
Такая сетевая архитектура является спецификой облака Yandex. В нее можно не углубляться, главное для понимания:
- Подсеть предоставляет диапазон IP-адресов, которые мы можем использовать для наших серверов и баз данных
- Сеть включает в себя подсети, ресурсы в которых могут общаться друг с другом
Разместим в этой сетевой инфраструктуре облачную базу данных.
Кластер БД
Опишем ресурс облачного кластера БД PostgreSQL. Добавим в него пользователя и создадим базу:
В ресурсах пользователя и базы данных сразу пропишем зависимость от кластера.
Для создания пользователя и самой базы мы будем использовать sensitive-переменные db_user, db_password и db_name. Их необходимо объявить, а затем добавить значения в наш secret.auto.tfvars:
db_name = "hexlet"
db_user = "me"
db_password = "bvcdV6sdBS7AXZs"
Теперь опишем сервер и настроим так, чтобы при развертывании инфраструктуры на нем запускалось приложение. Приложение будет подключаться к кластеру БД.
Сервер и приложение
Запустим на сервере приложение wiki.js, которое можно развернуть с помощью Docker. Это избавит нас от установки дополнительных пакетов на сервер. Единственное, что нам потребуется для запуска — наличие на сервере Docker.
Облако предоставляет готовый образ на базе Ubuntu с предустановленным Docker.
Мы можем скопировать id этого образа или использовать data source, чтобы получить актуальный образ по family ID: container-optimized-image.
Опишем сервер с помощью ресурса yandex_compute_instance:
Мы задаем для сервера:
- Выделенные ресурсы
- Образ виртуальной машины
- Подсеть, в которую он будет помещен
- Метаданные для инициации
Также для сетевого интерфейса машины задаем nat = true, чтобы машине был выдан внешний IP-адрес.
Не хватает описания запуска приложения и передачи ему параметров базы данных. Сделаем это с помощью Terraform provisioner. Провижнеры позволяют выполнять различные операции в процессе развертывания инфраструктуры — создавать файлы, выполнять скрипты локально или на новых созданных машинах.
Мы будем использовать провижнер remote-exec. Он подключается к серверу с помощью параметров, описанных в блоке connection. Там он выполняет операции, описанные в блоке inline.
И connection, и provisioner нужно добавить внутрь ресурса yandex_compute_instance. Допишем их ниже блока metadata:
В connection мы настроили подключение по ssh под пользователем ubuntu. Для этого использовали наш локальный приватный ключ. Провижнер будет подключаться по адресу self.network_interface[0].nat_ip_address — в это поле после создания сервера сохранится внешний IP-адрес сервера. Когда провижнер подключится, он выполнит команды, описанные в inline.
В блок inline команды передаются списком. У нас всего одна длинная команда, для удобства чтения мы записываем ее с переносами строки и передаем в heredoc-формате. Форматирование в примере выше является корректным: маркеры блока EOT для корректной работы не должны иметь отступов.
В итоге после создания виртуальной машины провижнер подключится к машине по ssh и запустит docker-контейнер ghcr.io/requarks/wiki:2.5 с приложением. В параметры подключения к кластеру БД указываем те же переменные, которые использовали при описании кластера. Хост кластера БД станет известен после его создания. Чтобы получить его в скрипте, ссылаемся на поле кластера yandex_mdb_postgresql_cluster.dbcluster.host.0.fqdn.
Единственное, что осталось учесть, — это порядок создания ресурсов. В этом нам помогут зависимости.
Зависимости
Технически возможна ситуация, когда виртуальная машина развернется раньше кластера БД, и приложение не сможет подключиться к базе. В этом случае нам хотелось бы иметь явную зависимость, ограничивающую создание виртуальных машин, пока кластер не готов к работе.
Добавим свойства depends_on в наших ресурсах. Кластер баз данных будет зависеть от ресурсов сети и подсети, а виртуальная машина — от кластера БД:
Для сервера тоже нужна подсеть, но поскольку мы уже неявно обозначили зависимость от нее через кластер БД, в блок depends_on ее можно не добавлять.
Terraform при развертывании инфраструктуры строит свой внутренний граф зависимостей на основе ссылок на поля других ресурсов. Если инфраструктура несложная (как в этом уроке), он вполне способен правильно выстроить порядок сам. Но лучше сразу привыкнуть держать зависимости под контролем.
Также этот подход добавляет прозрачности тому, что и в каком порядке происходит при конфигурации инфраструктуры.
На этом этапе у нас всё готово. Осталось выполнить terraform apply и убедиться, что все создалось и запустилось.
Создание инфраструктуры
Выполним terraform apply:
Terraform will perform the following actions:
# yandex_compute_instance.vm will be created
+ resource "yandex_compute_instance" "vm" {
...
# yandex_mdb_postgresql_cluster.dbcluster will be created
+ resource "yandex_mdb_postgresql_cluster" "dbcluster" {
...
Запустим создание инфраструктуры и проследим за порядком:
yandex_vpc_network.net: Creating...
yandex_vpc_network.net: Creation complete after 2s [id=enp49digsf8iut549fve]
yandex_vpc_subnet.subnet: Creating...
yandex_vpc_subnet.subnet: Creation complete after 1s [id=e9b0r2vjb50q1s37is8v]
yandex_mdb_postgresql_cluster.dbcluster: Creating...
yandex_mdb_postgresql_cluster.dbcluster: Still creating... [10s elapsed]
...
yandex_mdb_postgresql_cluster.dbcluster: Still creating... [6m20s elapsed]
yandex_mdb_postgresql_cluster.dbcluster: Creation complete after 6m23s [id=c9qnurf8pfd32bpmitmm]
yandex_mdb_postgresql_user.dbuser: Creating...
yandex_compute_instance.vm: Creating...
yandex_mdb_postgresql_user.dbuser: Still creating... [10s elapsed]
yandex_compute_instance.vm: Still creating... [10s elapsed]
...
andex_mdb_postgresql_user.dbuser: Creation complete after 24s [id=c9qnurf8pfd32bpmitmm:me]
yandex_mdb_postgresql_database.db: Creating...
yandex_compute_instance.vm: Still creating... [30s elapsed]
yandex_mdb_postgresql_database.db: Still creating... [10s elapsed]
yandex_compute_instance.vm: Still creating... [40s elapsed]
yandex_compute_instance.vm: Provisioning with 'remote-exec'...
yandex_compute_instance.vm (remote-exec): Connecting to remote host via SSH...
yandex_compute_instance.vm (remote-exec): Host: 51.250.1.251
yandex_compute_instance.vm (remote-exec): User: ubuntu
yandex_compute_instance.vm (remote-exec): Password: false
yandex_compute_instance.vm (remote-exec): Private key: true
yandex_compute_instance.vm (remote-exec): Certificate: false
yandex_compute_instance.vm (remote-exec): SSH Agent: true
yandex_compute_instance.vm (remote-exec): Checking Host Key: false
yandex_compute_instance.vm (remote-exec): Target Platform: unix
yandex_compute_instance.vm (remote-exec): Connected!
yandex_compute_instance.vm: Still creating... [1m0s elapsed]
yandex_compute_instance.vm (remote-exec): Unable to find image 'ghcr.io/requarks/wiki:2.5' locally
yandex_compute_instance.vm (remote-exec): 2.5: Pulling from requarks/wiki
yandex_compute_instance.vm (remote-exec): 31e352740f53: Pulling fs layer
yandex_compute_instance.vm (remote-exec): 2629b68d4311: Pulling fs layer
...
yandex_compute_instance.vm (remote-exec): Status: Downloaded newer image for ghcr.io/requarks/wiki:2.5
yandex_compute_instance.vm (remote-exec): c33a8f713d927bc0fa3f042a0d20a44b5abcc7798312ff58964d2f46664b4a17
yandex_compute_instance.vm: Creation complete after 1m23s [id=fhmdenv9loufk2m2gcj2]
Apply complete! Resources: 6 added, 0 changed, 0 destroyed.
По логу можно заметить:
- Заданный нами порядок соблюдается
- Провижнер логирует в Terraform операции, которые выполняет
- Создание сервера не считается завершенным до тех пор, пока провижнер не отработал полностью
Приложению понадобится пара минут, чтобы установить все зависимости и накатить миграции в БД. После этого мы можем зайти на внешний IP сервера и увидеть админку wiki.js, готового к работе.
Можем создать администратора и добавить пару статей в нашу новую wiki. Приложение сохранит данные в базу.
После этого мы можем полностью удалить наш сервер через интерфейс облака и повторно вызвать terraform apply. Terraform не найдет yandex_compute_instance.vm и предложит создать его заново. Развернем сервер снова и зайдем на него по новому внешнему IP, который ему выдало облако. Там мы должны увидеть ту же самую wiki и уже созданные нами статьи.
Когда закончите практику, не забудьте выполнить terraform destroy, чтобы убрать из облака всю созданную в проекте инфраструктуру.
Выводы
В этом уроке мы разобрались, как с помощью Terraform описать и поднять в облаке готовое к работе stateful-приложение. Такое решение с сервером и базой является вариантом применения подхода «неизменяемая инфраструктура» — когда мы полностью конфигурируем сервер на этапе создания. А если нужно что-то поменять в настройках — просто удаляем его и инициируем создание нового.
<!DOCTYPE html>
<html class="h-100" data-bs-theme="light" data-mantine-color-scheme="light" lang="ru" prefix="og: https://ogp.me/ns#">
<head>
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<link crossorigin="true" href="https://cdn.hexlet.io" rel="preconnect">
<link href="https://mc.yandex.ru" rel="preconnect">
<meta content="aa2vrdtq64dub8knuf83lwywit311w" name="facebook-domain-verification">
<link href="/favicon.ico" rel="icon" sizes="any">
<link href="/favicon.svg" rel="icon" type="image/svg+xml">
<link href="/apple-touch-icon.png" rel="apple-touch-icon">
<link href="/manifest.webmanifest" rel="manifest">
<script>
//<![CDATA[
window.gon={};gon.ym_counter="25559621";gon.is_bot=true;gon.applications={};gon.current_user={"id":null,"last_viewed_notification_id":null,"email":null,"state":null,"first_name":"","last_name":"","created_at":"2026-02-26 22:45:50 UTC","current_program":null,"current_team":null,"full_name":"","guest":true,"can_use_paid_features":false,"is_hexlet_employee":false,"sanitized_phone_number":"","can_subscribe":true,"can_renew_education":false};gon.token="KGkc0Gm134QDJecjQjbXiEhuf6T8zRdte7P5LzOsYtbHuNfnm8ty5LVmw7tOOSf_iGdSDvT66c_GU2N7YauFuA";gon.locale="ru";gon.language="ru";gon.theme="light";gon.rails_env="production";gon.mobile=false;gon.google={"analytics_key":"UA-1360700-51","optimize_key":"GTM-5QDVFPF"};gon.captcha={"google_v3_site_key":"6LenGbgZAAAAAM7HbrDbn5JlizCSzPcS767c9vaY","yandex_site_key":"ysc1_Vyob5ZPPUdPBsu0ykt8bVFdzsfpoVjQChLGl2b4g19647a89","verification_failed":null};gon.social_signin=false;gon.typoreporter_google_form_id="1FAIpQLSeibfGq-KvWQ2Fyru-zkFFRVTLBuzXAHAoEyN1p49FtDmNoNA";
//]]>
</script>
<meta charset="utf-8">
<title>Практика Terraform | Terraform: Основы</title>
<meta name="description" content="Практика Terraform / Terraform: Основы: Научимся разворачивать в облаке готовое к работе stateful-приложение">
<link rel="canonical" href="https://ru.hexlet.io/courses/terraform-basics/lessons/example-with-db/theory_unit">
<meta name="robots" content="noarchive">
<meta property="og:title" content="Практика Terraform">
<meta property="og:title" content="Terraform: Основы">
<meta property="og:description" content="Практика Terraform / Terraform: Основы: Научимся разворачивать в облаке готовое к работе stateful-приложение">
<meta property="og:url" content="https://ru.hexlet.io/courses/terraform-basics/lessons/example-with-db/theory_unit">
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="RM1wAQcxraSsM9-uwsCog2vg0ETm5DxhZluLrD-3ndurHLs29U8AxBpw-zbOz1j0q-n97u7TwsPbuxH4bbB6tQ" />
<script src="/vite/assets/inertia-DfXos102.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/preload-helper-BJ4cLWpC.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/init-BrRXra1y.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ahoy-DrlRQ-1D.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/analytics-cb8xch9l.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ErrorFallbackBlock-naDSYSy9.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Surface-DL2bpZA-.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/gon-D3e4yh1x.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/mantine-CGMYrt2Y.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/utils-DRqSHbQE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/extends-C-EagtpE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/inheritsLoose-BBd-DCVI.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/objectWithoutPropertiesLoose-DRHXDhjp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/index.esm-DAqKOkZ0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Button-CGPUux8l.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/CloseButton-D1euiPao.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Group-BX48WcuU.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Loader-BQEY8g6v.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Modal-Cy3HByv7.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/OptionalPortal-1Hza5P2w.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Stack-CtjJzfw4.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Textarea-Ck64llAy.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Box-B5-OOzBf.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/DirectionProvider-Dc9zdUke.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/events-DJQOhap0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-reduced-motion-D2owz4wa.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-disclosure-zKtK5W1r.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-hotkeys-Cnc_Rwkb.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/random-id-DOQyszCZ.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/notifications.store-C-3AFSMn.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/exports-C_MrNx_T.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/axios-BEvgo0ym.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dayjs.min-BkKovM-s.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/i18next-BlSq9s7B.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/client-U9M77rxp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-dom-DaLxUz_h.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useTranslation-Bx1Cdrkz.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/compiler-runtime-6XxiPFnt.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/jsx-runtime-CwjcCKJi.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-CkL4ZRHB.js" as="script" crossorigin="anonymous">
<link rel="stylesheet" href="/vite/assets/application-BqhCP46M.js" />
<script src="/vite/assets/application-Df9RExpe.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/autocomplete-VMNbxKGl.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/createPopper-C3aM9r1M.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/js.cookie-D1-O8zkX.js" as="script" crossorigin="anonymous"><link rel="stylesheet" href="/vite/assets/application-C8HjmMaq.css" media="screen" />
<script>
window.ym = function(){(ym.a=ym.a||[]).push(arguments)};
window.addEventListener('load', function() {
setTimeout(function() {
ym.l = 1*new Date();
ym(window.gon.ym_counter, "init", {
clickmap: true,
trackLinks: true,
accurateTrackBounce: true,
webvisor: true
});
// Загружаем скрипт
var k = document.createElement('script');
k.async = 1;
k.src = 'https://mc.yandex.ru/metrika/tag.js';
document.head.appendChild(k);
ym(window.gon.ym_counter, 'getClientID', function(clientID) {
window.ymClientId = clientID;
});
}, 1500);
});
</script>
<!-- Google Tag Manager - deferred -->
<script>
// dataLayer stub сразу — пуши работают до загрузки скрипта
window.dataLayer = window.dataLayer || [];
// Сам скрипт — отложенно после load
window.addEventListener('load', function() {
setTimeout(function() {
dataLayer.push({'gtm.start': new Date().getTime(), event: 'gtm.js'});
var j = document.createElement('script');
j.async = true;
j.src = 'https://www.googletagmanager.com/gtm.js?id=GTM-WK88TH';
document.head.appendChild(j);
}, 1500);
});
</script>
<!-- End Google Tag Manager -->
</head>
<body>
<noscript>
<div>
<img alt="" src="https://mc.yandex.ru/watch/25559621" style="position:absolute; left:-9999px;">
</div>
</noscript>
<header class="sticky-top bg-body">
<nav class="navbar navbar-expand-lg">
<div class="container-xxl">
<a class="navbar-brand" href="/"><img alt="Логотип Хекслета" height="24" src="https://ru.hexlet.io/vite/assets/logo_ru_light-BpiEA1LT.svg" width="96">
</a><button aria-controls="collapsable" aria-expanded="false" aria-label="Меню" class="navbar-toggler border-0 mb-0 mt-1" data-bs-target="#collapsable" data-bs-toggle="collapse">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="collapsable">
<ul class="navbar-nav mb-lg-0 mt-lg-1">
<li class="nav-item dropdown">
<button aria-haspopup class="btn nav-link" data-bs-toggle="dropdown" type="button">
Все курсы
<span class="bi bi-chevron-down align-middle ms-1"></span>
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item d-flex py-2" href="/courses"><div class="fw-bold me-auto">Все что есть</div>
<div class="text-muted">117</div>
</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li class="dropdown-item">
<b>Популярные категории</b>
</li>
<li>
<a class="dropdown-item py-2" href="/courses_devops">Курсы по DevOps
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_data_analytics">Курсы по аналитике данных
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_programming">Курсы по программированию
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_testing">Курсы по тестированию
</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li class="dropdown-item">
<b>Популярные курсы</b>
</li>
<li>
<a class="dropdown-item py-2" href="/programs/devops-engineer-from-scratch">DevOps-инженер с нуля
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/go">Go-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/java">Java-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/python">Python-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/qa-auto-engineer-java">Автоматизатор тестирования на Java
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/data-analytics">Аналитик данных
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/frontend">Фронтенд-разработчик
</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<button aria-haspopup class="btn nav-link" data-bs-toggle="dropdown" type="button">
О Хекслете
<span class="bi bi-chevron-down align-middle"></span>
</button>
<ul class="dropdown-menu bg-body">
<li>
<a class="dropdown-item py-2" href="/pages/about">О нас
</a></li>
<li>
<a class="dropdown-item py-2" href="/blog">Блог
</a></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/hse-research" role="button">Результаты (Исследование)
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://career.hexlet.io" role="button">Хекслет Карьера
</span></li>
<li>
<a class="dropdown-item py-2" href="/testimonials">Отзывы студентов
</a></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://t.me/hexlet_help_bot" role="button">Поддержка (В ТГ)
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/referal-program/?promo_creative=priglasite-druzei&promo_name=referal-program&promo_position=promo_position&promo_start=010724&promo_type=link" role="button">Реферальная программа
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/certificate" role="button">Подарочные сертификаты
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://hh.ru/employer/4307094" role="button">Вакансии
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://b2b.hexlet.io" data-target="_blank" role="button">Компаниям
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://hexly.ru/" data-target="_blank" role="button">Колледж
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://hexlyschool.ru/" data-target="_blank" role="button">Частная школа
</span></li>
</ul>
</li>
<li><a class="nav-link" href="/subscription/new">Подписка</a></li>
</ul>
<ul class="navbar-nav flex-lg-row align-items-lg-center gap-2 ms-auto">
<li>
<a class="nav-link" aria-label="Переключить тему" href="/theme/switch?new_theme=dark"><span aria-hidden="true" class="bi bi-moon"></span>
</a></li>
<li>
<span data-target="_self" class="nav-link external-link" data-href="/u/new" role="button"><span>Регистрация</span>
</span></li>
<li>
<span data-target="_self" class="nav-link external-link" data-href="https://ru.hexlet.io/session/new" role="button"><span>Вход</span>
</span></li>
</ul>
</div>
</div>
</nav>
</header>
<div class="x-container-xxxl">
</div>
<main class="mb-6 min-vh-100 h-100">
<link rel="preload" as="image" href="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6Mzk3NywicHVyIjoiYmxvYl9pZCJ9fQ==--bc5ef27286509b0ecf2f8ae6cbdce2376db3d394/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/500%20Internal%20Server%20Error-cuate.png"/><link rel="preload" as="image" href="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6Mzk2NSwicHVyIjoiYmxvYl9pZCJ9fQ==--84278a1852c9c6fb13b80a69f395bac6e47a422e/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Cloud%20sync-bro.png"/><link rel="preload" as="image" href="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MzY0MywicHVyIjoiYmxvYl9pZCJ9fQ==--74611367ca7524225d6b8670846088b4aa9fa1d2/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Server-bro.png"/><link rel="preload" as="image" href="/vite/assets/development-BVihs_d5.png"/><div id="app" data-page="{"component":"web/courses/lessons/theory_unit","props":{"errors":{},"locale":"ru","language":"ru","httpsHost":"https://ru.hexlet.io","host":"ru.hexlet.io","colorScheme":"light","auth":{"user":{"id":null,"last_viewed_notification_id":null,"email":null,"state":null,"first_name":"","last_name":"","created_at":"2026-02-26T22:45:50.242Z","current_program":null,"current_team":null,"full_name":"","guest":true,"can_use_paid_features":false,"is_hexlet_employee":false,"sanitized_phone_number":"","can_subscribe":true,"can_renew_education":false}},"cloudflareTurnstileSiteKey":"0x4AAAAAAA15KmeFXzd2H0Xo","vkIdClientId":"51586979","yandexIdClientId":"88d071f1d3384eb4bd1deb37910235c7","formAuthToken":"CKKJZ4C4s5drtSYrzB2yg5lkLwRYOdQaIyq6E3fdL6bnc0JQcsYe9932ArPAEkL0WW0CrlAOKrieyiBHJdrIyA","topics":[{"id":97926,"title":"Здравствуйте! Или в resource \"yandex_mdb_postgresql_cluster\" \"pgcluster\" - поменяйте \"pgcluster\" на \"dbcluster\" или в зависимостях. Спасибо)","plain_title":"Здравствуйте! Или в resource \"yandexmdbpostgresql_cluster\" \"pgcluster\" - поменяйте \"pgcluster\" на \"dbcluster\" или в зависимостях. Спасибо) ","creator":{"public_name":"Александр","id":234930,"is_tutor":false},"comments":[{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":189028,"body":"**Александр**, здравствуйте! Спасибо! Поменял.","topic_id":97926}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Практика Terraform","entity_url":null,"active":true}}],"lesson":{"exercise":null,"units":[{"id":9484,"name":"theory","url":"/courses/terraform-basics/lessons/example-with-db/theory_unit"},{"id":13155,"name":"quiz","url":"/courses/terraform-basics/lessons/example-with-db/quiz_unit"}],"links":[],"ordered_units":[{"id":9484,"name":"theory","url":"/courses/terraform-basics/lessons/example-with-db/theory_unit"},{"id":13155,"name":"quiz","url":"/courses/terraform-basics/lessons/example-with-db/quiz_unit"}],"id":4229,"slug":"example-with-db","state":"approved","name":"Практика Terraform","course_order":460,"goal":"Научимся разворачивать в облаке готовое к работе stateful-приложение","self_study":null,"theory_video_provider":null,"theory_video_uid":null,"theory":"Terraform позволяет хранить описание облачной инфраструктуры в виде кода. Мы можем получить готовую к работе инфраструктуру со всеми зависимостями, выполнив всего одну команду — `terraform apply`.\n\nВ этом уроке мы попрактикуемся с Terraform и попробуем с его помощью развернуть с нуля готовую к работе инфраструктуру: создадим базу и сервер, свяжем их друг с другом, поставим на сервер приложение и подключим его к базе.\n\n## Подключение Terraform к облаку\n\nДля начала организуем в проекте доступ к нашему облаку. Создадим папку с проектом и добавим в него файл `.gitignore`, в котором исключим рабочие файлы Terraform и секреты:\n\n```\nsecret.*\n.terraform\n*.backup\n```\n\nДобавим файл *providers.tf*, в котором опишем нужный нам провайдер и его параметры подключения:\n\n```terraform\nterraform {\n required_providers {\n yandex = {\n source = \"yandex-cloud/yandex\"\n version = \"~> 0.84.0\"\n }\n }\n}\n\nprovider \"yandex\" {\n token = var.yc_iam_token\n cloud_id = var.yc_cloud_id\n folder_id = var.yc_folder_id\n zone = \"ru-central1-a\"\n}\n```\n\nОбъявим используемые там переменные в *variables.tf*.\n\nРекомендуем настроить [консольную утилиту](https://cloud.yandex.ru/docs/cli/quickstart#install) для облака. В этом случае через нее можно получить токен и сразу сконвертировать его в нужную нам переменную:\n\n```bash\nyc iam create-token | awk '{print \"yc_iam_token = \\\"\"$1\"\\\"\"}' > secret.auto.tfvars\n```\n\nКоманда выполнит запрос к облаку, извлечет из ответа строку с токеном и сохранит ее в файл *secret.auto.tfvars*.\n\nМожно получить токен и [другим способом](https://cloud.yandex.ru/docs/iam/operations/iam-token/create). Главное сохранить его в секретный `*.auto.tfvars`.\n\nДопишем к токену значения переменных `yc_cloud_id и yc_folder_id` — это id облака и каталога, в которых будем работать.\n\nТеперь перейдем к описанию ресурсов. Поскольку в примере мы используем облако Yandex, нам нужно будет развернуть базовую сетевую инфраструктуру, в которой будут размещаться виртуальные машины и кластеры баз данных.\n\n## Сетевая связность\n\nДля нашей инфраструктуры будет достаточно одной виртуальной сети `yandex_vpc_network` и подсети `yandex_vpc_subnet`:\n\n```terraform\nresource \"yandex_vpc_network\" \"net\" {\n name = \"tfhexlet\"\n}\n\nresource \"yandex_vpc_subnet\" \"subnet\" {\n name = \"tfhexlet\"\n zone = \"ru-central1-a\"\n network_id = yandex_vpc_network.net.id\n v4_cidr_blocks = [\"192.168.192.0/24\"]\n}\n```\n\nМы описали сетевые ресурсы и дали им одинаковые имена. Поскольку это разные ресурсы, конфликтов из-за одинакового имени не будет, а в интерфейсе облака мы явно увидим, что управляется Terraform.\n\nТакая сетевая архитектура является спецификой облака Yandex. В нее можно не углубляться, главное для понимания:\n\n1. Подсеть предоставляет диапазон IP-адресов, которые мы можем использовать для наших серверов и баз данных\n2. Сеть включает в себя подсети, ресурсы в которых могут общаться друг с другом\n\nРазместим в этой сетевой инфраструктуре облачную базу данных.\n\n## Кластер БД\n\nОпишем ресурс облачного кластера БД PostgreSQL. Добавим в него пользователя и создадим базу:\n\n```terraform\nresource \"yandex_mdb_postgresql_cluster\" \"dbcluster\" {\n name = \"tfhexlet\"\n environment = \"PRESTABLE\"\n network_id = yandex_vpc_network.net.id\n\n config {\n version = var.yc_postgresql_version\n resources {\n resource_preset_id = \"s2.micro\"\n disk_type_id = \"network-ssd\"\n disk_size = 15\n }\n postgresql_config = {\n max_connections = 100\n }\n }\n\n maintenance_window {\n type = \"WEEKLY\"\n day = \"SAT\"\n hour = 12\n }\n\n host {\n zone = \"ru-central1-a\"\n subnet_id = yandex_vpc_subnet.subnet.id\n }\n}\n\nresource \"yandex_mdb_postgresql_user\" \"dbuser\" {\n cluster_id = yandex_mdb_postgresql_cluster.dbcluster.id\n name = var.db_user\n password = var.db_password\n depends_on = [yandex_mdb_postgresql_cluster.dbcluster]\n}\n\nresource \"yandex_mdb_postgresql_database\" \"db\" {\n cluster_id = yandex_mdb_postgresql_cluster.dbcluster.id\n name = var.db_name\n owner = yandex_mdb_postgresql_user.dbuser.name\n lc_collate = \"en_US.UTF-8\"\n lc_type = \"en_US.UTF-8\"\n depends_on = [yandex_mdb_postgresql_cluster.dbcluster]\n}\n```\n\nВ ресурсах пользователя и базы данных сразу пропишем зависимость от кластера.\n\nДля создания пользователя и самой базы мы будем использовать sensitive-переменные `db_user`, `db_password` и `db_name`. Их необходимо объявить, а затем добавить значения в наш *secret.auto.tfvars*:\n\n```\ndb_name = \"hexlet\"\ndb_user = \"me\"\ndb_password = \"bvcdV6sdBS7AXZs\"\n```\n\nТеперь опишем сервер и настроим так, чтобы при развертывании инфраструктуры на нем запускалось приложение. Приложение будет подключаться к кластеру БД.\n\n## Сервер и приложение\n\nЗапустим на сервере приложение [wiki.js](https://js.wiki/), которое можно развернуть [с помощью Docker](https://docs.requarks.io/install/docker). Это избавит нас от установки дополнительных пакетов на сервер. Единственное, что нам потребуется для запуска — наличие на сервере Docker.\n\nОблако предоставляет готовый образ на базе Ubuntu с предустановленным Docker.\n\n\n\nМы можем скопировать id этого образа или использовать data source, чтобы получить актуальный образ по family ID: *container-optimized-image*.\n\nОпишем сервер с помощью ресурса `yandex_compute_instance`:\n\n```terraform\nresource \"yandex_compute_instance\" \"vm\" {\n name = \"tfhexlet\"\n zone = \"ru-central1-a\"\n\n resources {\n cores = 2\n memory = 2\n }\n\n boot_disk {\n initialize_params {\n image_id = data.yandex_compute_image.img.id\n }\n }\n\n network_interface {\n subnet_id = yandex_vpc_subnet.subnet.id\n nat = true\n }\n\n metadata = {\n ssh-keys = \"ubuntu:${file(\"~/.ssh/id_rsa.pub\")}\"\n }\n}\n```\n\nМы задаем для сервера:\n\n- Выделенные ресурсы\n- Образ виртуальной машины\n- Подсеть, в которую он будет помещен\n- Метаданные для инициации\n\nТакже для сетевого интерфейса машины задаем `nat = true`, чтобы машине был выдан внешний IP-адрес.\n\nНе хватает описания запуска приложения и передачи ему параметров базы данных. Сделаем это с помощью **Terraform provisioner**. Провижнеры позволяют выполнять различные операции в процессе развертывания инфраструктуры — создавать файлы, выполнять скрипты локально или на новых созданных машинах.\n\nМы будем использовать провижнер `remote-exec`. Он подключается к серверу с помощью параметров, описанных в блоке *connection*. Там он выполняет операции, описанные в блоке *inline*.\n\nИ *connection*, и *provisioner* нужно добавить внутрь ресурса `yandex_compute_instance`. Допишем их ниже блока *metadata*:\n\n```terraform\n ...\n metadata = {\n ssh-keys = \"ubuntu:${file(\"~/.ssh/id_rsa.pub\")}\"\n }\n\n connection {\n type = \"ssh\"\n user = \"ubuntu\"\n private_key = file(\"~/.ssh/id_rsa\")\n host = self.network_interface[0].nat_ip_address\n }\n\n provisioner \"remote-exec\" {\n inline = [\n<<EOT\nsudo docker run -d -p 0.0.0.0:80:3000 \\\n -e DB_TYPE=postgres \\\n -e DB_NAME=${var.db_name} \\\n -e DB_HOST=${yandex_mdb_postgresql_cluster.dbcluster.host.0.fqdn} \\\n -e DB_PORT=6432 \\\n -e DB_USER=${var.db_user} \\\n -e DB_PASS=${var.db_password} \\\n ghcr.io/requarks/wiki:2.5\nEOT\n ]\n }\n}\n```\n\nВ *connection* мы настроили подключение по ssh под пользователем ubuntu. Для этого использовали наш локальный приватный ключ. Провижнер будет подключаться по адресу `self.network_interface[0].nat_ip_address` — в это поле после создания сервера сохранится внешний IP-адрес сервера. Когда провижнер подключится, он выполнит команды, описанные в *inline*.\n\nВ блок *inline* команды передаются списком. У нас всего одна длинная команда, для удобства чтения мы записываем ее с переносами строки и передаем в heredoc-формате. Форматирование в примере выше является корректным: маркеры блока EOT для корректной работы не должны иметь отступов.\n\nВ итоге после создания виртуальной машины провижнер подключится к машине по ssh и запустит docker-контейнер `ghcr.io/requarks/wiki:2.5` с приложением. В параметры подключения к кластеру БД указываем те же переменные, которые использовали при описании кластера. Хост кластера БД станет известен после его создания. Чтобы получить его в скрипте, ссылаемся на поле кластера `yandex_mdb_postgresql_cluster.dbcluster.host.0.fqdn`.\n\nЕдинственное, что осталось учесть, — это порядок создания ресурсов. В этом нам помогут зависимости.\n\n## Зависимости\n\nТехнически возможна ситуация, когда виртуальная машина развернется раньше кластера БД, и приложение не сможет подключиться к базе. В этом случае нам хотелось бы иметь явную зависимость, ограничивающую создание виртуальных машин, пока кластер не готов к работе.\n\nДобавим свойства `depends_on` в наших ресурсах. Кластер баз данных будет зависеть от ресурсов сети и подсети, а виртуальная машина — от кластера БД:\n\n```terraform\nresource \"yandex_mdb_postgresql_cluster\" \"dbcluster\" {\n name = \"tfhexlet\"\n ...\n depends_on = [yandex_vpc_network.net, yandex_vpc_subnet.subnet]\n}\n\nresource \"yandex_compute_instance\" \"vm\" {\n name = \"tfhexlet\"\n ...\n depends_on = [yandex_mdb_postgresql_cluster.dbcluster]\n}\n```\n\nДля сервера тоже нужна подсеть, но поскольку мы уже неявно обозначили зависимость от нее через кластер БД, в блок depends_on ее можно не добавлять.\n\nTerraform при развертывании инфраструктуры строит свой внутренний граф зависимостей на основе ссылок на поля других ресурсов. Если инфраструктура несложная (как в этом уроке), он вполне способен правильно выстроить порядок сам. Но лучше сразу привыкнуть держать зависимости под контролем.\n\nТакже этот подход добавляет прозрачности тому, что и в каком порядке происходит при конфигурации инфраструктуры.\n\nНа этом этапе у нас всё готово. Осталось выполнить `terraform apply` и убедиться, что все создалось и запустилось.\n\n## Создание инфраструктуры\n\nВыполним `terraform apply`:\n\n```\nTerraform will perform the following actions:\n\n # yandex_compute_instance.vm will be created\n + resource \"yandex_compute_instance\" \"vm\" {\n ...\n\n # yandex_mdb_postgresql_cluster.dbcluster will be created\n + resource \"yandex_mdb_postgresql_cluster\" \"dbcluster\" {\n ...\n```\n\nЗапустим создание инфраструктуры и проследим за порядком:\n\n```\nyandex_vpc_network.net: Creating...\nyandex_vpc_network.net: Creation complete after 2s [id=enp49digsf8iut549fve]\nyandex_vpc_subnet.subnet: Creating...\nyandex_vpc_subnet.subnet: Creation complete after 1s [id=e9b0r2vjb50q1s37is8v]\nyandex_mdb_postgresql_cluster.dbcluster: Creating...\nyandex_mdb_postgresql_cluster.dbcluster: Still creating... [10s elapsed]\n...\nyandex_mdb_postgresql_cluster.dbcluster: Still creating... [6m20s elapsed]\nyandex_mdb_postgresql_cluster.dbcluster: Creation complete after 6m23s [id=c9qnurf8pfd32bpmitmm]\nyandex_mdb_postgresql_user.dbuser: Creating...\nyandex_compute_instance.vm: Creating...\nyandex_mdb_postgresql_user.dbuser: Still creating... [10s elapsed]\nyandex_compute_instance.vm: Still creating... [10s elapsed]\n...\nandex_mdb_postgresql_user.dbuser: Creation complete after 24s [id=c9qnurf8pfd32bpmitmm:me]\nyandex_mdb_postgresql_database.db: Creating...\nyandex_compute_instance.vm: Still creating... [30s elapsed]\nyandex_mdb_postgresql_database.db: Still creating... [10s elapsed]\nyandex_compute_instance.vm: Still creating... [40s elapsed]\nyandex_compute_instance.vm: Provisioning with 'remote-exec'...\nyandex_compute_instance.vm (remote-exec): Connecting to remote host via SSH...\nyandex_compute_instance.vm (remote-exec): Host: 51.250.1.251\nyandex_compute_instance.vm (remote-exec): User: ubuntu\nyandex_compute_instance.vm (remote-exec): Password: false\nyandex_compute_instance.vm (remote-exec): Private key: true\nyandex_compute_instance.vm (remote-exec): Certificate: false\nyandex_compute_instance.vm (remote-exec): SSH Agent: true\nyandex_compute_instance.vm (remote-exec): Checking Host Key: false\nyandex_compute_instance.vm (remote-exec): Target Platform: unix\nyandex_compute_instance.vm (remote-exec): Connected!\nyandex_compute_instance.vm: Still creating... [1m0s elapsed]\nyandex_compute_instance.vm (remote-exec): Unable to find image 'ghcr.io/requarks/wiki:2.5' locally\nyandex_compute_instance.vm (remote-exec): 2.5: Pulling from requarks/wiki\nyandex_compute_instance.vm (remote-exec): 31e352740f53: Pulling fs layer\nyandex_compute_instance.vm (remote-exec): 2629b68d4311: Pulling fs layer\n...\nyandex_compute_instance.vm (remote-exec): Status: Downloaded newer image for ghcr.io/requarks/wiki:2.5\nyandex_compute_instance.vm (remote-exec): c33a8f713d927bc0fa3f042a0d20a44b5abcc7798312ff58964d2f46664b4a17\nyandex_compute_instance.vm: Creation complete after 1m23s [id=fhmdenv9loufk2m2gcj2]\n\nApply complete! Resources: 6 added, 0 changed, 0 destroyed.\n\n```\n\nПо логу можно заметить:\n\n* Заданный нами порядок соблюдается\n* Провижнер логирует в Terraform операции, которые выполняет\n* Создание сервера не считается завершенным до тех пор, пока провижнер не отработал полностью\n\nПриложению понадобится пара минут, чтобы установить все зависимости и накатить миграции в БД. После этого мы можем зайти на внешний IP сервера и увидеть админку wiki.js, готового к работе.\n\n\n\nМожем создать администратора и добавить пару статей в нашу новую wiki. Приложение сохранит данные в базу.\n\nПосле этого мы можем полностью удалить наш сервер через интерфейс облака и повторно вызвать `terraform apply`. Terraform не найдет `yandex_compute_instance.vm` и предложит создать его заново. Развернем сервер снова и зайдем на него по новому внешнему IP, который ему выдало облако. Там мы должны увидеть ту же самую wiki и уже созданные нами статьи.\n\nКогда закончите практику, не забудьте выполнить `terraform destroy`, чтобы убрать из облака всю созданную в проекте инфраструктуру.\n\n## Выводы\n\nВ этом уроке мы разобрались, как с помощью Terraform описать и поднять в облаке готовое к работе stateful-приложение. Такое решение с сервером и базой является вариантом применения подхода «неизменяемая инфраструктура» — когда мы полностью конфигурируем сервер на этапе создания. А если нужно что-то поменять в настройках — просто удаляем его и инициируем создание нового.\n"},"lessonMember":null,"courseMember":null,"course":{"start_lesson":{"exercise":null,"units":[{"id":5515,"name":"theory","url":"/courses/terraform-basics/lessons/init/theory_unit"},{"id":13152,"name":"quiz","url":"/courses/terraform-basics/lessons/init/quiz_unit"}],"links":[{"id":425569,"name":"Облачные провайдеры для практик DevOps","url":"https://help.hexlet.io/practice-guides/oblachnye-provaidery-dlya-praktik-po-devops"}],"ordered_units":[{"id":5515,"name":"theory","url":"/courses/terraform-basics/lessons/init/theory_unit"},{"id":13152,"name":"quiz","url":"/courses/terraform-basics/lessons/init/quiz_unit"}],"id":2436,"slug":"init","state":"approved","name":"Введение","course_order":100,"goal":"Знакомимся с курсом и его целями","self_study":null,"theory_video_provider":null,"theory_video_uid":null,"theory":"Облачные провайдеры позволяют работать со своими сервисами через веб-интерфейсы. Потратив час-два времени в Digital Ocean, Amazon, Yandex.Cloud можно создать типовую инфраструктуру: пару серверов, управляемую базу данных, балансировщик, файловое хранилище, CDN. Все это соединить, правильно настроить сеть, подумать о безопасности и, в конце концов, начать с ней работать.\n\n\n\nКажется несложно, но со временем, инфраструктура начнет расти, ее придется обновлять иногда пересоздавать. Делать это не имея перед собой слепка системы крайне сложно, вся инфраструктура и настройки разбросаны по разным разделам и регулируются множеством галочек. В случае ошибок практически невозможно откатиться и узнать какое изменение привело к проблемам. Особенно если с инфраструктурой работает несколько человек. Становится сложно отследить кто какое изменение внес.\n\nДля решения этих проблем используется подход **инфраструктура как код**. Вместо того, чтобы \"тыкать\" кнопки в интерфейсе, мы описываем нужную нам инфраструктуру в коде на каком-то удобном языке, который затем, в автоматическом режиме, делает нужные нам изменения. Одним из таких инструментов является Terraform.\n\nTerraform — инструмент, с помощью которого автоматизируется настройка серверной инфраструктуры. Он интегрирован со всеми популярными облаками и может развернуть одной кнопкой любые доступные там сервисы от баз данных и серверов, до CDN и балансировщиков.\n\nВ этом курсе мы научимся им пользоваться и развернем пару кластеров.\n\nДля экспериментов в этом курсе мы рекомендуем [Yandex Cloud](https://help.hexlet.io/practice-guides/oblachnye-provaidery-dlya-praktik-po-devops/). Зарегистрировавшись, вам будет доступен пробный период на 60 дней, которого должно хватить для экспериментов.\n"},"id":260,"slug":"terraform-basics","challenges_count":0,"name":"Terraform: Основы","allow_indexing":true,"state":"approved","course_state":"finished","pricing_type":"paid","description":"На этом курсе вы изучите Terraform. Вы узнаете больше о том, что такое инфраструктура как код. В итоге научитесь создавать ресурсы и поддерживать их идемпотентность. Terraform пригодится, если вы решите автоматизировать настройку серверной инфраструктуры. Знания из этого курса помогают программистам работать с облачной инфраструктурой.","kind":"basic","updated_at":"2026-01-20T11:54:10.765Z","language":"shell","duration_cache":10800,"skills":["Автоматически разворачивать инфраструктуру","Внедрять Terraform в уже существующий проект","Использовать переменные для настройки конфигурации","Настраивать зависимости между ресурсами"],"keywords":[],"lessons_count":10,"cover":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6OTA3OSwicHVyIjoiYmxvYl9pZCJ9fQ==--c5012d420e7691e22195551e248cc78c53b10bb9/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJwbmciLCJyZXNpemVfdG9fZmlsbCI6WzYwMCw0MDBdfSwicHVyIjoidmFyaWF0aW9uIn19--6067466c2912ca31a17eddee04b8cf2a38c6ad17/image.png"},"recommendedLandings":[{"stack":{"id":45,"slug":"infrastructure-automation","title":"Автоматизация IT-инфраструктуры","audience":"for_programmers","start_type":"anytime","pricing_model":"subscription","priority":"medium","kind":"track","state":"published","stack_state":"finished","order":1850,"duration_in_months":1},"id":78,"slug":"infrastructure-automation","title":"Автоматизация инфраструктуры","subtitle":"Навык, позволяющий автоматизировать развертывание и управление серверной инфраструктурой с Terraform","subtitle_for_lists":"Навык управления инфраструктурой с Terraform","locale":"ru","current":true,"duration_in_months_text":"1 месяц","stack_slug":"infrastructure-automation","price_text":"от 3 900 ₽","duration_text":"1 месяц","cover_list_variant":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6Mzk3NywicHVyIjoiYmxvYl9pZCJ9fQ==--bc5ef27286509b0ecf2f8ae6cbdce2376db3d394/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/500%20Internal%20Server%20Error-cuate.png"},{"stack":{"id":225,"slug":"devops-engineer-from-scratch","title":"DevOps-инженер с нуля","audience":"for_beginners","start_type":"weekly","pricing_model":"purchase","priority":"high","kind":"profession","state":"published","stack_state":"not_finished","order":50,"duration_in_months":14},"id":355,"slug":"devops-engineer-from-scratch","title":"DevOps-инженер с нуля","subtitle":"Полное погружение в DevOps: весь стек от Linux до Kubernetes","subtitle_for_lists":"Полное погружение в DevOps: весь стек от Linux до Kubernetes","locale":"ru","current":true,"duration_in_months_text":"14 месяцев","stack_slug":"devops-engineer-from-scratch","price_text":"от 6 792 ₽","duration_text":"14 месяцев","cover_list_variant":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6Mzk2NSwicHVyIjoiYmxvYl9pZCJ9fQ==--84278a1852c9c6fb13b80a69f395bac6e47a422e/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Cloud%20sync-bro.png"},{"stack":{"id":303,"slug":"devops-for-developers","title":"DevOps для разработчиков","audience":"for_programmers","start_type":"weekly","pricing_model":"purchase","priority":"medium","kind":"profession","state":"published","stack_state":"not_finished","order":150,"duration_in_months":3},"id":444,"slug":"devops-for-developers","title":"DevOps для разработчиков","subtitle":"Изучите деплой, автоматизацию, GitHub Actions, Docker, Ansible, Terraform, IaC","subtitle_for_lists":"Изучите деплой, автоматизацию, GitHub Actions, Docker, Ansible, Terraform, IaC","locale":"ru","current":true,"duration_in_months_text":"3 месяца","stack_slug":"devops-for-developers","price_text":"от 2 797 ₽","duration_text":"3 месяца","cover_list_variant":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MzY0MywicHVyIjoiYmxvYl9pZCJ9fQ==--74611367ca7524225d6b8670846088b4aa9fa1d2/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Server-bro.png"}],"lessonMemberUnit":null,"accessToLearnUnitExists":false,"accessToCourseExists":false},"url":"/courses/terraform-basics/lessons/example-with-db/theory_unit","version":"8f286f6358a90a7bef2263b3a6edf5a90a94fa42","encryptHistory":false,"clearHistory":false}"><style data-mantine-styles="true">:root, :host{--mantine-font-family: Arial, sans-serif;--mantine-font-family-headings: Arial, sans-serif;--mantine-heading-font-weight: normal;--mantine-radius-default: 0rem;--mantine-primary-color-filled: var(--mantine-color-indigo-filled);--mantine-primary-color-filled-hover: var(--mantine-color-indigo-filled-hover);--mantine-primary-color-light: var(--mantine-color-indigo-light);--mantine-primary-color-light-hover: var(--mantine-color-indigo-light-hover);--mantine-primary-color-light-color: var(--mantine-color-indigo-light-color);--mantine-spacing-xxl: calc(4rem * var(--mantine-scale));--mantine-font-size-xs: 12px;--mantine-font-size-sm: 14px;--mantine-font-size-md: 16px;--mantine-font-size-lg: clamp(16.0000px, calc(15.2727px + 0.2273vw), 18.0000px);--mantine-font-size-xl: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-font-size-display-3: clamp(32.0000px, calc(26.1818px + 1.8182vw), 48.0000px);--mantine-font-size-display-2: clamp(36.0000px, calc(25.8182px + 3.1818vw), 64.0000px);--mantine-font-size-display-1: clamp(40.0000px, calc(25.4545px + 4.5455vw), 80.0000px);--mantine-font-size-h1: clamp(28.0000px, calc(23.6364px + 1.3636vw), 40.0000px);--mantine-font-size-h2: clamp(24.0000px, calc(21.0909px + 0.9091vw), 32.0000px);--mantine-font-size-h3: clamp(20.0000px, calc(17.0909px + 0.9091vw), 28.0000px);--mantine-font-size-h4: clamp(16.0000px, calc(13.0909px + 0.9091vw), 24.0000px);--mantine-font-size-h5: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-font-size-h6: 1rem;--mantine-primary-color-0: var(--mantine-color-indigo-0);--mantine-primary-color-1: var(--mantine-color-indigo-1);--mantine-primary-color-2: var(--mantine-color-indigo-2);--mantine-primary-color-3: var(--mantine-color-indigo-3);--mantine-primary-color-4: var(--mantine-color-indigo-4);--mantine-primary-color-5: var(--mantine-color-indigo-5);--mantine-primary-color-6: var(--mantine-color-indigo-6);--mantine-primary-color-7: var(--mantine-color-indigo-7);--mantine-primary-color-8: var(--mantine-color-indigo-8);--mantine-primary-color-9: var(--mantine-color-indigo-9);--mantine-color-red-0: #ffeaea;--mantine-color-red-1: #fed4d4;--mantine-color-red-2: #f4a7a8;--mantine-color-red-3: #ec7878;--mantine-color-red-4: #e55050;--mantine-color-red-5: #e03131;--mantine-color-red-6: #e02829;--mantine-color-red-7: #c71a1c;--mantine-color-red-8: #b21218;--mantine-color-red-9: #9c0411;--mantine-color-violet-0: #fce9ff;--mantine-color-violet-1: #f1cfff;--mantine-color-violet-2: #e09bff;--mantine-color-violet-3: #d16fff;--mantine-color-violet-4: #be37fe;--mantine-color-violet-5: #b51afe;--mantine-color-violet-6: #b009ff;--mantine-color-violet-7: #9b00e4;--mantine-color-violet-8: #8a00cc;--mantine-color-violet-9: #7800b3;--mantine-color-indigo-0: #edecff;--mantine-color-indigo-1: #d6d5fe;--mantine-color-indigo-2: #aaa9f4;--mantine-color-indigo-3: #7b79eb;--mantine-color-indigo-4: #5451e4;--mantine-color-indigo-5: #3b37e0;--mantine-color-indigo-6: #2d2adf;--mantine-color-indigo-7: #1f1ec7;--mantine-color-indigo-8: #1819b2;--mantine-color-indigo-9: #0c149e;--mantine-color-cyan-0: #dffdff;--mantine-color-cyan-1: #caf5ff;--mantine-color-cyan-2: #99e8ff;--mantine-color-cyan-3: #64daff;--mantine-color-cyan-4: #3ccffe;--mantine-color-cyan-5: #24c8fe;--mantine-color-cyan-6: #00c2ff;--mantine-color-cyan-7: #00ade4;--mantine-color-cyan-8: #009acd;--mantine-color-cyan-9: #0085b5;--mantine-color-green-0: #e9fdec;--mantine-color-green-1: #d7f6dc;--mantine-color-green-2: #b0eab9;--mantine-color-green-3: #86df94;--mantine-color-green-4: #62d574;--mantine-color-green-5: #4ccf5f;--mantine-color-green-6: #3fcc54;--mantine-color-green-7: #2fb344;--mantine-color-green-8: #25a03b;--mantine-color-green-9: #138a2e;--mantine-color-yellow-0: #fff7e2;--mantine-color-yellow-1: #ffeecd;--mantine-color-yellow-2: #ffdc9c;--mantine-color-yellow-3: #ffc966;--mantine-color-yellow-4: #feb93a;--mantine-color-yellow-5: #feae1e;--mantine-color-yellow-6: #ffa90f;--mantine-color-yellow-8: #ca8200;--mantine-color-yellow-9: #af7000;--mantine-h1-font-size: clamp(28.0000px, calc(23.6364px + 1.3636vw), 40.0000px);--mantine-h1-font-weight: normal;--mantine-h2-font-size: clamp(24.0000px, calc(21.0909px + 0.9091vw), 32.0000px);--mantine-h2-font-weight: normal;--mantine-h3-font-size: clamp(20.0000px, calc(17.0909px + 0.9091vw), 28.0000px);--mantine-h3-font-weight: normal;--mantine-h4-font-size: clamp(16.0000px, calc(13.0909px + 0.9091vw), 24.0000px);--mantine-h4-font-weight: normal;--mantine-h5-font-size: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-h5-font-weight: normal;--mantine-h6-font-size: 1rem;--mantine-h6-font-weight: normal;}
:root[data-mantine-color-scheme="dark"], :host([data-mantine-color-scheme="dark"]){--mantine-color-anchor: var(--mantine-color-text);--mantine-color-dimmed: #495057;--mantine-color-dark-filled: var(--mantine-color-dark-5);--mantine-color-dark-filled-hover: var(--mantine-color-dark-6);--mantine-color-dark-light: rgba(105, 105, 105, 0.15);--mantine-color-dark-light-hover: rgba(105, 105, 105, 0.2);--mantine-color-dark-light-color: var(--mantine-color-dark-0);--mantine-color-dark-outline: var(--mantine-color-dark-1);--mantine-color-dark-outline-hover: rgba(184, 184, 184, 0.05);--mantine-color-gray-filled: var(--mantine-color-gray-5);--mantine-color-gray-filled-hover: var(--mantine-color-gray-6);--mantine-color-gray-light: rgba(222, 226, 230, 0.15);--mantine-color-gray-light-hover: rgba(222, 226, 230, 0.2);--mantine-color-gray-light-color: var(--mantine-color-gray-0);--mantine-color-gray-outline: var(--mantine-color-gray-1);--mantine-color-gray-outline-hover: rgba(241, 243, 245, 0.05);--mantine-color-red-filled: var(--mantine-color-red-5);--mantine-color-red-filled-hover: var(--mantine-color-red-6);--mantine-color-red-light: rgba(236, 120, 120, 0.15);--mantine-color-red-light-hover: rgba(236, 120, 120, 0.2);--mantine-color-red-light-color: var(--mantine-color-red-0);--mantine-color-red-outline: var(--mantine-color-red-1);--mantine-color-red-outline-hover: rgba(254, 212, 212, 0.05);--mantine-color-pink-filled: var(--mantine-color-pink-5);--mantine-color-pink-filled-hover: var(--mantine-color-pink-6);--mantine-color-pink-light: rgba(250, 162, 193, 0.15);--mantine-color-pink-light-hover: rgba(250, 162, 193, 0.2);--mantine-color-pink-light-color: var(--mantine-color-pink-0);--mantine-color-pink-outline: var(--mantine-color-pink-1);--mantine-color-pink-outline-hover: rgba(255, 222, 235, 0.05);--mantine-color-grape-filled: var(--mantine-color-grape-5);--mantine-color-grape-filled-hover: var(--mantine-color-grape-6);--mantine-color-grape-light: rgba(229, 153, 247, 0.15);--mantine-color-grape-light-hover: rgba(229, 153, 247, 0.2);--mantine-color-grape-light-color: var(--mantine-color-grape-0);--mantine-color-grape-outline: var(--mantine-color-grape-1);--mantine-color-grape-outline-hover: rgba(243, 217, 250, 0.05);--mantine-color-violet-filled: var(--mantine-color-violet-5);--mantine-color-violet-filled-hover: var(--mantine-color-violet-6);--mantine-color-violet-light: rgba(209, 111, 255, 0.15);--mantine-color-violet-light-hover: rgba(209, 111, 255, 0.2);--mantine-color-violet-light-color: var(--mantine-color-violet-0);--mantine-color-violet-outline: var(--mantine-color-violet-1);--mantine-color-violet-outline-hover: rgba(241, 207, 255, 0.05);--mantine-color-indigo-filled: var(--mantine-color-indigo-5);--mantine-color-indigo-filled-hover: var(--mantine-color-indigo-6);--mantine-color-indigo-light: rgba(123, 121, 235, 0.15);--mantine-color-indigo-light-hover: rgba(123, 121, 235, 0.2);--mantine-color-indigo-light-color: var(--mantine-color-indigo-0);--mantine-color-indigo-outline: var(--mantine-color-indigo-1);--mantine-color-indigo-outline-hover: rgba(214, 213, 254, 0.05);--mantine-color-blue-filled: var(--mantine-color-blue-5);--mantine-color-blue-filled-hover: var(--mantine-color-blue-6);--mantine-color-blue-light: rgba(116, 192, 252, 0.15);--mantine-color-blue-light-hover: rgba(116, 192, 252, 0.2);--mantine-color-blue-light-color: var(--mantine-color-blue-0);--mantine-color-blue-outline: var(--mantine-color-blue-1);--mantine-color-blue-outline-hover: rgba(208, 235, 255, 0.05);--mantine-color-cyan-filled: var(--mantine-color-cyan-5);--mantine-color-cyan-filled-hover: var(--mantine-color-cyan-6);--mantine-color-cyan-light: rgba(100, 218, 255, 0.15);--mantine-color-cyan-light-hover: rgba(100, 218, 255, 0.2);--mantine-color-cyan-light-color: var(--mantine-color-cyan-0);--mantine-color-cyan-outline: var(--mantine-color-cyan-1);--mantine-color-cyan-outline-hover: rgba(202, 245, 255, 0.05);--mantine-color-teal-filled: var(--mantine-color-teal-5);--mantine-color-teal-filled-hover: var(--mantine-color-teal-6);--mantine-color-teal-light: rgba(99, 230, 190, 0.15);--mantine-color-teal-light-hover: rgba(99, 230, 190, 0.2);--mantine-color-teal-light-color: var(--mantine-color-teal-0);--mantine-color-teal-outline: var(--mantine-color-teal-1);--mantine-color-teal-outline-hover: rgba(195, 250, 232, 0.05);--mantine-color-green-filled: var(--mantine-color-green-5);--mantine-color-green-filled-hover: var(--mantine-color-green-6);--mantine-color-green-light: rgba(134, 223, 148, 0.15);--mantine-color-green-light-hover: rgba(134, 223, 148, 0.2);--mantine-color-green-light-color: var(--mantine-color-green-0);--mantine-color-green-outline: var(--mantine-color-green-1);--mantine-color-green-outline-hover: rgba(215, 246, 220, 0.05);--mantine-color-lime-filled: var(--mantine-color-lime-5);--mantine-color-lime-filled-hover: var(--mantine-color-lime-6);--mantine-color-lime-light: rgba(192, 235, 117, 0.15);--mantine-color-lime-light-hover: rgba(192, 235, 117, 0.2);--mantine-color-lime-light-color: var(--mantine-color-lime-0);--mantine-color-lime-outline: var(--mantine-color-lime-1);--mantine-color-lime-outline-hover: rgba(233, 250, 200, 0.05);--mantine-color-yellow-filled: var(--mantine-color-yellow-5);--mantine-color-yellow-filled-hover: var(--mantine-color-yellow-6);--mantine-color-yellow-light: rgba(255, 201, 102, 0.15);--mantine-color-yellow-light-hover: rgba(255, 201, 102, 0.2);--mantine-color-yellow-light-color: var(--mantine-color-yellow-0);--mantine-color-yellow-outline: var(--mantine-color-yellow-1);--mantine-color-yellow-outline-hover: rgba(255, 238, 205, 0.05);--mantine-color-orange-filled: var(--mantine-color-orange-5);--mantine-color-orange-filled-hover: var(--mantine-color-orange-6);--mantine-color-orange-light: rgba(255, 192, 120, 0.15);--mantine-color-orange-light-hover: rgba(255, 192, 120, 0.2);--mantine-color-orange-light-color: var(--mantine-color-orange-0);--mantine-color-orange-outline: var(--mantine-color-orange-1);--mantine-color-orange-outline-hover: rgba(255, 232, 204, 0.05);--app-cta-gradient: linear-gradient(90deg, var(--mantine-color-blue-9) 0%, var(--mantine-color-cyan-7) 100%);--app-color-surface: #2e2e2e;}
:root[data-mantine-color-scheme="light"], :host([data-mantine-color-scheme="light"]){--mantine-color-anchor: var(--mantine-color-text);--mantine-color-dimmed: #495057;--mantine-color-red-light: rgba(224, 40, 41, 0.1);--mantine-color-red-light-hover: rgba(224, 40, 41, 0.12);--mantine-color-red-outline-hover: rgba(224, 40, 41, 0.05);--mantine-color-violet-light: rgba(176, 9, 255, 0.1);--mantine-color-violet-light-hover: rgba(176, 9, 255, 0.12);--mantine-color-violet-outline-hover: rgba(176, 9, 255, 0.05);--mantine-color-indigo-light: rgba(45, 42, 223, 0.1);--mantine-color-indigo-light-hover: rgba(45, 42, 223, 0.12);--mantine-color-indigo-outline-hover: rgba(45, 42, 223, 0.05);--mantine-color-cyan-light: rgba(0, 194, 255, 0.1);--mantine-color-cyan-light-hover: rgba(0, 194, 255, 0.12);--mantine-color-cyan-outline-hover: rgba(0, 194, 255, 0.05);--mantine-color-green-light: rgba(63, 204, 84, 0.1);--mantine-color-green-light-hover: rgba(63, 204, 84, 0.12);--mantine-color-green-outline-hover: rgba(63, 204, 84, 0.05);--mantine-color-yellow-light: rgba(255, 169, 15, 0.1);--mantine-color-yellow-light-hover: rgba(255, 169, 15, 0.12);--mantine-color-yellow-outline-hover: rgba(255, 169, 15, 0.05);--app-color-surface: #f1f3f5;--app-cta-gradient: linear-gradient(90deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-5) 100%);}</style><style data-mantine-styles="classes">@media (max-width: 35.99375em) {.mantine-visible-from-xs {display: none !important;}}@media (min-width: 36em) {.mantine-hidden-from-xs {display: none !important;}}@media (max-width: 47.99375em) {.mantine-visible-from-sm {display: none !important;}}@media (min-width: 48em) {.mantine-hidden-from-sm {display: none !important;}}@media (max-width: 61.99375em) {.mantine-visible-from-md {display: none !important;}}@media (min-width: 62em) {.mantine-hidden-from-md {display: none !important;}}@media (max-width: 74.99375em) {.mantine-visible-from-lg {display: none !important;}}@media (min-width: 75em) {.mantine-hidden-from-lg {display: none !important;}}@media (max-width: 87.99375em) {.mantine-visible-from-xl {display: none !important;}}@media (min-width: 88em) {.mantine-hidden-from-xl {display: none !important;}}</style><div style="position:absolute;top:0rem" class=""></div><div style="max-width:var(--container-size-xl);height:100%;min-height:0rem" class=""><style data-mantine-styles="inline">.__m__-_R_5ub_{--grid-gutter:0rem;}</style><div style="height:100%;min-height:0rem" class="m_410352e9 mantine-Grid-root __m__-_R_5ub_"><div class="m_dee7bd2f mantine-Grid-inner" style="height:100%"><style data-mantine-styles="inline">.__m__-_R_rdub_{--col-flex-grow:auto;--col-flex-basis:91.66666666666667%;--col-max-width:91.66666666666667%;}@media(min-width: 48em){.__m__-_R_rdub_{--col-flex-grow:auto;--col-flex-basis:83.33333333333334%;--col-max-width:83.33333333333334%;}}</style><div style="min-width:0rem;height:100%;min-height:0rem;display:flex" class="m_96bdd299 mantine-Grid-col __m__-_R_rdub_"><style data-mantine-styles="inline">.__m__-_R_6qrdub_{margin-top:0rem;padding-inline:var(--mantine-spacing-xs);width:100%;}@media(min-width: 48em){.__m__-_R_6qrdub_{margin-top:var(--mantine-spacing-xl);width:80%;}}@media(min-width: 62em){.__m__-_R_6qrdub_{padding-inline:var(--mantine-spacing-xl);}}</style><div style="margin-inline:auto;max-width:var(--mantine-breakpoint-xl)" class="__m__-_R_6qrdub_"><div style="color:var(--mantine-color-dimmed)" class="m_4451eb3a mantine-Center-root" data-inline="true"><div style="--ti-size:var(--ti-size-xs);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;margin-inline-end:calc(0.125rem * var(--mantine-scale));color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="xs"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-lock "><path d="M5 13a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-6"></path><path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0 -2 0"></path><path d="M8 11v-4a4 4 0 1 1 8 0v4"></path></svg></div><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Terraform: Основы</p></div><h1 style="--title-fw:var(--mantine-h1-font-weight);--title-lh:var(--mantine-h1-line-height);--title-fz:var(--mantine-h1-font-size);margin-bottom:var(--mantine-spacing-xl)" class="m_8a5d1357 mantine-Title-root" data-order="1">Теория: Практика Terraform</h1><script type="application/ld+json">{"@context":"https://schema.org","@type":"LearningResource","name":"Практика Terraform","inLanguage":"ru","isPartOf":{"@type":"LearningResource","name":"Terraform: Основы"},"isAccessibleForFree":"False","hasPart":{"@type":"WebPageElement","isAccessibleForFree":"False","cssSelector":".paywalled"}}</script><div class=""><div style="--alert-color:var(--mantine-color-indigo-light-color);margin-bottom:var(--mantine-spacing-lg);font-size:var(--mantine-font-size-lg)" class="m_66836ed3 mantine-Alert-root" id="mantine-_R_remqrdub_" role="alert" aria-describedby="mantine-_R_remqrdub_-body" aria-labelledby="mantine-_R_remqrdub_-title"><div class="m_a5d60502 mantine-Alert-wrapper"><div class="m_667f2a6a mantine-Alert-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-rocket "><path d="M4 13a8 8 0 0 1 7 7a6 6 0 0 0 3 -5a9 9 0 0 0 6 -8a3 3 0 0 0 -3 -3a9 9 0 0 0 -8 6a6 6 0 0 0 -5 3"></path><path d="M7 14a6 6 0 0 0 -3 6a6 6 0 0 0 6 -3"></path><path d="M14 9a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path></svg></div><div class="m_667c2793 mantine-Alert-body"><div class="m_6a03f287 mantine-Alert-title"><span id="mantine-_R_remqrdub_-title" class="m_698f4f23 mantine-Alert-label">Полный доступ к материалам</span></div><div id="mantine-_R_remqrdub_-body" class="m_7fa78076 mantine-Alert-message"><div style="--group-gap:var(--mantine-spacing-md);--group-align:center;--group-justify:space-between;--group-wrap:wrap" class="m_4081bf90 mantine-Group-root"><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Зарегистрируйтесь и получите доступ к этому и десяткам других курсов</p><a style="--button-height:var(--button-height-xs);--button-padding-x:var(--button-padding-x-xs);--button-fz:var(--mantine-font-size-xs);--button-bg:linear-gradient(45deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-filled) 100%);--button-hover:linear-gradient(45deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-filled) 100%);--button-color:var(--mantine-color-white);--button-bd:none" class="mantine-focus-auto mantine-active m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root" data-variant="gradient" data-size="xs" href="/u/new"><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label">Зарегистрироваться</span></span></a></div></div></div></div></div><div class="paywalled m_d08caa0 mantine-Typography-root"><p>Terraform позволяет хранить описание облачной инфраструктуры в виде кода. Мы можем получить готовую к работе инфраструктуру со всеми зависимостями, выполнив всего одну команду — <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">terraform apply</code>.</p>
<p>В этом уроке мы попрактикуемся с Terraform и попробуем с его помощью развернуть с нуля готовую к работе инфраструктуру: создадим базу и сервер, свяжем их друг с другом, поставим на сервер приложение и подключим его к базе.</p>
<h2 id="heading-2-1">Подключение Terraform к облаку</h2>
<p>Для начала организуем в проекте доступ к нашему облаку. Создадим папку с проектом и добавим в него файл <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">.gitignore</code>, в котором исключим рабочие файлы Terraform и секреты:</p>
<code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">secret.*
.terraform
*.backup</code>
<p>Добавим файл <em>providers.tf</em>, в котором опишем нужный нам провайдер и его параметры подключения:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">terraform {
required_providers {
yandex = {
source = "yandex-cloud/yandex"
version = "~> 0.84.0"
}
}
}
provider "yandex" {
token = var.yc_iam_token
cloud_id = var.yc_cloud_id
folder_id = var.yc_folder_id
zone = "ru-central1-a"
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Объявим используемые там переменные в <em>variables.tf</em>.</p>
<p>Рекомендуем настроить <a style="text-decoration:underline" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="https://cloud.yandex.ru/docs/cli/quickstart#install" rel="noopener noreferrer" target="_blank">консольную утилиту</a> для облака. В этом случае через нее можно получить токен и сразу сконвертировать его в нужную нам переменную:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">yc iam create-token | awk '{print "yc_iam_token = \""$1"\""}' > secret.auto.tfvars</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Команда выполнит запрос к облаку, извлечет из ответа строку с токеном и сохранит ее в файл <em>secret.auto.tfvars</em>.</p>
<p>Можно получить токен и <a style="text-decoration:underline" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="https://cloud.yandex.ru/docs/iam/operations/iam-token/create" rel="noopener noreferrer" target="_blank">другим способом</a>. Главное сохранить его в секретный <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">*.auto.tfvars</code>.</p>
<p>Допишем к токену значения переменных <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">yc_cloud_id и yc_folder_id</code> — это id облака и каталога, в которых будем работать.</p>
<p>Теперь перейдем к описанию ресурсов. Поскольку в примере мы используем облако Yandex, нам нужно будет развернуть базовую сетевую инфраструктуру, в которой будут размещаться виртуальные машины и кластеры баз данных.</p>
<h2 id="heading-2-2">Сетевая связность</h2>
<p>Для нашей инфраструктуры будет достаточно одной виртуальной сети <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">yandex_vpc_network</code> и подсети <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">yandex_vpc_subnet</code>:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">resource "yandex_vpc_network" "net" {
name = "tfhexlet"
}
resource "yandex_vpc_subnet" "subnet" {
name = "tfhexlet"
zone = "ru-central1-a"
network_id = yandex_vpc_network.net.id
v4_cidr_blocks = ["192.168.192.0/24"]
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Мы описали сетевые ресурсы и дали им одинаковые имена. Поскольку это разные ресурсы, конфликтов из-за одинакового имени не будет, а в интерфейсе облака мы явно увидим, что управляется Terraform.</p>
<p>Такая сетевая архитектура является спецификой облака Yandex. В нее можно не углубляться, главное для понимания:</p>
<ol>
<li>Подсеть предоставляет диапазон IP-адресов, которые мы можем использовать для наших серверов и баз данных</li>
<li>Сеть включает в себя подсети, ресурсы в которых могут общаться друг с другом</li>
</ol>
<p>Разместим в этой сетевой инфраструктуре облачную базу данных.</p>
<h2 id="heading-2-3">Кластер БД</h2>
<p>Опишем ресурс облачного кластера БД PostgreSQL. Добавим в него пользователя и создадим базу:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">resource "yandex_mdb_postgresql_cluster" "dbcluster" {
name = "tfhexlet"
environment = "PRESTABLE"
network_id = yandex_vpc_network.net.id
config {
version = var.yc_postgresql_version
resources {
resource_preset_id = "s2.micro"
disk_type_id = "network-ssd"
disk_size = 15
}
postgresql_config = {
max_connections = 100
}
}
maintenance_window {
type = "WEEKLY"
day = "SAT"
hour = 12
}
host {
zone = "ru-central1-a"
subnet_id = yandex_vpc_subnet.subnet.id
}
}
resource "yandex_mdb_postgresql_user" "dbuser" {
cluster_id = yandex_mdb_postgresql_cluster.dbcluster.id
name = var.db_user
password = var.db_password
depends_on = [yandex_mdb_postgresql_cluster.dbcluster]
}
resource "yandex_mdb_postgresql_database" "db" {
cluster_id = yandex_mdb_postgresql_cluster.dbcluster.id
name = var.db_name
owner = yandex_mdb_postgresql_user.dbuser.name
lc_collate = "en_US.UTF-8"
lc_type = "en_US.UTF-8"
depends_on = [yandex_mdb_postgresql_cluster.dbcluster]
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>В ресурсах пользователя и базы данных сразу пропишем зависимость от кластера.</p>
<p>Для создания пользователя и самой базы мы будем использовать sensitive-переменные <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">db_user</code>, <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">db_password</code> и <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">db_name</code>. Их необходимо объявить, а затем добавить значения в наш <em>secret.auto.tfvars</em>:</p>
<code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">db_name = "hexlet"
db_user = "me"
db_password = "bvcdV6sdBS7AXZs"</code>
<p>Теперь опишем сервер и настроим так, чтобы при развертывании инфраструктуры на нем запускалось приложение. Приложение будет подключаться к кластеру БД.</p>
<h2 id="heading-2-4">Сервер и приложение</h2>
<p>Запустим на сервере приложение <a style="text-decoration:underline" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="https://js.wiki/" rel="noopener noreferrer" target="_blank">wiki.js</a>, которое можно развернуть <a style="text-decoration:underline" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="https://docs.requarks.io/install/docker" rel="noopener noreferrer" target="_blank">с помощью Docker</a>. Это избавит нас от установки дополнительных пакетов на сервер. Единственное, что нам потребуется для запуска — наличие на сервере Docker.</p>
<p>Облако предоставляет готовый образ на базе Ubuntu с предустановленным Docker.</p>
<p><img style="--image-object-fit:contain;width:auto" class="m_9e117634 mantine-Image-root" src="/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6OTA4OCwicHVyIjoiYmxvYl9pZCJ9fQ==--5e9a9694472b9b7f5954424b0b33ed6440d83392/yandex_container_optimized.png" alt="image" loading="lazy"/></p>
<p>Мы можем скопировать id этого образа или использовать data source, чтобы получить актуальный образ по family ID: <em>container-optimized-image</em>.</p>
<p>Опишем сервер с помощью ресурса <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">yandex_compute_instance</code>:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">resource "yandex_compute_instance" "vm" {
name = "tfhexlet"
zone = "ru-central1-a"
resources {
cores = 2
memory = 2
}
boot_disk {
initialize_params {
image_id = data.yandex_compute_image.img.id
}
}
network_interface {
subnet_id = yandex_vpc_subnet.subnet.id
nat = true
}
metadata = {
ssh-keys = "ubuntu:${file("~/.ssh/id_rsa.pub")}"
}
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Мы задаем для сервера:</p>
<ul>
<li>Выделенные ресурсы</li>
<li>Образ виртуальной машины</li>
<li>Подсеть, в которую он будет помещен</li>
<li>Метаданные для инициации</li>
</ul>
<p>Также для сетевого интерфейса машины задаем <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">nat = true</code>, чтобы машине был выдан внешний IP-адрес.</p>
<p>Не хватает описания запуска приложения и передачи ему параметров базы данных. Сделаем это с помощью <strong>Terraform provisioner</strong>. Провижнеры позволяют выполнять различные операции в процессе развертывания инфраструктуры — создавать файлы, выполнять скрипты локально или на новых созданных машинах.</p>
<p>Мы будем использовать провижнер <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">remote-exec</code>. Он подключается к серверу с помощью параметров, описанных в блоке <em>connection</em>. Там он выполняет операции, описанные в блоке <em>inline</em>.</p>
<p>И <em>connection</em>, и <em>provisioner</em> нужно добавить внутрь ресурса <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">yandex_compute_instance</code>. Допишем их ниже блока <em>metadata</em>:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">...
metadata = {
ssh-keys = "ubuntu:${file("~/.ssh/id_rsa.pub")}"
}
connection {
type = "ssh"
user = "ubuntu"
private_key = file("~/.ssh/id_rsa")
host = self.network_interface[0].nat_ip_address
}
provisioner "remote-exec" {
inline = [
<<EOT
sudo docker run -d -p 0.0.0.0:80:3000 \
-e DB_TYPE=postgres \
-e DB_NAME=${var.db_name} \
-e DB_HOST=${yandex_mdb_postgresql_cluster.dbcluster.host.0.fqdn} \
-e DB_PORT=6432 \
-e DB_USER=${var.db_user} \
-e DB_PASS=${var.db_password} \
ghcr.io/requarks/wiki:2.5
EOT
]
}
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>В <em>connection</em> мы настроили подключение по ssh под пользователем ubuntu. Для этого использовали наш локальный приватный ключ. Провижнер будет подключаться по адресу <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">self.network_interface[0].nat_ip_address</code> — в это поле после создания сервера сохранится внешний IP-адрес сервера. Когда провижнер подключится, он выполнит команды, описанные в <em>inline</em>.</p>
<p>В блок <em>inline</em> команды передаются списком. У нас всего одна длинная команда, для удобства чтения мы записываем ее с переносами строки и передаем в heredoc-формате. Форматирование в примере выше является корректным: маркеры блока EOT для корректной работы не должны иметь отступов.</p>
<p>В итоге после создания виртуальной машины провижнер подключится к машине по ssh и запустит docker-контейнер <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">ghcr.io/requarks/wiki:2.5</code> с приложением. В параметры подключения к кластеру БД указываем те же переменные, которые использовали при описании кластера. Хост кластера БД станет известен после его создания. Чтобы получить его в скрипте, ссылаемся на поле кластера <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">yandex_mdb_postgresql_cluster.dbcluster.host.0.fqdn</code>.</p>
<p>Единственное, что осталось учесть, — это порядок создания ресурсов. В этом нам помогут зависимости.</p>
<h2 id="heading-2-5">Зависимости</h2>
<p>Технически возможна ситуация, когда виртуальная машина развернется раньше кластера БД, и приложение не сможет подключиться к базе. В этом случае нам хотелось бы иметь явную зависимость, ограничивающую создание виртуальных машин, пока кластер не готов к работе.</p>
<p>Добавим свойства <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">depends_on</code> в наших ресурсах. Кластер баз данных будет зависеть от ресурсов сети и подсети, а виртуальная машина — от кластера БД:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">resource "yandex_mdb_postgresql_cluster" "dbcluster" {
name = "tfhexlet"
...
depends_on = [yandex_vpc_network.net, yandex_vpc_subnet.subnet]
}
resource "yandex_compute_instance" "vm" {
name = "tfhexlet"
...
depends_on = [yandex_mdb_postgresql_cluster.dbcluster]
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Для сервера тоже нужна подсеть, но поскольку мы уже неявно обозначили зависимость от нее через кластер БД, в блок depends_on ее можно не добавлять.</p>
<p>Terraform при развертывании инфраструктуры строит свой внутренний граф зависимостей на основе ссылок на поля других ресурсов. Если инфраструктура несложная (как в этом уроке), он вполне способен правильно выстроить порядок сам. Но лучше сразу привыкнуть держать зависимости под контролем.</p>
<p>Также этот подход добавляет прозрачности тому, что и в каком порядке происходит при конфигурации инфраструктуры.</p>
<p>На этом этапе у нас всё готово. Осталось выполнить <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">terraform apply</code> и убедиться, что все создалось и запустилось.</p>
<h2 id="heading-2-6">Создание инфраструктуры</h2>
<p>Выполним <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">terraform apply</code>:</p>
<code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">Terraform will perform the following actions:
# yandex_compute_instance.vm will be created
+ resource "yandex_compute_instance" "vm" {
...
# yandex_mdb_postgresql_cluster.dbcluster will be created
+ resource "yandex_mdb_postgresql_cluster" "dbcluster" {
...</code>
<p>Запустим создание инфраструктуры и проследим за порядком:</p>
<code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">yandex_vpc_network.net: Creating...
yandex_vpc_network.net: Creation complete after 2s [id=enp49digsf8iut549fve]
yandex_vpc_subnet.subnet: Creating...
yandex_vpc_subnet.subnet: Creation complete after 1s [id=e9b0r2vjb50q1s37is8v]
yandex_mdb_postgresql_cluster.dbcluster: Creating...
yandex_mdb_postgresql_cluster.dbcluster: Still creating... [10s elapsed]
...
yandex_mdb_postgresql_cluster.dbcluster: Still creating... [6m20s elapsed]
yandex_mdb_postgresql_cluster.dbcluster: Creation complete after 6m23s [id=c9qnurf8pfd32bpmitmm]
yandex_mdb_postgresql_user.dbuser: Creating...
yandex_compute_instance.vm: Creating...
yandex_mdb_postgresql_user.dbuser: Still creating... [10s elapsed]
yandex_compute_instance.vm: Still creating... [10s elapsed]
...
andex_mdb_postgresql_user.dbuser: Creation complete after 24s [id=c9qnurf8pfd32bpmitmm:me]
yandex_mdb_postgresql_database.db: Creating...
yandex_compute_instance.vm: Still creating... [30s elapsed]
yandex_mdb_postgresql_database.db: Still creating... [10s elapsed]
yandex_compute_instance.vm: Still creating... [40s elapsed]
yandex_compute_instance.vm: Provisioning with 'remote-exec'...
yandex_compute_instance.vm (remote-exec): Connecting to remote host via SSH...
yandex_compute_instance.vm (remote-exec): Host: 51.250.1.251
yandex_compute_instance.vm (remote-exec): User: ubuntu
yandex_compute_instance.vm (remote-exec): Password: false
yandex_compute_instance.vm (remote-exec): Private key: true
yandex_compute_instance.vm (remote-exec): Certificate: false
yandex_compute_instance.vm (remote-exec): SSH Agent: true
yandex_compute_instance.vm (remote-exec): Checking Host Key: false
yandex_compute_instance.vm (remote-exec): Target Platform: unix
yandex_compute_instance.vm (remote-exec): Connected!
yandex_compute_instance.vm: Still creating... [1m0s elapsed]
yandex_compute_instance.vm (remote-exec): Unable to find image 'ghcr.io/requarks/wiki:2.5' locally
yandex_compute_instance.vm (remote-exec): 2.5: Pulling from requarks/wiki
yandex_compute_instance.vm (remote-exec): 31e352740f53: Pulling fs layer
yandex_compute_instance.vm (remote-exec): 2629b68d4311: Pulling fs layer
...
yandex_compute_instance.vm (remote-exec): Status: Downloaded newer image for ghcr.io/requarks/wiki:2.5
yandex_compute_instance.vm (remote-exec): c33a8f713d927bc0fa3f042a0d20a44b5abcc7798312ff58964d2f46664b4a17
yandex_compute_instance.vm: Creation complete after 1m23s [id=fhmdenv9loufk2m2gcj2]
Apply complete! Resources: 6 added, 0 changed, 0 destroyed.</code>
<p>По логу можно заметить:</p>
<ul>
<li>Заданный нами порядок соблюдается</li>
<li>Провижнер логирует в Terraform операции, которые выполняет</li>
<li>Создание сервера не считается завершенным до тех пор, пока провижнер не отработал полностью</li>
</ul>
<p>Приложению понадобится пара минут, чтобы установить все зависимости и накатить миграции в БД. После этого мы можем зайти на внешний IP сервера и увидеть админку wiki.js, готового к работе.</p>
<p><img style="--image-object-fit:contain;width:auto" class="m_9e117634 mantine-Image-root" src="/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6OTA4OSwicHVyIjoiYmxvYl9pZCJ9fQ==--c333478106c0d454bf1770e0dad331ed6761832d/appready.png" alt="app" loading="lazy"/></p>
<p>Можем создать администратора и добавить пару статей в нашу новую wiki. Приложение сохранит данные в базу.</p>
<p>После этого мы можем полностью удалить наш сервер через интерфейс облака и повторно вызвать <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">terraform apply</code>. Terraform не найдет <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">yandex_compute_instance.vm</code> и предложит создать его заново. Развернем сервер снова и зайдем на него по новому внешнему IP, который ему выдало облако. Там мы должны увидеть ту же самую wiki и уже созданные нами статьи.</p>
<p>Когда закончите практику, не забудьте выполнить <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">terraform destroy</code>, чтобы убрать из облака всю созданную в проекте инфраструктуру.</p>
<h2 id="heading-2-7">Выводы</h2>
<p>В этом уроке мы разобрались, как с помощью Terraform описать и поднять в облаке готовое к работе stateful-приложение. Такое решение с сервером и базой является вариантом применения подхода «неизменяемая инфраструктура» — когда мы полностью конфигурируем сервер на этапе создания. А если нужно что-то поменять в настройках — просто удаляем его и инициируем создание нового.</p></div><div style="margin-block:var(--mantine-spacing-xl)" class=""><h2 style="--title-fw:var(--mantine-h2-font-weight);--title-lh:var(--mantine-h2-line-height);--title-fz:var(--mantine-h2-font-size);margin-bottom:var(--mantine-spacing-md)" class="m_8a5d1357 mantine-Title-root" data-order="2">Рекомендуемые программы</h2><style data-mantine-styles="inline">.__m__-_R_2mremqrdub_{--carousel-slide-gap:var(--mantine-spacing-xs);--carousel-slide-size:70%;}@media(min-width: 36em){.__m__-_R_2mremqrdub_{--carousel-slide-gap:var(--mantine-spacing-xl);--carousel-slide-size:50%;}}</style><div style="--carousel-control-size:calc(2.5rem * var(--mantine-scale));--carousel-controls-offset:var(--mantine-spacing-sm);margin-bottom:var(--mantine-spacing-lg);padding-block:var(--mantine-spacing-sm);background:var(--app-color-surface)" class="m_17884d0f mantine-Carousel-root responsiveClassName" data-orientation="horizontal" data-include-gap-in-size="true"><div class="m_39bc3463 mantine-Carousel-controls" data-orientation="horizontal"><button class="mantine-focus-auto m_64f58e10 mantine-Carousel-control m_87cf2631 mantine-UnstyledButton-root" type="button" data-inactive="true" data-type="previous" tabindex="-1"><svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" style="transform:rotate(90deg);width:calc(1rem * var(--mantine-scale));height:calc(1rem * var(--mantine-scale));display:block"><path d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg></button><button class="mantine-focus-auto m_64f58e10 mantine-Carousel-control m_87cf2631 mantine-UnstyledButton-root" type="button" data-inactive="true" data-type="next" tabindex="-1"><svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" style="transform:rotate(-90deg);width:calc(1rem * var(--mantine-scale));height:calc(1rem * var(--mantine-scale));display:block"><path d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg></button></div><div class="m_a2dae653 mantine-Carousel-viewport" data-type="media"><div class="m_fcd81474 mantine-Carousel-container __m__-_R_2mremqrdub_" data-orientation="horizontal"><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/programs/infrastructure-automation?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card" target="_blank"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><div style="--group-gap:calc(0.25rem * var(--mantine-scale));--group-align:center;--group-justify:flex-start;--group-wrap:nowrap" class="m_4081bf90 mantine-Group-root"><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">1 месяц</span><span class="mantine-focus-auto m_b6d8b162 mantine-Text-root">·</span><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Для продвинутых</span></div><p style="margin-bottom:var(--mantine-spacing-sm);font-size:var(--mantine-font-size-h5);font-weight:bold" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Автоматизация инфраструктуры</p><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Навык управления инфраструктурой с Terraform</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6Mzk3NywicHVyIjoiYmxvYl9pZCJ9fQ==--bc5ef27286509b0ecf2f8ae6cbdce2376db3d394/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/500%20Internal%20Server%20Error-cuate.png" alt="Автоматизация инфраструктуры" loading="eager"/></div><div style="--group-gap:var(--mantine-spacing-md);--group-align:end;--group-justify:space-between;--group-wrap:wrap;margin-top:var(--mantine-spacing-xs)" class="m_4081bf90 mantine-Group-root"><p style="font-size:var(--mantine-font-size-xl)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">от 3 900 ₽</p><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Посмотреть →</p></div></div></div></a></div></div><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/programs/devops-engineer-from-scratch?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card" target="_blank"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><div style="--group-gap:calc(0.25rem * var(--mantine-scale));--group-align:center;--group-justify:flex-start;--group-wrap:nowrap" class="m_4081bf90 mantine-Group-root"><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">14 месяцев</span><span class="mantine-focus-auto m_b6d8b162 mantine-Text-root">·</span><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">С нуля</span></div><p style="margin-bottom:var(--mantine-spacing-sm);font-size:var(--mantine-font-size-h5);font-weight:bold" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">DevOps-инженер с нуля</p><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Полное погружение в DevOps: весь стек от Linux до Kubernetes</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6Mzk2NSwicHVyIjoiYmxvYl9pZCJ9fQ==--84278a1852c9c6fb13b80a69f395bac6e47a422e/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Cloud%20sync-bro.png" alt="DevOps-инженер с нуля" loading="eager"/></div><div style="--group-gap:var(--mantine-spacing-md);--group-align:end;--group-justify:space-between;--group-wrap:wrap;margin-top:var(--mantine-spacing-xs)" class="m_4081bf90 mantine-Group-root"><p style="font-size:var(--mantine-font-size-xl)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">от 6 792 ₽</p><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Посмотреть →</p></div></div></div></a></div></div><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/programs/devops-for-developers?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card" target="_blank"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><div style="--group-gap:calc(0.25rem * var(--mantine-scale));--group-align:center;--group-justify:flex-start;--group-wrap:nowrap" class="m_4081bf90 mantine-Group-root"><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">3 месяца</span><span class="mantine-focus-auto m_b6d8b162 mantine-Text-root">·</span><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Для продвинутых</span></div><p style="margin-bottom:var(--mantine-spacing-sm);font-size:var(--mantine-font-size-h5);font-weight:bold" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">DevOps для разработчиков</p><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Изучите деплой, автоматизацию, GitHub Actions, Docker, Ansible, Terraform, IaC</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MzY0MywicHVyIjoiYmxvYl9pZCJ9fQ==--74611367ca7524225d6b8670846088b4aa9fa1d2/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Server-bro.png" alt="DevOps для разработчиков" loading="eager"/></div><div style="--group-gap:var(--mantine-spacing-md);--group-align:end;--group-justify:space-between;--group-wrap:wrap;margin-top:var(--mantine-spacing-xs)" class="m_4081bf90 mantine-Group-root"><p style="font-size:var(--mantine-font-size-xl)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">от 2 797 ₽</p><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Посмотреть →</p></div></div></div></a></div></div><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><h2 style="--title-fw:var(--mantine-h2-font-weight);--title-lh:var(--mantine-h2-line-height);--title-fz:var(--mantine-h2-font-size);margin-bottom:var(--mantine-spacing-md);font-size:var(--mantine-font-size-h3)" class="m_8a5d1357 mantine-Title-root" data-order="2" data-responsive="true">Каталог</h2><p style="margin-bottom:auto" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Полный список доступных курсов по разным направлениям</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="/vite/assets/development-BVihs_d5.png" alt="Orientation"/></div></div></div></a></div></div></div></div></div></div></div></div></div><style data-mantine-styles="inline">.__m__-_R_1bdub_{--col-flex-grow:auto;--col-flex-basis:8.333333333333334%;--col-max-width:8.333333333333334%;}@media(min-width: 48em){.__m__-_R_1bdub_{--col-flex-grow:auto;--col-flex-basis:16.666666666666668%;--col-max-width:16.666666666666668%;}}</style><div style="min-width:0rem;height:100%;min-height:0rem" class="m_96bdd299 mantine-Grid-col __m__-_R_1bdub_"><div style="margin-inline:var(--mantine-spacing-xs)" class="mantine-visible-from-sm"><a style="--button-color:var(--mantine-color-white);margin-bottom:var(--mantine-spacing-lg);text-decoration:none" class="mantine-focus-auto m_849cf0da mantine-focus-auto m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses/terraform-basics/lessons/example-with-db/finish_unit?unit=theory" data-disabled="true" data-block="true" disabled=""><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label"><span style="margin-inline-end:var(--mantine-spacing-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Дальше</span>→</span></span></a><a style="padding-inline:0rem" class="mantine-focus-auto m_f0824112 mantine-NavLink-root m_87cf2631 mantine-UnstyledButton-root"><span class="m_690090b5 mantine-NavLink-section" data-position="left"><div style="--ti-size:var(--ti-size-sm);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="sm"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-list-numbers "><path d="M11 6h9"></path><path d="M11 12h9"></path><path d="M12 18h8"></path><path d="M4 16a2 2 0 1 1 4 0c0 .591 -.5 1 -1 1.5l-3 2.5h4"></path><path d="M6 10v-6l-2 2"></path></svg></div></span><div class="m_f07af9d2 mantine-NavLink-body"><span class="m_1f6ac4c4 mantine-NavLink-label">Навигация по теме</span><span class="m_57492dcc mantine-NavLink-description">Теория</span></div><span class="m_690090b5 mantine-NavLink-section" data-position="right"></span></a><div style="margin-block:var(--mantine-spacing-lg)" class="m_3eebeb36 mantine-Divider-root" data-orientation="horizontal" role="separator"></div><div style="margin-block:var(--mantine-spacing-lg)" class=""><div style="justify-content:space-between;margin-bottom:calc(0.1875rem * var(--mantine-scale));color:var(--mantine-color-dimmed);font-size:var(--mantine-font-size-xs)" class="m_8bffd616 mantine-Flex-root __m__-_R_qimrbdub_"><p style="font-size:var(--mantine-font-size-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Завершено</p><p style="font-size:var(--mantine-font-size-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">0 / 10</p></div><div style="--progress-size:var(--progress-size-sm)" class="m_db6d6462 mantine-Progress-root" data-size="sm"><div style="--progress-section-size:0%;--progress-section-color:var(--mantine-color-gray-filled)" class="m_2242eb65 mantine-Progress-section" role="progressbar" aria-valuemax="100" aria-valuemin="0" aria-valuenow="0" aria-valuetext="0%"></div></div></div><button style="padding-inline:0rem" class="mantine-focus-auto m_f0824112 mantine-NavLink-root m_87cf2631 mantine-UnstyledButton-root" type="button"><span class="m_690090b5 mantine-NavLink-section" data-position="left"><div style="--ti-size:var(--ti-size-sm);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="sm"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-message "><path d="M8 9h8"></path><path d="M8 13h6"></path><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12"></path></svg></div></span><div class="m_f07af9d2 mantine-NavLink-body"><span class="m_1f6ac4c4 mantine-NavLink-label">Обсуждения (архив)</span><span class="m_57492dcc mantine-NavLink-description"></span></div></button><div style="--toc-bg:var(--mantine-color-blue-light);--toc-color:var(--mantine-color-blue-light-color);--toc-size:var(--mantine-font-size-sm);--toc-radius:var(--mantine-radius-sm);margin-top:var(--mantine-spacing-xl)" class="m_bcaa9990 mantine-TableOfContents-root" data-variant="light" data-size="sm"></div></div><div class="mantine-hidden-from-sm"><div style="--stack-gap:0rem;--stack-align:stretch;--stack-justify:flex-start" class="m_6d731127 mantine-Stack-root"><a style="--button-color:var(--mantine-color-white);margin-bottom:var(--mantine-spacing-xs);padding:0rem;text-decoration:none" class="mantine-focus-auto m_849cf0da mantine-focus-auto m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses/terraform-basics/lessons/example-with-db/finish_unit?unit=theory" data-disabled="true" data-block="true" disabled=""><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label">→</span></span></a><button style="--ai-size:var(--ai-size-sm);--ai-bg:transparent;--ai-hover:var(--mantine-color-indigo-light-hover);--ai-color:var(--mantine-color-indigo-light-color);--ai-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;padding-block:var(--mantine-spacing-lg);color:inherit;width:100%" class="mantine-focus-auto m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="subtle" data-size="sm" data-disabled="true" type="button" disabled=""><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-list-numbers "><path d="M11 6h9"></path><path d="M11 12h9"></path><path d="M12 18h8"></path><path d="M4 16a2 2 0 1 1 4 0c0 .591 -.5 1 -1 1.5l-3 2.5h4"></path><path d="M6 10v-6l-2 2"></path></svg></span></button><button style="--ai-size:var(--ai-size-sm);--ai-bg:transparent;--ai-hover:var(--mantine-color-indigo-light-hover);--ai-color:var(--mantine-color-indigo-light-color);--ai-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;padding-block:var(--mantine-spacing-lg);color:inherit;width:100%" class="mantine-focus-auto mantine-active m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="subtle" data-size="sm" type="button"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-message "><path d="M8 9h8"></path><path d="M8 13h6"></path><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12"></path></svg></span></button></div></div></div></div></div></div></div>
</main>
<footer class="bg-dark fw-light text-light px-3 py-5">
<div class="row small">
<div class="col-12 col-sm-6 col-md-3">
<div class="h5 mb-3">Хекслет</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/about">О нас</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/testimonials">Отзывы</a>
</li>
<li>
<span class="nav-link link-light py-1 ps-0 external-link" data-href="https://b2b.hexlet.io" role="button">Корпоративное обучение</span>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/blog">Блог</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/qna">Вопросы и ответы</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/glossary">Глоссарий</a>
</li>
<li>
<span class="nav-link link-light py-1 ps-0 external-link" data-href="https://help.hexlet.io" data-target="_blank" role="button">Справка</span>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" target="_blank" rel="noopener noreferrer" href="/map">Карта сайта</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5 fw-normal mb-3">Направления</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_devops">DevOps
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_data_analytics">Аналитика
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_backend_development">Бэкенд
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_programming">Программирование
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_testing">Тестирование
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_front_end_dev">Фронтенд
</a></li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5">Профессии</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/devops-engineer-from-scratch">DevOps-инженер с нуля</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/go">Go-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/java">Java-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/python">Python-разработчик </a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/data-analytics">Аналитик данных</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/qa-engineer">Инженер по ручному тестированию</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/php">РНР-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/frontend">Фронтенд-разработчик</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5">Навыки</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/python-django-developer">Django</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/docker">Docker</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/php-laravel-developer">Laravel</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/postman">Postman</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/js-react-developer">React</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/js-rest-api">REST API в Node.js</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/spring-boot">Spring Boot</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/typescript">Typescript</a>
</li>
</ul>
</div>
</div>
<hr>
<div class="row">
<div class="col-12 col-sm-4 col-md-2">
<div class="fs-4">
<ul class="list-unstyled d-flex">
<li class="me-3">
<a aria-label="Telegram" target="_blank" class="link-light" rel="noopener noreferrer nofollow" href="https://t.me/hexlet_ru"><span class="bi bi-telegram"></span>
</a></li>
<li>
<a aria-label="Youtube" target="_blank" class="link-light" rel="noopener noreferrer nofollow" href="https://www.youtube.com/user/HexletUniversity"><span class="bi bi-youtube"></span>
</a></li>
</ul>
</div>
<div class="mb-2 d-flex flex-column">
<a class="link-light text-decoration-none" rel="nofollow" href="mailto:support@hexlet.io">support@hexlet.io</a>
<a class="link-light text-decoration-none py-2" target="_blank" href="https://t.me/hexlet_help_bot">t.me/hexlet_help_bot</a>
</div>
<ul class="list-unstyled d-flex">
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 external-link" rel="nofollow" data-href="https://hexlet.io/locale/switch?new_locale=en" data-target="_self" role="button"><span class="my-auto">EN</span>
</span></li>
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 opacity-100 external-link" rel="nofollow" data-href="https://ru.hexlet.io/locale/switch?new_locale=ru" data-target="_self" role="button"><span class="my-auto">RU</span>
</span></li>
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 external-link" rel="nofollow" data-href="https://kz.hexlet.io/locale/switch?new_locale=kz" data-target="_self" role="button"><span class="my-auto">KZ</span>
</span></li>
</ul>
</div>
<div class="col-12 col-sm-4 col-md-3">
<ul class="list-unstyled fs-4">
<li class="mb-3">
<a class="link-light text-decoration-none" href="tel:8%20800%20100%2022%2047">8 800 100 22 47</a>
<span class="d-block opacity-50 small">бесплатно по РФ</span>
</li>
<li>
<a class="link-light text-decoration-none" href="tel:%2B7%20495%20085%2021%2062">+7 495 085 21 62</a>
<span class="d-block opacity-50 small">бесплатно по Москве</span>
</li>
</ul>
</div>
<div class="col-12 col-sm-4 col-md-3">
<div class="small mb-3">Образовательные услуги оказываются на основании Л035-01298-77/01989008 от 14.03.2025</div>
<ul class="list-unstyled small">
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/legal">Правовая информация</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/offer">Оферта</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/license">Лицензия</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/contacts">Контакты</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-12 col-md-4 small">
<div class="mb-2">
<div>ООО «<a href="/" class="text-decoration-none link-light">Хекслет Рус</a>»</div>
<div>108813 г. Москва, вн.тер.г. поселение Московский,</div>
<div>г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3</div>
<div>ОГРН 1217300010476</div>
<div>ИНН 7325174845</div>
</div>
<hr>
<div>АНО ДПО «<a href="/" class="text-decoration-none link-light">Учебный центр «Хекслет</a>»</div>
<div>119331 г. Москва, вн. тер. г. муниципальный округ</div>
<div>Ломоносовский, пр-кт Вернадского, д. 29</div>
<div>ОГРН 1247700712390</div>
<div>ИНН 7736364948</div>
</div>
</div>
</footer>
<div id="root-assistant-offcanvas"></div>
<script src="/vite/assets/assistant-Bukl1lYy.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/init-BrRXra1y.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ErrorFallbackBlock-naDSYSy9.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/MarkdownBlock-DbyKWoR_.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/gon-D3e4yh1x.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/mantine-CGMYrt2Y.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/shiki-V011pkdv.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/utils-DRqSHbQE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/lib-XR8Qr8kR.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dist-GCHh59xr.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Box-B5-OOzBf.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/notifications.store-C-3AFSMn.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useIsomorphicEffect-HJ6VK0D3.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/lib-KSp6QbZ0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/axios-BEvgo0ym.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/classnames-l6ipYlLR.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dayjs.min-BkKovM-s.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/debounce-jMQ_Cf4f.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/i18next-BlSq9s7B.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/client-U9M77rxp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-dom-DaLxUz_h.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useTranslation-Bx1Cdrkz.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/compiler-runtime-6XxiPFnt.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/jsx-runtime-CwjcCKJi.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-CkL4ZRHB.js" as="script" crossorigin="anonymous">
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v67327c56f0bb4ef8b305cae61679db8f1769101564043" integrity="sha512-rdcWY47ByXd76cbCFzznIcEaCN71jqkWBBqlwhF1SY7KubdLKZiEGeP7AyieKZlGP9hbY/MhGrwXzJC/HulNyg==" data-cf-beacon='{"version":"2024.11.0","token":"d11015b65d11429ea6b4a2ef37dd7e0b","server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
</body>
</html>