linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Уроки
  • How it works
  • База знаний
  • Шпаргалка
  • Capstone
  • Собеседование
home/terraform/kb/Провайдеры/tf-cloudinit-provider

kb/providers ── Провайдеры ── intermediate

cloudinit provider: user_data для EC2 и не только

Provider `cloudinit` собирает multi-part MIME для `user_data` EC2. `data "cloudinit_config"` принимает несколько `part` (cloud-config YAML, shell-script, jinja и т.п.) и упаковывает их в один blob. Заменяет ручной base64-encoding одной строки и позволяет компоновать конфиг из кусков.

view as markdownaka: terraform-cloudinit, terraform-user-data

Зачем 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 полетит на инстанс.

§ см. также

  • tf-utility-providersУтилитарные провайдеры: random, time, null, terraform_dataПровайдеры, которые не управляют облаком, а помогают HCL. `random`, генерация ID/паролей. `time`, задержки и timestamp-метки. `null`, устаревший «нерессурс» для триггеров. `terraform_data`, современная замена `null_resource`, встроена в Terraform. Каждый снимает конкретное ограничение деклеаративного подхода.
  • tf-archive-external-httparchive, external, http: данные снаружи в HCLТри провайдера для получения данных снаружи Terraform. `archive`, упаковка файлов в zip (lambda code, layers). `external`, вызов любого скрипта с JSON I/O. `http`. GET-запрос к URL, парс ответа. Все три, data sources, читают, не пишут. Полезны где declarative HCL не доходит.
  • aws-providerAWS Provider: настройки и где Terraform берёт ключиAWS-провайдер ищет credentials в нескольких местах подряд: env-переменные, ~/.aws/credentials, IAM-роль инстанса. Чаще всего достаточно `aws configure` локально или роль на EC2, больше ничего не настраивать.
  • tf-resource-blockБлок resource: главный кирпич Terraformresource, это блок, который говорит Terraform «создай мне такую штуку в облаке». У него три части: тип ресурса (что это), имя (как зовём внутри), и аргументы (как настроить). 90% времени в Terraform, это написание таких блоков.
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки