Использование Caddy

Vitalii Filiuchkov
7 min readJan 11, 2022

--

Оригинал: https://nivethan.dev/devlog/using-caddy.html

Я уже довольно давно использую nginx в качестве обратного прокси-сервера и вполне им доволен. По большей части он прост в использовании, и у меня есть довольно стандартная конфигурация, которую я копирую и повторно использую для большинства своих проектов. Обычно я настраиваю SSL, сжатие и передачу запросов в приложение. Я нашел Caddy пару дней назад, и примеры кода выглядели великолепно! Caddy автоматически делает многое из того, что в nginx приходится делать руками, и это заставило меня попробовать.

Переход с nginx на caddy

Вот моя 48-строчная конфигурация nginx для одного из моих проектов. Честно говоря, я нашел и скопировал секцию gzip и так и не узнал, что он на самом деле делает, поэтому я думаю, что есть в этом конфиге вещи, которые можно вырезать.

Но несмотря ни на что, это работает, и это то, чем я сейчас пользуюсь. Я думаю, что наличие значений по умолчанию действительно важно, и в идеале nginx должен иметь конфигурацию, которую я мог бы легко использовать для сжатия, и это было бы здорово.

Простая конфигурация с проксипассом, ssl-сертификатом и включенным сжатием:

server {
gzip on;
gzip_http_version 1.0;
gzip_comp_level 2;
gzip_min_length 1100;
gzip_buffers 4 8k;
gzip_proxied any;
gzip_types
text/css
text/javascript
text/xml
text/plain
text/x-component
application/javascript
application/json
application/xml
application/rss+xml
font/truetype
font/opentype
application/vnd.ms-fontobject
image/svg+xml;

gzip_static on;
gzip_proxied expired no-cache no-store private auth;
gzip_disable "MSIE [1-6]\.";
gzip_vary on;

listen 7088 ssl http2;
server_name _;

ssl_certificate /etc/ssl/certs/selfsigned.crt;
ssl_certificate_key /etc/ssl/selfsigned.key;

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers AES256+EECDH:AES256+EDH:!aNULL;

location / {
proxy_pass http://localhost:6002;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}

location /static {
alias /home/nivethan/bp/commentless/public/;
}
}

Теперь к Кэдди

Приведенная ниже конфигурация из двенадцати строк полностью равноценна предыдущей! Честно говоря, я мог бы также поместить gzip в конфигурацию nginx выше, чтобы блок сервера был короче, но мне нравится то, что с Сaddy, мне даже не нужно думать об этом:

192.168.1.70:7088 {
encode gzip

handle /static/* {
root /static/* /home/nivethan/bp/commentless/public
uri strip_prefix /static
file_server
}

reverse_proxy localhost:6002
}

Меня беспокоит секция handle, так как nginx очень упрощает добавление псевдонимов, и когда я искал аналогичную функциональность для Caddy, я обнаружил, что не было других способов, кроме применения хаков. Почему gzip не используется по умолчанию? Я предполагаю, что если браузер не сможет обработать gzip, Caddy вернет несжатые файлы, поэтому безопасно всегда иметь его включенным.

Я думаю, что с некоторыми незначительными изменениями, конфигурация Caddy действительно может быть четырехстрочной для большинства моих проектов.

Итоговая конфигурация будет следующей:

192.168.1.70:7088 {
alias /static /home/nivethan/bp/commentless/public
reverse_proxy localhost:6002
}

Все, что мне нужно сообщить своему веб-серверу — это порт для прокси-сервера и директорию, где будут лежать статические ресурсы. Есть в конфигурации некоторые части, которые я, вероятно, мог бы удалить, например, я всегда использую /staticдля своих статических файлов, чтобы их можно было установить автоматически. У меня также есть статический IP-адрес, так что его также можно удалить. Но я не хотел добавлять слишком много кода, так как мог бы придумать лучший способ сделать все это.

Хак Кэдди

На мой взгляд, добавление команды alias не должно требовать копания в глубинах Сaddy. Действительно, перед обработкой Caddy-файла мы можем переписать строку alias в набор строк, которые Caddy уже может понять. Это означает, что мне просто нужно найти, где caddy читает в файле конфигурации, и заменить строку, начинающуюся с псевдонима, несколькими строками. Это должно быть довольно простое изменение.

Первое предостережение: я не программировал в Go, я прочитал пару вещей и попробовал свои силы в изучении примеров go на главном веб-сайте, но я обычно сдаюсь и возвращаюсь к надежному node или python, чтобы быстро что-то сделать. Это означает, что я пишу здесь действительно плохой код, я просто перебираю строки, и код, который я пишу, довольно хрупкий. Мне было бы удобно использовать это для своих личных проектов, но я бы не стал внедрять это в прод :)

У Caddy есть инструкции по установке и сборке на их сайте, если вы их прочитаете, то сборка и запуск Сaddy должны быть довольно простыми. Но я не прочитал и потратил некоторое время, выполняя > go buildне в той папке.

Шаги по сборке Caddy из исходников:

> git clone https://github.com/caddyserver/caddy.git
> cd caddy/cmd/caddy
> go build

В результате, в текущей директории я получаю исполняемый бинарник, и там же пишу свой Caddyfile:

192.168.1.70:7088 {
alias /static /home/nivethan/bp/commentless/public
reverse_proxy localhost:6002
}

Это приводит к следующей ошибке:

2022/01/02 00:36:48.544 INFO    using adjacent Caddyfile
run: adapting config using caddyfile: Caddyfile:3: unrecognized directive: alias

Отлично! Я убедился, что Caddy работает с правильной конфигурацией, просто чтобы убедиться, что это так. Итак, теперь у меня был способ собрать Caddy и запустить его. Следующим шагом было начать читать код и копаться в нем. Мне нужно было выяснить, где Caddy читает файл, поэтому я начал с обратного и начал искать ключевые слова, которые, по моему мнению, были бы уникальными. Такие вещи, как reverse_proxy и strip_prefix, было довольно легко найти, и, прочитав несколько файлов, я обнаружил, что все это вытекает из очевидного места: файл caddy/cmd/main.go— это место, где файл конфигурации считывается и затем передается в Caddy.

* Разумнее всего было бы искать файл os.ReadFile или какую-нибудь его вариацию, но по какой-то причине я знал, что хочу найти все места, где читается файл, но я не знал, как это сделать.

Внутри этого файла, после того, как конфигурация будет прочитана, мы можем перехватить ее, изменить, а затем передать дальше. Изменение, которое я пытаюсь внести — это всего лишь простой rewrite, чтобы мне могло сойти с рук внесение изменений прямо здесь. Я думаю, что было бы здорово правильно добавить псевдоним, который, как я полагаю, включает обновление файла directives.go, который, похоже, является файлом со всеми параметрами, и, похоже, существует довольно хорошая структура для анализа этих директив.

А пока я просто собираюсь хакнуть его прямо здесь.

Приведенный ниже код будет отправлен в caddy/cmd/main.go в функции loadConfig после того, как конфигурация будет полностью прочитана и вся проверка ошибок выполнена. У меня это примерно 156-я строка.

На этом этапе у нас есть переменная конфигурации с частью валидного файла конфигурации:

var configString = strings.Split(string(config), "\n")
var lines []string
for _, element := range configString {
var line = strings.TrimSpace(element)
if strings.HasPrefix(line, "alias") {
var tokens = strings.Split(line, " ")
var source = tokens[1]
var target = tokens[2]
line = fmt.Sprintf("encode gzip\n handle %[1]s/* {\n root %[1]s/* %[2]s\n uri strip_prefix %[1]s\n file_server\n }", source, target)
}
lines = append(lines, line)
}
var c = strings.Join(lines, "\n")
config = []byte(c)

Первая строка этого фрагмента преобразовывает байты в строку, а затем разбивает ее по символу новой строки. Затем я перебираю каждую строку и обрезаю ее. Затем я проверяю, начинается ли строка с alias. Если это так, то я разделяю эту строку на пробелы, чтобы получить набор токенов. Первый токен — это ключевое слово, второй токен — источник, а последний токен — назначение. Я хочу сопоставить URL-адрес, который ссылается на источник, с файлом в месте назначения.

Следующая строка — это перезапись, я хочу изменить строку, чтобы она была логикой обработчика из предыдущей. Я также добавил сюда часть encode gzip, так как на самом деле я хочу, чтобы это было по умолчанию. В любом случае, у меня обычно есть только один alias в моих конфигурациях, так что это самое подходящее место, чтобы вставить его.

Следующая строка просто добавляет строку в массив строк.

Как только цикл заканчивается, я соединяю строки вместе и преобразую их в байт, а затем помещаю его обратно в переменную config.

Теперь мы можем пересобрать Caddy и запустить его:

> go build
> sudo ./caddy run

Вуаля! Мы не должны видеть никаких ошибок, и если мы перейдем к нашему приложению, мы сможем подтвердить, что все сжимается на лету и статические файлы обслуживаются должным образом!

Я был очень доволен тем, как это получилось, так как теперь мой конфиг стал еще меньше. Я думаю, что это может быть даже самое маленькое, что может быть. Это действительно конфиг из двух строк, одна из которых указывает порт обратного прокси, а другая — где найти статические файлы.

Заключительные мысли

Я использовал отладку печати, чтобы выяснить, как все происходило и в каком порядке, но я не нашел фактическую команду печати, поэтому в основном использовал caddy.Log().Info(). Это работало, но иногда мои строки терялись в реальных выводах Caddy.

Код Caddy действительно легко читается, и его было довольно легко понять. В основном я использовал grep для поиска, а использование CoC с vim в основном позволяло мне даже не обращаться к документации по go, потому что я все понял из нее. Я не знаю, насколько это связано с тем, что go является простым языком и имеет стандартный форматтер, или просто сам Caddy очень хорошо структурирован. Вероятно, и то и другое.

Было довольно здорово полагаться на комплишены кода, чтобы рассказать мне о функции TrimSpace и функции Join. go немного странный в том смысле, что все кажется функцией, в которую вы что-то передаете, а не классом, в котором вы вызываете метод. Я привык делать что-то вроде array.append или array.push, но не array = append.

Также есть что сказать о том факте, что go имеет репутацию языка, благоприятного для начинающих, и является небольшим языком. Я до сих пор неуютно себя чувствую, когда собираю nginx из исходникови ковыряюсь в коде на C. С другой стороны, Go так похож на javascript, что страха не было.

Я удивлен тем, как легко и быстро было внести это изменение. Интересно, где стоит делать это изменение, если бы я хотел, чтобы оно стало частью проекта caddy. Это очень плохой патч.

Одна из проблем с моим кодом прямо сейчас заключается в том, что он слишком сложен в отношении наличия конечной косой черты в путях алиасов. Если вы добавите один, я полагаю, это вызовет проблемы с удалением префикса. Я мог бы добавить некоторую логику, чтобы убедиться, что я удаляю конечную косую черту в путях, прежде чем переписывать, но я оставлю этот способ. Есть также факт, что пути могут содержать пробелы, и я разделяю их, чтобы получить токены. Реальным способом получить токены было бы разобрать строку по символу и создать токены, но у меня нет пробелов в МОИХ путях, поэтому я не беспокоюсь об этом.

Я бы хотел, чтобы у go были именованные параметры в sprintf, мне не нравится, что он использует индексы. Хотелось бы, чтобы в go были шаблонные литералы, как в javascript. Использование функции append для массива — это странно. Синтаксис цикла for — это странно. Я уверен, что если бы я провел больше времени с go, то я увидел бы смысл, но первоначальная реакция такова, что все это странно.

Использование ключа--watch в Caddy — это очень здорово, я могу добавить новый проект без необходимости перезапускать веб-сервер, что очень удобно.

В целом, это было забавное отвлечение от работы.

--

--

Vitalii Filiuchkov

SRE Lead in Cloud Division of the largest telecom operator in Russia