Terraform хранит текущее состояние инфраструктуры в файле с расширением .tfstate. При выполнении операций Terraform идет в файл состояния и проверяет, какая инфраструктура уже развернута. На основе того, что есть в состоянии и что описано в проекте, Terraform понимает, что нужно сделать — создать инфраструктуру или изменить.
По умолчанию состояние Terraform хранится локально в вашей папке с проектом Terraform. Этого достаточно для небольших личных проектов, инфраструктурой которых управляем только мы. Но если мы работаем над проектом и его инфраструктурой в команде, возникают проблемы.
Например, два разработчика могут одновременно запустить изменение инфраструктуры. Или в разных ветках проекта могут храниться разные состояния инфраструктуры.
Удаленное хранение состояния Terraform изолирует состояние инфраструктуры от системы контроля версий. Также это позволяет ограничивать доступ к состоянию, если инфраструктура находится в процессе изменения кем-то еще. Как это делается — разберем в уроке.
Зачем нужно удаленное хранилище состояния
Когда мы управляем своей инфраструктурой на одной машине, схема работы с Terraform выглядит просто. Мы обновляем инфраструктуру, ее состояние изменяется.
У нас локально всегда хранится актуальный файл с состоянием:
Когда над проектом работает команда, процесс усложняется. Состояние инфраструктуры нужно как-то синхронизировать и актуализировать у всех участников команды.
Нам нужно делиться с командой актуальным состоянием инфраструктуры. Это проще делать через удаленное хранилище, где все члены команды могут найти это состояние.
Если организовать этот процесс через git-репозиторий, нужно сначала обновить инфраструктуру, затем добавить коммит с новым tfstate и отправить его в удаленный репозиторий. Коллега должен получить из репозитория актуальный tfstate, внести свои изменения, и в свою очередь отправить изменения в коде Terraform и новый файл состояния в Git:
Такой подход требует концентрации внимания и может привести к проблемам:
- Человеческий фактор. Один из разработчиков может забыть обновить state из репозитория и применит неправильные изменения на инфраструктуру
- Неконсистентность состояний. В git могут храниться разные версии состояния, и кто-нибудь применит неактуальные изменения на инфраструктуру
- Конфликт применения изменений. Terraform будет работать у обоих с локальным файлом состояния и не будет знать о том, что кто-то уже обновляет ту же инфраструктуру
Чтобы избавиться от этих проблем, Terraform предлагает собственный механизм удаленного хранения состояния. В нем большую часть операций контроля и синхронизации Terraform выполняет автоматически.
Концепция Terraform, описывающая удаленное хранение состояния, называется remote backend.
Что такое Terraform remote backends
В терминологии Terraform backend — это решение, которое отвечает за хранение состояния. Если состояние хранится удаленно — это remote backend.
При использовании remote backend Terraform сохраняет состояние в удаленное хранилище, а локально в tfstate хранит только информацию об этом удаленном хранилище.
В качестве хранилища может выступать любое облачное объектное хранилище по типу Amazon S3: Google Cloud Storage, Azure Storage, Yandex Cloud Storage и другие подобные решения. Также Terraform может использовать для удаленного хранения состояния HTTP-сервер, базу данных PostgreSQL или облачную платформу Terraform Cloud.
В схеме с remote backend при выполнении любых операций над инфраструктурой Terraform будет обращаться к удаленному файлу состояния, блокировать его на время выполнения изменений, затем перезаписывать этот файл с учетом внесенных изменений:
C remote backend Terraform самостоятельно будет решать ту проблему синхронизации состояний, которая возникала при командной работе с Git. Любой участник процесса при работе с инфраструктурой будет получать единственный актуальный файл состояния — у него не будет возможности получить некорректный.
Еще Terraform remote backend нативно поддерживает механизмы блокировки состояния. Если начать изменять какую-либо инфраструктуру, ее состояние не будет доступно никому другому, пока все изменения не будут применены. Это позволит исключить любые конфликты с параллельным применением изменений на одной и той же инфраструктуре.
Перейдем к практике и попробуем организовать хранение состояния удаленно. В качестве remote backend используем хранилище типа S3.
Как настроить хранение состояния в S3
Настроим хранение состояния в облаке YandexCloud. Будем использовать консольную утилиту облака YandexCLI. Она позволит создавать объекты в облаке через терминал.
Для достижения цели нам понадобятся:
- Хранилище файлов типа S3, в которое Terraform будет сохранять состояние инфраструктуры
- Бессерверная БД типа DynamoDB, в которой Terraform будет фиксировать блокировки
- Сервисный аккаунт облака, через который Terraform сможет управлять содержимым S3 и DynamoDB
Эти объекты в облаке нам предстоит создать. Технически мы можем использовать Terraform и для управления этими объектами тоже. Но к моменту выполнения terraform init в проекте они уже должны существовать. То есть объекты для хранения remote backend должны быть созданы в другом проекте Terraform с локальным состоянием.
Для упрощения процесса создадим нужные для remote backend объекты вручную. Они в дальнейшем почти никогда не меняются.
Подготавливаем облако для хранения состояния
Создадим в облаке S3-хранилище yc-hexlet-state объемом 10МБ. Этого хватит для хранения состояния надолго:
Дальше нам нужно сделать табличку в облачной базе данных, где Terraform будет фиксировать блокировки состояния:
Сохраним document_api_endpoint, он нам понадобится при конфигурации Terraform.
Таблицы в YDB YandexCli создавать не умеет, поэтому таблицу добавим в веб-интерфейсе облака. Найдем нашу базу в разделе «Managed Service for YDB»:
Нам нужен раздел «Навигация». Там найдем опцию «Создать таблицу» и создадим документную таблицу lock с колонкой LockID типа String, которая будет являться ключом партиционирования:
Далее через YandexCLI создадим сервисный аккаунт hexlet-remote, который будет сохранять состояние Terraform в облачное хранилище:
В ответе получим id нового сервисного аккаунта. Он понадобится, чтобы разрешить сервисному аккаунту взаимодействовать с хранилищем.
Проверим имя каталога YandexCloud, в котором работаем:
В примере выше используется каталог default.
Выдадим роли storage.editor и ydb.editor нашему сервисному аккаунту в этом каталоге:
В --subject указываем id сервисного аккаунта, который создали ранее.
Мы разрешили сервисному аккаунту hexlet-remote просматривать и изменять S3-хранилище в нашем каталоге, а также работать с базой в Managed YDB. Теперь настроим Terraform так, чтобы он подключался к хранилищу S3 и клал туда состояние.
Создадим у сервисного аккаунта ключи доступа к S3 хранилищу:
Сохраним значения key_id и secret. Они понадобятся для подключения Terraform к удаленному хранилищу.
Мы создали в облаке всё необходимое для удаленного хранения состояния. Теперь настроим Terraform так, чтобы он отправлял состояние в удаленное хранилище.
Описываем remote backend в проекте Terraform
Создадим в проекте файл backend.tf и вставим туда блок backend, описывающий хранение состояния:
У каждого типа backend в Terraform свой набор параметров. Мы используем backend "s3" и указываем для него:
-
endpoints — список эндпоинтов. Здесь мы указываем сетевой адрес хранилища S3. В случае YandexCloud — это всегда storage.yandexcloud.net. У других облачных провайдеров будет другой. А так же указываем document_api_endpoint созданной нами Managed YDB
-
region — свойство, характерное для S3 конкретного облачного провайдера. У YandexCloud это всегда ru-central1
-
bucket — имя хранилища, которое мы создали
-
key — директория в хранилище, куда Terraform будет класть файл состояния
-
access_key и secret_key — соответственно, key_id и secret созданного нами ключа доступа
-
dynamodb_table — имя таблицы, которую мы создали в Managed YDB через интерфейс облака
- Параметры skip служат для упрощения подключения к S3, в учебных целях
Инициируем инфраструктуру, выполнив terraform init.
Если изменить настройки backend в существующем проекте, необходимо будет вызвать модифицированную команду: terraform init -migrate-state. В этом случае Terraform постарается перенести локальный файл с состоянием в удаленное хранилище.
В итоге мы должны увидеть, что Terraform смог настроить работу с бэкендом s3:
Initializing the backend...
Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.
Теперь проверим, как работает наша блокировка. Откроем две вкладки терминала на проекте с Terraform. Выполним в первой вкладке terraform apply:
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# yandex_vpc_network.net will be created
+ resource "yandex_vpc_network" "net" {
...
}
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value:
Видим стандартный вывод Terraform, информирующий о готовности изменить инфраструктуру.
Теперь зайдем во вторую вкладку и попробуем выполнить terraform apply в этом же проекте:
Acquiring state lock. This may take a few moments...
╷
│ Error: Error acquiring the state lock
│
│ Error message: ConditionalCheckFailedException: Condition not satisfied
│ Lock Info:
│ ID: ac5bf646-b858-c819-c847-95f260d3c613
│ Path: yc-hexlet-state/hexlet-remote-state
│ Operation: OperationTypeApply
│ Who: hexlet@user
│ Version: 1.3.7
│ Created: 2023-07-06 11:38:14.794552864 +0000 UTC
│ Info:
│
Здесь мы видим блокировку. В тот момент, когда мы выполнили terraform apply в первый раз, Terraform создал запись о блокировке. Пока первая сессия работы с Terraform не завершится, остальные попытки менять инфраструктуру будут блокироваться.
Так мы с помощью удаленного хранения состояния в S3-хранилище и блокировки состояния в YDB добились того, что:
- Состояние инфраструктуры всегда будет одинаковым и актуальным у всей команды. Локально в .tfstate проекта будет храниться только адрес удаленного бэкенда
- Не возникнут конфликты одновременного обновления инфраструктуры двумя или более членами команды
Мы настроили удаленное хранение состояния. Осталось позаботиться о безопасности и о том, чтобы ключи для нашего хранилища не утекли в сеть.
Как работать с секретами backend
В работе с секретами backend стоит придерживаться тех же правил, что при работе с другими секретами Terraform. В примере выше мы прописали все настройки backend прямым текстом. Это может сделать наши облачные S3 и Managed YDB уязвимыми.
Доработаем хранение секретов backend, чтобы обезопасить нашу инфраструктуру.
В разделе backend мы не можем обращаться к обычным переменным Terraform. Переменные как сущность относятся к самому tfstate. А на момент обработки состояния Terraform еще не знает о переменных, которые существуют в инфраструктуре. Так секретами backend понадобится управлять отдельно.
Terraform позволяет подключать секреты состояния при выполнении terraform init по одному, либо в файле с помощью параметра -backend-config.
Вернемся к нашему backend.tf и уберем из него данные, которые можно считать чувствительными:
Мы убрали значения bucket, access_key, secret_key и параметры dynamodb_* для подключения к базе. Без них в открытом виде нашим данным об инфраструктуре ничего не угрожает.
Создадим отдельный файл secret.backend.tfvars и вставим в него то, что вынесли из backend.tf:
Имя secret.backend.tfvars будет соответствовать маске secret.* в .gitignore нашего проекта Terraform, и файл с этими секретами не попадет в сеть.
Подключим файл с секретами backend при инициации инфраструктуры:
terraform init -backend-config=secret.backend.tfvars
Инициация должна пройти успешно. С точки зрения Terraform ничего не изменилось. Он подгружает все параметры, описанные в -backend-config, внутрь блока backend.
Для передачи файла с секретами коллегам можно применять облачные менеджеры ключей, такие как AWS Secret Manager или Yandex Lockbox.
Выводы
Удаленное хранение состояния предполагает, что Terraform сохраняет файл с состоянием инфраструктуры terraform.tfstate в некоторое удаленное хранилище. Локально он хранит только адрес удаленного хранилища и параметры подключения к нему.
Удаленное хранение решает проблемы организации доступа к актуальному состоянию Terraform, когда инфраструктура управляется более чем с одной машины. Это в основном касается командной работы, но может быть удобно и при работе с личными проектами. Это позволяет использовать единый подход на проектах любого масштаба.
<!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 20:24:25 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="f6eJ4ZQJy2sGvcBIKVo3-2vM_ag-sr3htjZrJnkD3uOQdkLWZndmC7D-5NAlVceMq8XQAjaFQ0ML1vFyKwQ5jQ";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: Основы</title>
<meta name="description" content="Бекенды для хранения состояния / Terraform: Основы: Познакомимся с разными способами хранения состояния инфраструктуры вне репозитория для удобства совместной работы">
<link rel="canonical" href="https://ru.hexlet.io/courses/terraform-basics/lessons/remote-state/theory_unit">
<meta name="robots" content="noarchive">
<meta property="og:title" content="Бекенды для хранения состояния">
<meta property="og:title" content="Terraform: Основы">
<meta property="og:description" content="Бекенды для хранения состояния / Terraform: Основы: Познакомимся с разными способами хранения состояния инфраструктуры вне репозитория для удобства совместной работы">
<meta property="og:url" content="https://ru.hexlet.io/courses/terraform-basics/lessons/remote-state/theory_unit">
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="9M00YNsrKBjqHUP03wvibP_wvZtY3Eko5h5cg_zgtBEbHP9XKVWFeFxeZ2zTBBIbP_mQMVDrt4pb_sbXrudTfw" />
<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-26T20:24:25.591Z","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":"DHHG6_EBkyH6VMFIAyF2tgBv3fO4yAZu3Fv0RdrbsVDjoA3cA38-QUwX5dAPLobBwGbwWbD_-Mxhu24RiNxWPg","topics":[{"id":104291,"title":"Еще нужно добавить skip_requesting_account_id = true , иначе выдает ошибку Error: Retrieving AWS account details","plain_title":"Еще нужно добавить skiprequestingaccount_id = true , иначе выдает ошибку Error: Retrieving AWS account details ","creator":{"public_name":"Nicestas","id":899326,"is_tutor":false},"comments":[{"creator":{"public_name":"Ivan Mamtsev","id":294764,"is_tutor":true},"id":196876,"body":"Добавили, спасибо","topic_id":104291}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Бекенды для хранения состояния","entity_url":null,"active":true}},{"id":104724,"title":"Вы добавили `skip_requesting_account_id = true` только в одном месте - Как работать с секретами backend, но параметр нужен и в [Описываем remote backend в проекте Terraform](https://ru.hexlet.io/courses/terraform-basics/lessons/remote-state/theory_unit#opisyvaem-remote-backend-v-proekte-terraform)","plain_title":"Вы добавили skip_requesting_account_id = true только в одном месте - Как работать с секретами backend, но параметр нужен и в Описываем remote backend в проекте Terraform (https://ru.hexlet.io/courses/terraform-basics/lessons/remote-state/theory_unit#opisyvaem-remote-backend-v-proekte-terraform) ","creator":{"public_name":"Павел Михайлов","id":181221,"is_tutor":false},"comments":[{"creator":{"public_name":"Ivan Mamtsev","id":294764,"is_tutor":true},"id":197605,"body":"Ага, добавил","topic_id":104724},{"creator":{"public_name":"Ivan Mamtsev","id":294764,"is_tutor":true},"id":197330,"body":"Спасибо, там тоже пофиксил","topic_id":104724},{"creator":{"public_name":"Павел Михайлов","id":181221,"is_tutor":false},"id":197417,"body":"и еще одна строчка нужна )\nпри ошибке `Error: Failed to save state`, надо добавить `skip_s3_checksum = true` - [docs](https://yandex.cloud/en/docs/tutorials/infrastructure-management/terraform-state-storage#deploy)\n```\nError: Failed to save state\n\nError saving state: failed to upload state: operation error S3: PutObject, https response error StatusCode: 400, RequestID: b92df937c5cae97e, HostID: , api error XAmzContentSHA256Mismatch: The provided 'x-amz-content-sha256' header does not match what was computed.\n```","topic_id":104724}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Бекенды для хранения состояния","entity_url":null,"active":true}}],"lesson":{"exercise":null,"units":[{"id":5522,"name":"theory","url":"/courses/terraform-basics/lessons/remote-state/theory_unit"},{"id":13157,"name":"quiz","url":"/courses/terraform-basics/lessons/remote-state/quiz_unit"}],"links":[],"ordered_units":[{"id":5522,"name":"theory","url":"/courses/terraform-basics/lessons/remote-state/theory_unit"},{"id":13157,"name":"quiz","url":"/courses/terraform-basics/lessons/remote-state/quiz_unit"}],"id":2443,"slug":"remote-state","state":"approved","name":"Бекенды для хранения состояния","course_order":500,"goal":"Познакомимся с разными способами хранения состояния инфраструктуры вне репозитория для удобства совместной работы","self_study":"1. Зарегистрируйтесь в на Terraform Cloud\n2. Измените хранение стейта Terraform на удаленное в облаке Terraform Cloud\n\nПроверьте, что стейт не хранится в истории Git.\n","theory_video_provider":null,"theory_video_uid":null,"theory":"Terraform хранит текущее состояние инфраструктуры в файле с расширением `.tfstate`. При выполнении операций Terraform идет в файл состояния и проверяет, какая инфраструктура уже развернута. На основе того, что есть в состоянии и что описано в проекте, Terraform понимает, что нужно сделать — создать инфраструктуру или изменить.\n\nПо умолчанию состояние Terraform хранится локально в вашей папке с проектом Terraform. Этого достаточно для небольших личных проектов, инфраструктурой которых управляем только мы. Но если мы работаем над проектом и его инфраструктурой в команде, возникают проблемы.\n\nНапример, два разработчика могут одновременно запустить изменение инфраструктуры. Или в разных ветках проекта могут храниться разные состояния инфраструктуры.\n\nУдаленное хранение состояния Terraform изолирует состояние инфраструктуры от системы контроля версий. Также это позволяет ограничивать доступ к состоянию, если инфраструктура находится в процессе изменения кем-то еще. Как это делается — разберем в уроке.\n\n## Зачем нужно удаленное хранилище состояния\n\nКогда мы управляем своей инфраструктурой на одной машине, схема работы с Terraform выглядит просто. Мы обновляем инфраструктуру, ее состояние изменяется.\n\nУ нас локально всегда хранится актуальный файл с состоянием:\n\n\n\nКогда над проектом работает команда, процесс усложняется. Состояние инфраструктуры нужно как-то синхронизировать и актуализировать у всех участников команды.\n\nНам нужно делиться с командой актуальным состоянием инфраструктуры. Это проще делать через удаленное хранилище, где все члены команды могут найти это состояние.\n\nЕсли организовать этот процесс через git-репозиторий, нужно сначала обновить инфраструктуру, затем добавить коммит с новым `tfstate` и отправить его в удаленный репозиторий. Коллега должен получить из репозитория актуальный `tfstate`, внести свои изменения, и в свою очередь отправить изменения в коде Terraform и новый файл состояния в Git:\n\n\n\nТакой подход требует концентрации внимания и может привести к проблемам:\n\n* Человеческий фактор. Один из разработчиков может забыть обновить state из репозитория и применит неправильные изменения на инфраструктуру\n* Неконсистентность состояний. В git могут храниться разные версии состояния, и кто-нибудь применит неактуальные изменения на инфраструктуру\n* Конфликт применения изменений. Terraform будет работать у обоих с локальным файлом состояния и не будет знать о том, что кто-то уже обновляет ту же инфраструктуру\n\nЧтобы избавиться от этих проблем, Terraform предлагает собственный механизм удаленного хранения состояния. В нем большую часть операций контроля и синхронизации Terraform выполняет автоматически.\n\nКонцепция Terraform, описывающая удаленное хранение состояния, называется remote backend.\n\n## Что такое Terraform remote backends\n\nВ терминологии Terraform [**backend**](https://developer.hashicorp.com/terraform/language/settings/backends/configuration) — это решение, которое отвечает за хранение состояния. Если состояние хранится удаленно — это **remote backend**.\n\nПри использовании remote backend Terraform сохраняет состояние в удаленное хранилище, а локально в `tfstate` хранит только информацию об этом удаленном хранилище.\n\nВ качестве хранилища может выступать любое [облачное объектное хранилище](https://developer.hashicorp.com/terraform/language/settings/backends/s3) по типу Amazon S3: Google Cloud Storage, Azure Storage, Yandex Cloud Storage и другие подобные решения. Также Terraform может использовать для удаленного хранения состояния HTTP-сервер, базу данных PostgreSQL или облачную платформу Terraform Cloud.\n\nВ схеме с remote backend при выполнении любых операций над инфраструктурой Terraform будет обращаться к удаленному файлу состояния, блокировать его на время выполнения изменений, затем перезаписывать этот файл с учетом внесенных изменений:\n\n\n\nC remote backend Terraform самостоятельно будет решать ту проблему синхронизации состояний, которая возникала при командной работе с Git. Любой участник процесса при работе с инфраструктурой будет получать единственный актуальный файл состояния — у него не будет возможности получить некорректный.\n\nЕще Terraform remote backend нативно поддерживает механизмы блокировки состояния. Если начать изменять какую-либо инфраструктуру, ее состояние не будет доступно никому другому, пока все изменения не будут применены. Это позволит исключить любые конфликты с параллельным применением изменений на одной и той же инфраструктуре.\n\nПерейдем к практике и попробуем организовать хранение состояния удаленно. В качестве remote backend используем хранилище типа S3.\n\n## Как настроить хранение состояния в S3\n\nНастроим хранение состояния в облаке YandexCloud. Будем использовать консольную утилиту облака [*YandexCLI*](https://cloud.yandex.ru/docs/cli/quickstart). Она позволит создавать объекты в облаке через терминал.\n\nДля достижения цели нам понадобятся:\n\n* Хранилище файлов типа S3, в которое Terraform будет сохранять состояние инфраструктуры\n* Бессерверная БД типа DynamoDB, в которой Terraform будет фиксировать блокировки\n* Сервисный аккаунт облака, через который Terraform сможет управлять содержимым S3 и DynamoDB\n\nЭти объекты в облаке нам предстоит создать. Технически мы можем использовать Terraform и для управления этими объектами тоже. Но к моменту выполнения `terraform init` в проекте они уже должны существовать. То есть объекты для хранения remote backend должны быть созданы в другом проекте Terraform с локальным состоянием.\n\nДля упрощения процесса создадим нужные для remote backend объекты вручную. Они в дальнейшем почти никогда не меняются.\n\n### Подготавливаем облако для хранения состояния\n\nСоздадим в облаке S3-хранилище `yc-hexlet-state` объемом 10МБ. Этого хватит для хранения состояния надолго:\n\n```bash\nyc storage bucket create --name yc-hexlet-state --max-size 10000000\n```\n\nДальше нам нужно сделать табличку в облачной базе данных, где Terraform будет фиксировать блокировки состояния:\n\n```bash\nyc ydb database create terraform-state-lock --serverless\n\ndone (7s)\nid: etnpkn3gs4s56qk9g7kf\nfolder_id: ...\ncreated_at: ...\nname: terraform-state-lock\n...\ndocument_api_endpoint: https://docapi.serverless.yandexcloud.net/ru-central1/b1gjrod3dvqni46u3paj/etnpkn3gs4s56qk9g7kf\n```\n\nСохраним `document_api_endpoint`, он нам понадобится при конфигурации Terraform.\n\nТаблицы в YDB YandexCli создавать не умеет, поэтому таблицу добавим в веб-интерфейсе облака. Найдем нашу базу в разделе «Managed Service for YDB»:\n\n\n\nНам нужен раздел «Навигация». Там найдем опцию «Создать таблицу» и создадим документную таблицу `lock` с колонкой `LockID` типа String, которая будет являться ключом партиционирования:\n\n\n\nДалее через YandexCLI создадим сервисный аккаунт `hexlet-remote`, который будет сохранять состояние Terraform в облачное хранилище:\n\n```bash\nyc iam service-account create --name hexlet-remote --description \"SA to manage terraform state\"\n\nid: ajejk11p9ls1vvhc12mb\nfolder_id: ...\ncreated_at: ...\nname: hexlet-remote\ndescription: SA to manage terraform state\n```\n\nВ ответе получим id нового сервисного аккаунта. Он понадобится, чтобы разрешить сервисному аккаунту взаимодействовать с хранилищем.\n\nПроверим имя каталога YandexCloud, в котором работаем:\n\n```bash\nyc resource-manager folder list\n+----------------------+---------+--------+--------+\n| ID | NAME | LABELS | STATUS |\n+----------------------+---------+--------+--------+\n| ... | default | | ACTIVE |\n+----------------------+---------+--------+--------+\n```\n\nВ примере выше используется каталог `default`.\n\nВыдадим роли `storage.editor` и `ydb.editor` нашему сервисному аккаунту в этом каталоге:\n\n```bash\nyc resource-manager folder add-access-binding default --role storage.editor --subject serviceAccount:ajejk11p9ls1vvhc12mb\n\ndone (1s)\neffective_deltas:\n - action: ADD\n access_binding:\n role_id: storage.editor\n subject:\n id: ajejk11p9ls1vvhc12mb\n type: serviceAccount\n\nyc resource-manager folder add-access-binding default --role ydb.editor --subject serviceAccount:ajejk11p9ls1vvhc12mb\n\ndone (1s)\neffective_deltas:\n - action: ADD\n access_binding:\n role_id: ydb.editor\n subject:\n id: ajejk11p9ls1vvhc12mb\n type: serviceAccount\n```\n\nВ `--subject` указываем `id` сервисного аккаунта, который создали ранее.\n\nМы разрешили сервисному аккаунту `hexlet-remote` просматривать и изменять S3-хранилище в нашем каталоге, а также работать с базой в Managed YDB. Теперь настроим Terraform так, чтобы он подключался к хранилищу S3 и клал туда состояние.\n\nСоздадим у сервисного аккаунта ключи доступа к S3 хранилищу:\n\n```bash\nyc iam access-key create --service-account-name hexlet-remote --description \"terraform s3 backend access key\"\n\naccess_key:\n id: ...\n service_account_id: ...\n created_at: ...\n description: ...\n key_id: YCABX6vQXtCjoKu_oB7QabuZO\nsecret: YCOL4xZ1tdpduS46z_YTlvDzYUwv8xBK_UuRq18m\n```\n\nСохраним значения `key_id` и `secret`. Они понадобятся для подключения Terraform к удаленному хранилищу.\n\nМы создали в облаке всё необходимое для удаленного хранения состояния. Теперь настроим Terraform так, чтобы он отправлял состояние в удаленное хранилище.\n\n### Описываем remote backend в проекте Terraform\n\nСоздадим в проекте файл `backend.tf` и вставим туда блок `backend`, описывающий хранение состояния:\n\n```terraform\nterraform {\n backend \"s3\" {\n endpoints = {\n s3 = \"https://storage.yandexcloud.net\"\n dynamodb = \"https://docapi.serverless.yandexcloud.net/ru-central1/b1gjrod3dvqni46u3paj/etnpkn3gs4s56qk9g7kf\"\n }\n region = \"ru-central1\"\n bucket = \"yc-hexlet-state\"\n key = \"hexlet-remote-state\"\n access_key = \"YCABX6vQXtCjoKu_oB7QabuZO\"\n secret_key = \"YCOL4xZ1tdpduS46z_YTlvDzYUwv8xBK_UuRq18m\"\n dynamodb_table = \"lock\"\n skip_region_validation = true\n skip_credentials_validation = true\n skip_requesting_account_id = true\n skip_s3_checksum = true\n }\n}\n```\n\nУ каждого типа backend в Terraform свой набор параметров. Мы используем `backend \"s3\"` и указываем для него:\n\n* **endpoints** — список эндпоинтов. Здесь мы указываем сетевой адрес хранилища S3. В случае YandexCloud — это всегда `storage.yandexcloud.net`. У других облачных провайдеров будет другой. А так же указываем `document_api_endpoint` созданной нами Managed YDB\n* **region** — свойство, характерное для S3 конкретного облачного провайдера. У YandexCloud это всегда `ru-central1`\n* **bucket** — имя хранилища, которое мы создали\n* **key** — директория в хранилище, куда Terraform будет класть файл состояния\n* **access_key** и **secret_key** — соответственно, `key_id` и `secret` созданного нами ключа доступа\n* **dynamodb_table** — имя таблицы, которую мы создали в Managed YDB через интерфейс облака\n* Параметры **skip** служат для упрощения подключения к S3, в учебных целях\n\nИнициируем инфраструктуру, выполнив `terraform init`.\n\nЕсли изменить настройки backend в существующем проекте, необходимо будет вызвать модифицированную команду: `terraform init -migrate-state`. В этом случае Terraform постарается перенести локальный файл с состоянием в удаленное хранилище.\n\nВ итоге мы должны увидеть, что Terraform смог настроить работу с бэкендом s3:\n\n```\nInitializing the backend...\n\nSuccessfully configured the backend \"s3\"! Terraform will automatically\nuse this backend unless the backend configuration changes.\n```\n\nТеперь проверим, как работает наша блокировка. Откроем две вкладки терминала на проекте с Terraform. Выполним в первой вкладке `terraform apply`:\n\n```\nTerraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:\n + create\n\nTerraform will perform the following actions:\n\n # yandex_vpc_network.net will be created\n + resource \"yandex_vpc_network\" \"net\" {\n ...\n }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nDo you want to perform these actions?\n Terraform will perform the actions described above.\n Only 'yes' will be accepted to approve.\n\n Enter a value:\n```\n\nВидим стандартный вывод Terraform, информирующий о готовности изменить инфраструктуру.\n\nТеперь зайдем во вторую вкладку и попробуем выполнить `terraform apply` в этом же проекте:\n\n```\nAcquiring state lock. This may take a few moments...\n╷\n│ Error: Error acquiring the state lock\n│\n│ Error message: ConditionalCheckFailedException: Condition not satisfied\n│ Lock Info:\n│ ID: ac5bf646-b858-c819-c847-95f260d3c613\n│ Path: yc-hexlet-state/hexlet-remote-state\n│ Operation: OperationTypeApply\n│ Who: hexlet@user\n│ Version: 1.3.7\n│ Created: 2023-07-06 11:38:14.794552864 +0000 UTC\n│ Info:\n│\n```\n\nЗдесь мы видим блокировку. В тот момент, когда мы выполнили `terraform apply` в первый раз, Terraform создал запись о блокировке. Пока первая сессия работы с Terraform не завершится, остальные попытки менять инфраструктуру будут блокироваться.\n\nТак мы с помощью удаленного хранения состояния в S3-хранилище и блокировки состояния в YDB добились того, что:\n\n* Состояние инфраструктуры всегда будет одинаковым и актуальным у всей команды. Локально в `.tfstate` проекта будет храниться только адрес удаленного бэкенда\n* Не возникнут конфликты одновременного обновления инфраструктуры двумя или более членами команды\n\nМы настроили удаленное хранение состояния. Осталось позаботиться о безопасности и о том, чтобы ключи для нашего хранилища не утекли в сеть.\n\n## Как работать с секретами backend\n\nВ работе с секретами backend стоит придерживаться тех же правил, что при работе с другими секретами Terraform. В примере выше мы прописали все настройки backend прямым текстом. Это может сделать наши облачные S3 и Managed YDB уязвимыми.\n\nДоработаем хранение секретов backend, чтобы обезопасить нашу инфраструктуру.\n\nВ разделе backend мы не можем обращаться к обычным переменным Terraform. Переменные как сущность относятся к самому `tfstate`. А на момент обработки состояния Terraform еще не знает о переменных, которые существуют в инфраструктуре. Так секретами backend понадобится управлять отдельно.\n\nTerraform позволяет подключать секреты состояния при выполнении `terraform init` по одному, либо в файле с помощью параметра `-backend-config`.\n\nВернемся к нашему `backend.tf` и уберем из него данные, которые можно считать чувствительными:\n\n```terraform\nterraform {\n backend \"s3\" {\n endpoint = \"storage.yandexcloud.net\"\n region = \"ru-central1\"\n key = \"hexlet-remote-state\"\n skip_region_validation = true\n skip_credentials_validation = true\n skip_requesting_account_id = true\n skip_s3_checksum = true\n }\n}\n```\n\nМы убрали значения `bucket`, `access_key`, `secret_key` и параметры `dynamodb_*` для подключения к базе. Без них в открытом виде нашим данным об инфраструктуре ничего не угрожает.\n\nСоздадим отдельный файл `secret.backend.tfvars` и вставим в него то, что вынесли из `backend.tf`:\n\n```terraform\nbucket = \"yc-hexlet-state\"\naccess_key = \"YCABX6vQXtCjoKu_oB7QabuZO\"\nsecret_key = \"YCOL4xZ1tdpduS46z_YTlvDzYUwv8xBK_UuRq18m\"\ndynamodb_endpoint = \"https://docapi.serverless.yandexcloud.net/ru-central1/b1gjrod3dvqni46u3paj/etnpkn3gs4s56qk9g7kf\"\ndynamodb_table = \"lock\"\n```\n\nИмя `secret.backend.tfvars` будет соответствовать маске `secret.*` в .gitignore нашего проекта Terraform, и файл с этими секретами не попадет в сеть.\n\nПодключим файл с секретами backend при инициации инфраструктуры:\n\n```\nterraform init -backend-config=secret.backend.tfvars\n```\n\nИнициация должна пройти успешно. С точки зрения Terraform ничего не изменилось. Он подгружает все параметры, описанные в `-backend-config`, внутрь блока `backend`.\n\nДля передачи файла с секретами коллегам можно применять облачные менеджеры ключей, такие как AWS Secret Manager или Yandex Lockbox.\n\n## Выводы\n\nУдаленное хранение состояния предполагает, что Terraform сохраняет файл с состоянием инфраструктуры `terraform.tfstate` в некоторое удаленное хранилище. Локально он хранит только адрес удаленного хранилища и параметры подключения к нему.\n\nУдаленное хранение решает проблемы организации доступа к актуальному состоянию Terraform, когда инфраструктура управляется более чем с одной машины. Это в основном касается командной работы, но может быть удобно и при работе с личными проектами. Это позволяет использовать единый подход на проектах любого масштаба.\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/remote-state/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">Теория: Бекенды для хранения состояния</h1><script type="application/ld+json">{"@context":"https://schema.org","@type":"LearningResource","name":"Бекенды для хранения состояния","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">.tfstate</code>. При выполнении операций Terraform идет в файл состояния и проверяет, какая инфраструктура уже развернута. На основе того, что есть в состоянии и что описано в проекте, Terraform понимает, что нужно сделать — создать инфраструктуру или изменить.</p>
<p>По умолчанию состояние Terraform хранится локально в вашей папке с проектом Terraform. Этого достаточно для небольших личных проектов, инфраструктурой которых управляем только мы. Но если мы работаем над проектом и его инфраструктурой в команде, возникают проблемы.</p>
<p>Например, два разработчика могут одновременно запустить изменение инфраструктуры. Или в разных ветках проекта могут храниться разные состояния инфраструктуры.</p>
<p>Удаленное хранение состояния Terraform изолирует состояние инфраструктуры от системы контроля версий. Также это позволяет ограничивать доступ к состоянию, если инфраструктура находится в процессе изменения кем-то еще. Как это делается — разберем в уроке.</p>
<h2 id="heading-2-1">Зачем нужно удаленное хранилище состояния</h2>
<p>Когда мы управляем своей инфраструктурой на одной машине, схема работы с Terraform выглядит просто. Мы обновляем инфраструктуру, ее состояние изменяется.</p>
<p>У нас локально всегда хранится актуальный файл с состоянием:</p>
<p><img style="--image-object-fit:contain;width:auto" class="m_9e117634 mantine-Image-root" src="/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6OTA5MCwicHVyIjoiYmxvYl9pZCJ9fQ==--2210ee7774844bad22c93cf5dd16540362cbd116/lstate.png" alt="Terraform local state" loading="lazy"/></p>
<p>Когда над проектом работает команда, процесс усложняется. Состояние инфраструктуры нужно как-то синхронизировать и актуализировать у всех участников команды.</p>
<p>Нам нужно делиться с командой актуальным состоянием инфраструктуры. Это проще делать через удаленное хранилище, где все члены команды могут найти это состояние.</p>
<p>Если организовать этот процесс через git-репозиторий, нужно сначала обновить инфраструктуру, затем добавить коммит с новым <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">tfstate</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">tfstate</code>, внести свои изменения, и в свою очередь отправить изменения в коде Terraform и новый файл состояния в Git:</p>
<p><img style="--image-object-fit:contain;width:auto" class="m_9e117634 mantine-Image-root" src="/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6OTA5MSwicHVyIjoiYmxvYl9pZCJ9fQ==--e2c8af4aef34c7dd952421c74d43e8206a1ee58d/gitstate.png" alt="Terraform git state" loading="lazy"/></p>
<p>Такой подход требует концентрации внимания и может привести к проблемам:</p>
<ul>
<li>Человеческий фактор. Один из разработчиков может забыть обновить state из репозитория и применит неправильные изменения на инфраструктуру</li>
<li>Неконсистентность состояний. В git могут храниться разные версии состояния, и кто-нибудь применит неактуальные изменения на инфраструктуру</li>
<li>Конфликт применения изменений. Terraform будет работать у обоих с локальным файлом состояния и не будет знать о том, что кто-то уже обновляет ту же инфраструктуру</li>
</ul>
<p>Чтобы избавиться от этих проблем, Terraform предлагает собственный механизм удаленного хранения состояния. В нем большую часть операций контроля и синхронизации Terraform выполняет автоматически.</p>
<p>Концепция Terraform, описывающая удаленное хранение состояния, называется remote backend.</p>
<h2 id="heading-2-2">Что такое Terraform remote backends</h2>
<p>В терминологии Terraform <a style="text-decoration:underline" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="https://developer.hashicorp.com/terraform/language/settings/backends/configuration" rel="noopener noreferrer" target="_blank"><strong>backend</strong></a> — это решение, которое отвечает за хранение состояния. Если состояние хранится удаленно — это <strong>remote backend</strong>.</p>
<p>При использовании remote backend 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">tfstate</code> хранит только информацию об этом удаленном хранилище.</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://developer.hashicorp.com/terraform/language/settings/backends/s3" rel="noopener noreferrer" target="_blank">облачное объектное хранилище</a> по типу Amazon S3: Google Cloud Storage, Azure Storage, Yandex Cloud Storage и другие подобные решения. Также Terraform может использовать для удаленного хранения состояния HTTP-сервер, базу данных PostgreSQL или облачную платформу Terraform Cloud.</p>
<p>В схеме с remote backend при выполнении любых операций над инфраструктурой Terraform будет обращаться к удаленному файлу состояния, блокировать его на время выполнения изменений, затем перезаписывать этот файл с учетом внесенных изменений:</p>
<p><img style="--image-object-fit:contain;width:auto" class="m_9e117634 mantine-Image-root" src="/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6OTA5MiwicHVyIjoiYmxvYl9pZCJ9fQ==--9985de3e96f5e9cbd578c8f1f6b218a0169d0359/rstate.png" alt="Terraform remote state" loading="lazy"/></p>
<p>C remote backend Terraform самостоятельно будет решать ту проблему синхронизации состояний, которая возникала при командной работе с Git. Любой участник процесса при работе с инфраструктурой будет получать единственный актуальный файл состояния — у него не будет возможности получить некорректный.</p>
<p>Еще Terraform remote backend нативно поддерживает механизмы блокировки состояния. Если начать изменять какую-либо инфраструктуру, ее состояние не будет доступно никому другому, пока все изменения не будут применены. Это позволит исключить любые конфликты с параллельным применением изменений на одной и той же инфраструктуре.</p>
<p>Перейдем к практике и попробуем организовать хранение состояния удаленно. В качестве remote backend используем хранилище типа S3.</p>
<h2 id="heading-2-3">Как настроить хранение состояния в S3</h2>
<p>Настроим хранение состояния в облаке YandexCloud. Будем использовать консольную утилиту облака <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" rel="noopener noreferrer" target="_blank"><em>YandexCLI</em></a>. Она позволит создавать объекты в облаке через терминал.</p>
<p>Для достижения цели нам понадобятся:</p>
<ul>
<li>Хранилище файлов типа S3, в которое Terraform будет сохранять состояние инфраструктуры</li>
<li>Бессерверная БД типа DynamoDB, в которой Terraform будет фиксировать блокировки</li>
<li>Сервисный аккаунт облака, через который Terraform сможет управлять содержимым S3 и DynamoDB</li>
</ul>
<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 init</code> в проекте они уже должны существовать. То есть объекты для хранения remote backend должны быть созданы в другом проекте Terraform с локальным состоянием.</p>
<p>Для упрощения процесса создадим нужные для remote backend объекты вручную. Они в дальнейшем почти никогда не меняются.</p>
<h3 id="heading-3-4">Подготавливаем облако для хранения состояния</h3>
<p>Создадим в облаке S3-хранилище <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-hexlet-state</code> объемом 10МБ. Этого хватит для хранения состояния надолго:</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 storage bucket create --name yc-hexlet-state --max-size 10000000</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>
<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 ydb database create terraform-state-lock --serverless
done (7s)
id: etnpkn3gs4s56qk9g7kf
folder_id: ...
created_at: ...
name: terraform-state-lock
...
document_api_endpoint: https://docapi.serverless.yandexcloud.net/ru-central1/b1gjrod3dvqni46u3paj/etnpkn3gs4s56qk9g7kf</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>Сохраним <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">document_api_endpoint</code>, он нам понадобится при конфигурации Terraform.</p>
<p>Таблицы в YDB YandexCli создавать не умеет, поэтому таблицу добавим в веб-интерфейсе облака. Найдем нашу базу в разделе «Managed Service for YDB»:</p>
<p><img style="--image-object-fit:contain;width:auto" class="m_9e117634 mantine-Image-root" src="/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6OTA5MywicHVyIjoiYmxvYl9pZCJ9fQ==--26f02a2e9213478ff235961e95059e568755fa40/ydb.png" alt="Yandex Managed YDB" loading="lazy"/></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">lock</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">LockID</code> типа String, которая будет являться ключом партиционирования:</p>
<p><img style="--image-object-fit:contain;width:auto" class="m_9e117634 mantine-Image-root" src="/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6OTA5NCwicHVyIjoiYmxvYl9pZCJ9fQ==--d60dcae78bfd8469803a0b1d5f669f7aca0d205f/ydb2.png" alt="Yandex Managed YDB" loading="lazy"/></p>
<p>Далее через YandexCLI создадим сервисный аккаунт <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">hexlet-remote</code>, который будет сохранять состояние Terraform в облачное хранилище:</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 service-account create --name hexlet-remote --description "SA to manage terraform state"
id: ajejk11p9ls1vvhc12mb
folder_id: ...
created_at: ...
name: hexlet-remote
description: SA to manage terraform state</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>В ответе получим id нового сервисного аккаунта. Он понадобится, чтобы разрешить сервисному аккаунту взаимодействовать с хранилищем.</p>
<p>Проверим имя каталога YandexCloud, в котором работаем:</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 resource-manager folder list
+----------------------+---------+--------+--------+
| ID | NAME | LABELS | STATUS |
+----------------------+---------+--------+--------+
| ... | default | | ACTIVE |
+----------------------+---------+--------+--------+</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>В примере выше используется каталог <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">default</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">storage.editor</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">ydb.editor</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">yc resource-manager folder add-access-binding default --role storage.editor --subject serviceAccount:ajejk11p9ls1vvhc12mb
done (1s)
effective_deltas:
- action: ADD
access_binding:
role_id: storage.editor
subject:
id: ajejk11p9ls1vvhc12mb
type: serviceAccount
yc resource-manager folder add-access-binding default --role ydb.editor --subject serviceAccount:ajejk11p9ls1vvhc12mb
done (1s)
effective_deltas:
- action: ADD
access_binding:
role_id: ydb.editor
subject:
id: ajejk11p9ls1vvhc12mb
type: serviceAccount</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>В <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">--subject</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">id</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">hexlet-remote</code> просматривать и изменять S3-хранилище в нашем каталоге, а также работать с базой в Managed YDB. Теперь настроим Terraform так, чтобы он подключался к хранилищу S3 и клал туда состояние.</p>
<p>Создадим у сервисного аккаунта ключи доступа к S3 хранилищу:</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 access-key create --service-account-name hexlet-remote --description "terraform s3 backend access key"
access_key:
id: ...
service_account_id: ...
created_at: ...
description: ...
key_id: YCABX6vQXtCjoKu_oB7QabuZO
secret: YCOL4xZ1tdpduS46z_YTlvDzYUwv8xBK_UuRq18m</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>Сохраним значения <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">key_id</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">secret</code>. Они понадобятся для подключения Terraform к удаленному хранилищу.</p>
<p>Мы создали в облаке всё необходимое для удаленного хранения состояния. Теперь настроим Terraform так, чтобы он отправлял состояние в удаленное хранилище.</p>
<h3 id="heading-3-5">Описываем remote backend в проекте Terraform</h3>
<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">backend.tf</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">backend</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">terraform {
backend "s3" {
endpoints = {
s3 = "https://storage.yandexcloud.net"
dynamodb = "https://docapi.serverless.yandexcloud.net/ru-central1/b1gjrod3dvqni46u3paj/etnpkn3gs4s56qk9g7kf"
}
region = "ru-central1"
bucket = "yc-hexlet-state"
key = "hexlet-remote-state"
access_key = "YCABX6vQXtCjoKu_oB7QabuZO"
secret_key = "YCOL4xZ1tdpduS46z_YTlvDzYUwv8xBK_UuRq18m"
dynamodb_table = "lock"
skip_region_validation = true
skip_credentials_validation = true
skip_requesting_account_id = true
skip_s3_checksum = true
}
}</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>У каждого типа backend в 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">backend "s3"</code> и указываем для него:</p>
<ul>
<li><strong>endpoints</strong> — список эндпоинтов. Здесь мы указываем сетевой адрес хранилища S3. В случае YandexCloud — это всегда <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">storage.yandexcloud.net</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">document_api_endpoint</code> созданной нами Managed YDB</li>
<li><strong>region</strong> — свойство, характерное для S3 конкретного облачного провайдера. У YandexCloud это всегда <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">ru-central1</code></li>
<li><strong>bucket</strong> — имя хранилища, которое мы создали</li>
<li><strong>key</strong> — директория в хранилище, куда Terraform будет класть файл состояния</li>
<li><strong>access_key</strong> и <strong>secret_key</strong> — соответственно, <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">key_id</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">secret</code> созданного нами ключа доступа</li>
<li><strong>dynamodb_table</strong> — имя таблицы, которую мы создали в Managed YDB через интерфейс облака</li>
<li>Параметры <strong>skip</strong> служат для упрощения подключения к S3, в учебных целях</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">terraform init</code>.</p>
<p>Если изменить настройки backend в существующем проекте, необходимо будет вызвать модифицированную команду: <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 init -migrate-state</code>. В этом случае Terraform постарается перенести локальный файл с состоянием в удаленное хранилище.</p>
<p>В итоге мы должны увидеть, что Terraform смог настроить работу с бэкендом s3:</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">Initializing the backend...
Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.</code>
<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>
<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 used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# yandex_vpc_network.net will be created
+ resource "yandex_vpc_network" "net" {
...
}
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value:</code>
<p>Видим стандартный вывод Terraform, информирующий о готовности изменить инфраструктуру.</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>
<code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">Acquiring state lock. This may take a few moments...
╷
│ Error: Error acquiring the state lock
│
│ Error message: ConditionalCheckFailedException: Condition not satisfied
│ Lock Info:
│ ID: ac5bf646-b858-c819-c847-95f260d3c613
│ Path: yc-hexlet-state/hexlet-remote-state
│ Operation: OperationTypeApply
│ Who: hexlet@user
│ Version: 1.3.7
│ Created: 2023-07-06 11:38:14.794552864 +0000 UTC
│ Info:
│</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 apply</code> в первый раз, Terraform создал запись о блокировке. Пока первая сессия работы с Terraform не завершится, остальные попытки менять инфраструктуру будут блокироваться.</p>
<p>Так мы с помощью удаленного хранения состояния в S3-хранилище и блокировки состояния в YDB добились того, что:</p>
<ul>
<li>Состояние инфраструктуры всегда будет одинаковым и актуальным у всей команды. Локально в <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">.tfstate</code> проекта будет храниться только адрес удаленного бэкенда</li>
<li>Не возникнут конфликты одновременного обновления инфраструктуры двумя или более членами команды</li>
</ul>
<p>Мы настроили удаленное хранение состояния. Осталось позаботиться о безопасности и о том, чтобы ключи для нашего хранилища не утекли в сеть.</p>
<h2 id="heading-2-6">Как работать с секретами backend</h2>
<p>В работе с секретами backend стоит придерживаться тех же правил, что при работе с другими секретами Terraform. В примере выше мы прописали все настройки backend прямым текстом. Это может сделать наши облачные S3 и Managed YDB уязвимыми.</p>
<p>Доработаем хранение секретов backend, чтобы обезопасить нашу инфраструктуру.</p>
<p>В разделе backend мы не можем обращаться к обычным переменным 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">tfstate</code>. А на момент обработки состояния Terraform еще не знает о переменных, которые существуют в инфраструктуре. Так секретами backend понадобится управлять отдельно.</p>
<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 init</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">-backend-config</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">backend.tf</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">terraform {
backend "s3" {
endpoint = "storage.yandexcloud.net"
region = "ru-central1"
key = "hexlet-remote-state"
skip_region_validation = true
skip_credentials_validation = true
skip_requesting_account_id = true
skip_s3_checksum = true
}
}</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>Мы убрали значения <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">bucket</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">access_key</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">secret_key</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">dynamodb_*</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">secret.backend.tfvars</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">backend.tf</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">bucket = "yc-hexlet-state"
access_key = "YCABX6vQXtCjoKu_oB7QabuZO"
secret_key = "YCOL4xZ1tdpduS46z_YTlvDzYUwv8xBK_UuRq18m"
dynamodb_endpoint = "https://docapi.serverless.yandexcloud.net/ru-central1/b1gjrod3dvqni46u3paj/etnpkn3gs4s56qk9g7kf"
dynamodb_table = "lock"</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>Имя <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.backend.tfvars</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">secret.*</code> в .gitignore нашего проекта Terraform, и файл с этими секретами не попадет в сеть.</p>
<p>Подключим файл с секретами backend при инициации инфраструктуры:</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 init -backend-config=secret.backend.tfvars</code>
<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">-backend-config</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">backend</code>.</p>
<p>Для передачи файла с секретами коллегам можно применять облачные менеджеры ключей, такие как AWS Secret Manager или Yandex Lockbox.</p>
<h2 id="heading-2-7">Выводы</h2>
<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.tfstate</code> в некоторое удаленное хранилище. Локально он хранит только адрес удаленного хранилища и параметры подключения к нему.</p>
<p>Удаленное хранение решает проблемы организации доступа к актуальному состоянию Terraform, когда инфраструктура управляется более чем с одной машины. Это в основном касается командной работы, но может быть удобно и при работе с личными проектами. Это позволяет использовать единый подход на проектах любого масштаба.</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/remote-state/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/remote-state/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>