Зачем
HCL, декларативный. Иногда нужно что-то построить: упаковать
директорию в zip, прочитать JSON из CI-secret-store, спросить current IP
у ifconfig.io. Три data-провайдера закрывают этот gap, оставаясь в
парадигме «всё описано в HCL».
archive_file
Упаковка локальных файлов в zip. Главный кейс, aws_lambda_function.filename.
terraform { required_providers { archive = {source = "hashicorp/archive"
version = "~> 2.6"
}
}
}
data "archive_file" "lambda" {type = "zip"
source_file = "${path.module}/lambda/handler.py" output_path = "${path.module}/lambda.zip"}
resource "aws_lambda_function" "demo" {function_name = "demo"
filename = data.archive_file.lambda.output_path
source_code_hash = data.archive_file.lambda.output_base64sha256
handler = "handler.main"
runtime = "python3.12"
role = aws_iam_role.lambda.arn
}
Главное:
output_base64sha256, хэш zip-содержимого. Передаётся в Lambda какsource_code_hash. Если код поменялся → хэш меняется → Lambda замечает → перевыкатывает. Без этого Terraform не увидит изменения в исходниках и не обновит лямбду.
Целая директория
data "archive_file" "lambda" {type = "zip"
source_dir = "${path.module}/lambda/" output_path = "${path.module}/lambda.zip"excludes = [
"__pycache__",
"*.pyc",
"tests/**",
]
}
Удобно для лямбд с requirements/node_modules.
Inline-источник
data "archive_file" "config" {type = "zip"
output_path = "${path.module}/config.zip" source { content = jsonencode({ feature_flags = { ... } })filename = "config.json"
}
}
Файл генерируется из HCL значения. Полезно для config-payload'ов которые считаются динамически.
external
Вызов произвольного скрипта. Скрипт принимает JSON в stdin, отдаёт JSON в stdout.
terraform { required_providers { external = {source = "hashicorp/external"
version = "~> 2.3"
}
}
}
data "external" "git_info" { program = ["bash", "${path.module}/scripts/git-info.sh"] query = {repo = path.cwd
}
}
output "git_commit" {value = data.external.git_info.result.commit
}
scripts/git-info.sh:
#!/usr/bin/env bash
set -euo pipefail
eval "$(jq -r '@sh "REPO=\(.repo)"')"
COMMIT=$(git -C "$REPO" rev-parse HEAD)
BRANCH=$(git -C "$REPO" rev-parse --abbrev-ref HEAD)
jq -n --arg c "$COMMIT" --arg b "$BRANCH" '{commit:$c, branch:$b}'Что важно:
- Скрипт должен вернуть JSON-объект, где все значения, строки. Нельзя число, нельзя nested object. Это ограничение API.
- Скрипт должен быть idempotent. Terraform может вызвать его на каждом plan. Если скрипт меняет состояние мира, плохо.
- Скрипт работает с правами того, кто запустил
terraform.
Используется когда:
- Нужно прочитать значение из vault/CI secret manager у которого нет Terraform-provider'а.
- Нужно вычислить значение через сложную логику (curl + jq + sed).
- Build-time integration: «вытащи docker tag из CI».
Анти-кейс: не делай external для side-effects. Это data source, не
ресурс.
http
GET-запрос к URL, ответ в data source.
terraform { required_providers { http = {source = "hashicorp/http"
version = "~> 3.4"
}
}
}
data "http" "my_ip" {url = "https://ifconfig.io"
}
resource "aws_security_group_rule" "allow_my_ip" {type = "ingress"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["${chomp(data.http.my_ip.response_body)}/32"]security_group_id = aws_security_group.bastion.id
}
Аргументы:
url, endpoint.request_headers, map, для auth/User-Agent.method, по умолчанию GET, можно POST/PUT/DELETE.request_body, body для POST.
Атрибуты:
response_body, текст ответа.status_code. HTTP-код.response_headers, map.
Валидация status_code:
data "http" "config" {url = "https://config.example.com/feature-flags"
lifecycle { postcondition {condition = self.status_code == 200
error_message = "Config server returned ${self.status_code}, expected 200"}
}
}
Если упало, apply остановится с понятным сообщением. См. Урок про precondition/postcondition.
Когда что использовать
| Задача | Provider |
|---|---|
| Упаковать lambda-код в zip | archive_file |
| Прочитать значение из произвольного источника (vault, CLI) | external |
| Получить JSON от REST API | http (или external + curl) |
| Прочитать что-то из git/AWS CLI | external |
| Динамически узнать current public IP | http |
| Сгенерить файл из переменной | archive_file с source.content |
Подводные камни
-
Все три,
dataисточники. Вычисляются на каждом plan. Еслиexternalзовёт долгий скрипт /httpходит на медленный endpoint, плата на каждом plan. Кэшируй на стороне самого скрипта/API. -
external-скрипт ловит всё что не int/string. Возвращает42как число. Terraform упадёт. Превращай в строку:{"count":"42"}. Парсить число обратно, черезtonumber(data.external.x.result.count). -
httpне следует за redirect-ами по умолчанию. Включаетсяfollow_redirects = true. Чтобы поймать первый 301, оставь дефолт. -
archive_fileиспользует mtime, не content-hash для idempotency. Точнее, использует hash содержимого внутри. Это правильно,terraform planстабилен. Но если в zip попадают файлы которые меняются на каждом билде (timestamps в byte-code): на каждом plan будет diff. Чисти такие файлы черезexcludes. -
httpбез auth, для public endpoints. Если endpoint требует bearer-token, передавай черезrequest_headers = { Authorization = "Bearer ${var.token}" }. Токен попадает в state, пометь как sensitive. -
external, security risk. Скрипт исполняется с правами terraform-юзера. Если HCL приходит из недоверенного источника, это remote code execution. Не используйexternalв reusable модулях, которые качают чужие. -
http.response_bodyвсегда строка. JSON-ответ нужно парситьjsondecode(data.http.x.response_body). Не пытайся писатьdata.http.x.response_body.field, это string indexing, не object access. -
LocalStack не нужен для этих провайдеров. Все три не ходят в AWS. Тесты с ними, чистый HCL.