Конфиг, который умеет считать сам себя
Однажды я полдня вылавливал опечатку в YAML-файле. Два пробела вместо четырёх, и весь деплой развалился с сообщением в духе «неверный формат на строке 214». Строка 214 была абсолютно пустой. Знакомая боль, правда?
Именно из этой категории проблем вырос проект, о котором хочу рассказать. HCL — HashiCorp Configuration Language. Это не ещё один сериализатор данных, а инструмент для построения собственных языков конфигурации. Разница принципиальная, и сейчас объясню, в чём она.
Что это вообще такое
HCL — это библиотека на Go. Точнее, набор инструментов, с помощью которых ваше приложение объявляет: «вот такие атрибуты и блоки я ожидаю в конфиге». HCL читает файл, проверяет, что структура совпадает с заявленной, и отдаёт готовые объекты. Всё остальное берёт на себя вызывающая сторона.
Здесь и кроется главный трюк. JSON и YAML отвечают на вопрос «как записать данные». HCL отвечает на вопрос «как записать конфигурацию конкретного приложения так, чтобы и человеку было приятно, и машине понятно». Это разные задачи, хотя на первый взгляд кажутся похожими.
Проекту уже больше десяти лет — репозиторий живёт с 2014 года. И это не игрушка на выходные: HCL лежит в основе Terraform, а через него — в инфраструктуре огромного числа компаний. Так что если вы когда-нибудь писали resource "aws_instance" "web" { ... }, вы уже работали с HCL, возможно, даже не подозревая об этом.
Два синтаксиса на один и тот же конфиг
Самая интересная идея проекта — один и тот же конфиг можно описать двумя способами. Первый — родной синтаксис, рассчитанный на людей:
io_mode = "async"
service "http" "web_proxy" {
listen_addr = "127.0.0.1:8080"
process "main" {
command = ["/usr/local/bin/awesome-app", "server"]
}
process "mgmt" {
command = ["/usr/local/bin/awesome-app", "mgmt"]
}
}
Второй — эквивалентный JSON, который проще генерировать кодом:
{
"io_mode": "async",
"service": {
"http": {
"web_proxy": {
"listen_addr": "127.0.0.1:8080",
"process": {
"main": {
"command": ["/usr/local/bin/awesome-app", "server"]
}
}
}
}
}
}
Зачем это нужно на практике? Часть конфигов пишут руками в читаемом виде, часть генерируют скрипты и пайплайны. HCL позволяет смешивать оба источника, не переписывая систему под каждый формат. Приложение читает и то, и другое одинаково.
Выражения прямо в конфиге
Вот здесь становится по-настоящему любопытно. Значения атрибутов в HCL — это не просто строки и числа, а выражения:
# Арифметика с литералами и переменными приложения
sum = 1 + addend
# Интерполяция строк и шаблоны
message = "Hello, ${name}!"
# Функции, которые предоставляет приложение
shouty_message = upper(message)
Простая арифметика, подстановка переменных, вызовы функций, доступные из внешнего кода. В JSON напрямую так не напишешь, но HCL даёт обходной путь: выражения можно встроить в строки JSON через синтаксис ${...}. Получается компромисс между строгим форматом данных и полноценным языком программирования.
В документации проекта это описано как попытка занять середину между «просто JSON» и «конфиг на Ruby, где можно случайно написать бизнес-логику». Мне кажется, они близки к балансу.
Как это выглядит в коде
Для типовых случаев есть высокоуровневый API, и он приятно короткий. Опишите структуру через Go-теги, и всё:
package main
import (
"log"
"github.com/hashicorp/hcl/v2/hclsimple"
)
type Config struct {
IOMode string `hcl:"io_mode"`
Service ServiceConfig `hcl:"service,block"`
}
type ServiceConfig struct {
Protocol string `hcl:"protocol,label"`
Type string `hcl:"type,label"`
ListenAddr string `hcl:"listen_addr"`
Processes []ProcessConfig `hcl:"process,block"`
}
type ProcessConfig struct {
Type string `hcl:"type,label"`
Command []string `hcl:"command"`
}
func main() {
var config Config
err := hclsimple.DecodeFile("config.hcl", nil, &config)
if err != nil {
log.Fatalf("Failed to load configuration: %s", err)
}
log.Printf("Configuration is %#v", config)
}
Всё содержимое конфига разбирается в структуру Config за один вызов. Если файл не совпадает с ожидаемой схемой, вы получите внятную ошибку с указанием места, а не загадочное «unexpected token». Это, пожалуй, главная причина, по которой люди выбирают HCL: качество сообщений об ошибках заложено в саму архитектуру.
Если нужен контроль над разбором, вычислением выражений или собственная логика обработки, есть низкоуровневый API. Он заметно сложнее, но иногда без него не обойтись.
Второй мажор и почему он не совместим с первым
HCL 2.0 объединил в себе два проекта: собственно HCL первого поколения и язык интерполяции HIL. Раньше это были отдельные библиотеки, теперь — единый язык с произвольными выражениями. Парсер и Go-API переписали с нуля, поэтому прямого пути миграции с первой версии нет. Синтаксис похож, а реализация внутри совсем другая.
Если у вас в проекте оказались обе версии, Go спасает механизм семантического версионирования импортов:
import (
hcl1 "github.com/hashicorp/hcl"
hcl2 "github.com/hashicorp/hcl/v2"
)
Две версии могут спокойно жить в одной программе под разными именами. Ещё один нюанс: HCL 2 нельзя импортировать в проект, который не использует Go Modules. Для старых GOPATH-проектов это может стать неприятным сюрпризом.
Кому это нужно
HCL стоит смотреть в двух ситуациях. Первая — вы пишете инструмент, которому нужен собственный конфигурационный язык: CI-систему, деплой-утилиту, оркестратор, сервер с параметрами. Вместо того чтобы изобретать формат и писать парсер руками, вы описываете схему и получаете готовый разбор с проверкой и понятными ошибками.
Вторая — вы работаете с Terraform и хотите понимать, что происходит под капотом. Знание HCL тут окупается быстро.
А вот если вам просто нужно сохранить и прочитать пару словарей в файл, HCL избыточен. Берите JSON или YAML, не мучайте себя.
Что стоит учесть
Проект живой: около 5,8 тысячи звёзд, регулярные коммиты, лицензия MPL-2.0. Но документация довольно сухая — по сути это набор спецификаций и примеров, а не учебник. Новичку придётся вчитываться в описания синтаксиса. Открытых issue тоже хватает, их больше двух сотен, так что на мгновенный ответ мейнтейнеров рассчитывать не всегда получится.
Начать проще всего с пакета hclsimple и примера из README. Если поймёте, что нужен более тонкий контроль, переходите к низкоуровневому API. А если хочется увидеть HCL в реальном деле, посмотрите на конфиги Terraform — это та же библиотека в боевых условиях.
Что мне здесь нравится больше всего: проект честно признаёт, что JSON незаменим как универсальный обменный формат, и не пытается его вытеснить. Вместо этого HCL предлагает там, где нужен именно конфиг с выражениями и проверяемой структурой, свой синтаксис, совместимый с JSON. Редкий случай прагматичного дизайна без попытки «победить» всех конкурентов.
