# cloudinit provider: user_data для EC2 и не только _Провайдеры · TerraformLab Knowledge Base_ **TL;DR:** Provider `cloudinit` собирает multi-part MIME для `user_data` EC2. `data "cloudinit_config"` принимает несколько `part` (cloud-config YAML, shell-script, jinja и т.п.) и упаковывает их в один blob. Заменяет ручной base64-encoding одной строки и позволяет компоновать конфиг из кусков. ## Зачем cloudinit EC2-инстанс при первом старте запускает `cloud-init`, который читает `user_data` (любой текст до 16 KiB). Поддерживаются форматы: - **cloud-config YAML**, декларативные команды (создать пользователя, написать файл, поставить пакет). - **shell script**, `#!/bin/bash` и поехали. - **boothook**, выполнить **до** cloud-init main run. - **mime-multipart**, несколько форматов в одном. Когда конфигурация сложная (надо и YAML, и скрипт): пишут multi-part MIME руками: boundaries, headers, base64-encode. Это утомительно. Provider `cloudinit` собирает MIME за тебя. ## Установка ```hcl terraform { required_providers { cloudinit = { source = "hashicorp/cloudinit" version = "~> 2.3" } } } ``` Без конфигурации, всё локально. ## cloudinit_config, простой кейс ```hcl data "cloudinit_config" "bootstrap" { gzip = false base64_encode = true part { content_type = "text/cloud-config" content = yamlencode({ package_update = true package_upgrade = false packages = [ "nginx", "jq", ] write_files = [ { path = "/etc/nginx/conf.d/app.conf" content = file("${path.module}/templates/nginx.conf") owner = "root:root" permissions = "0644" }, ] runcmd = [ "systemctl enable nginx", "systemctl start nginx", ] }) } } resource "aws_instance" "web" { ami = data.aws_ami.ubuntu.id instance_type = "t3.micro" user_data = data.cloudinit_config.bootstrap.rendered } ``` Что произошло: - `cloudinit_config` собрал MIME с одной частью (YAML). - `base64_encode = true`, итог в base64 (EC2 user_data ожидает base64). - `data.cloudinit_config.bootstrap.rendered`, готовый blob. Без provider'а пришлось бы вручную писать MIME-boundaries и base64-encode. ## Multi-part: YAML + bash Иногда нужно и cloud-config (декларатив), и shell script (для того, чего в cloud-config нет). ```hcl data "cloudinit_config" "app" { gzip = true base64_encode = true part { filename = "init.cfg" content_type = "text/cloud-config" content = yamlencode({ users = [ { name = "appuser" sudo = "ALL=(ALL) NOPASSWD:ALL" shell = "/bin/bash" ssh_authorized_keys = [tls_private_key.deploy.public_key_openssh] }, ] package_update = true packages = ["docker.io", "awscli"] }) } part { filename = "register.sh" content_type = "text/x-shellscript" content = templatefile("${path.module}/scripts/register.sh.tpl", { env = var.env cluster = aws_ecs_cluster.app.name }) } } ``` Два part: cloud-config + shell. Cloud-init выполнит сначала YAML (создать пользователя, поставить пакеты), потом shell (зарегистрировать в ECS cluster через AWS CLI). ### `gzip = true` user_data на EC2, 16 KiB лимит. Gzip эффективен, конфиг 30KB ужимается до 5-7KB. Cloud-init сам распаковывает. ## templatefile для динамики cloudinit и templatefile, идеальная пара. Шаблон файла с переменными: `templates/register.sh.tpl`: ```bash #!/usr/bin/env bash set -euo pipefail ENV="${env}" CLUSTER="${cluster}" echo "Registering in $CLUSTER ($ENV)" >> /var/log/register.log aws ecs register-container-instance \ --cluster "$CLUSTER" \ --instance-identity-document "$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/document)" ``` В HCL: ```hcl part { content_type = "text/x-shellscript" content = templatefile("${path.module}/templates/register.sh.tpl", { env = var.env cluster = aws_ecs_cluster.app.name }) } ``` `templatefile` подставляет `${env}` и `${cluster}` на этапе plan. Результат, готовый к выполнению bash. ## Полный пример: EC2 со Stack ```hcl resource "tls_private_key" "deploy" { algorithm = "ED25519" } resource "aws_key_pair" "deploy" { key_name = "deploy" public_key = tls_private_key.deploy.public_key_openssh } data "cloudinit_config" "web" { gzip = true base64_encode = true part { content_type = "text/cloud-config" content = yamlencode({ users = [{ name = "deploy" sudo = "ALL=(ALL) NOPASSWD:ALL" ssh_authorized_keys = [tls_private_key.deploy.public_key_openssh] }] package_update = true packages = ["nginx"] }) } part { content_type = "text/x-shellscript" content = templatefile("${path.module}/templates/configure.sh.tpl", { site_name = var.site_name }) } } resource "aws_instance" "web" { ami = data.aws_ami.ubuntu.id instance_type = "t3.micro" key_name = aws_key_pair.deploy.key_name vpc_security_group_ids = [aws_security_group.web.id] user_data = data.cloudinit_config.web.rendered tags = { Name = "${var.site_name}-web" } lifecycle { ignore_changes = [user_data] # см. Подводный камень } } ``` ## Подводные камни - **user_data выполняется ОДИН РАЗ.** Только при первой загрузке. Меняешь user_data → Terraform хочет пересоздать инстанс. Если этого не нужно (просто новый конфиг для будущих инстансов). `lifecycle.ignore_changes = [user_data]`. Иначе любая правка скриптов = плановое уничтожение парка. - **Лимит 16 KiB.** До base64-encoding. С `gzip = true` влезает гораздо больше, но всё равно: не файлохранилище. Большие artifact'ы тяни через S3 в самом user_data. - **Ошибки в cloud-config не видны Terraform'у.** Apply прошёл, инстанс стартовал, user_data «выполнился». Что внутри, `tail -f /var/log/cloud-init.log` на самом инстансе. Терраформ не знает что у тебя пакет не поставился. - **YAML отступы.** `yamlencode()` решает большую часть, но если делаешь inline YAML, отступы критичны. Cloud-init YAML строгий. - **`runcmd` запускается ПОСЛЕ packages.** Если runcmd зависит от установленного пакета, норм. Если до, фейл. `bootcmd` запускается раньше. - **gzip не для всех. Старый cloud-init (CentOS 6, Amazon Linux 1)**, может не распаковать. Современный (Ubuntu 20+, AL2/AL2023). OK. - **Sensitive в user_data.** Если в части есть пароль/токен, пометь output sensitive, защити state. Сама cloudinit_config не пометит за тебя. - **Тестировать локально через `cloud-init schema --config-file ...`.** Утилита cloud-init умеет валидировать YAML до отправки на инстанс. Полезно в pre-commit. ## Команды ```bash terraform console -chdir=. ``` Можно вытащить data.cloudinit_config.x.rendered и base64 -d увидеть итоговый MIME. ```bash echo $rendered | base64 -d | gunzip | head -50 ``` Увидеть собранный multi-part глазами. Полезно при дебаге 'почему cloud-init не сработал'. ```bash cloud-init schema --config-file cloud-config.yaml ``` Локальная валидация cloud-config YAML до того, как user_data полетит на инстанс. ## См. также - [Утилитарные провайдеры: random, time, null, terraform_data](/terraform/kb/tf-utility-providers.md) - [archive, external, http: данные снаружи в HCL](/terraform/kb/tf-archive-external-http.md) - [AWS Provider: настройки и где Terraform берёт ключи](/terraform/kb/aws-provider.md) - [Блок resource: главный кирпич Terraform](/terraform/kb/tf-resource-block.md)