Во время давней поездки в Фанские Горы я познакомился с интересным чуваком из Беларуси. У него был принцип “жить не больше, чем на доллар в день”. Экономная одежда, снаряга, далёкая от “hipster/fancy” (а многие, включая меня, грешили технической стороной вместо реальной необходимости). Можно было подумать, что он обычный человек с очень скромным доходом и сбережениями. В этом случае принято говорить, что ты экономишь по “зову души”, не будешь же рассказывать, что у тебя просто нет денег. Но нет — у того чувака были сбережения в сотни тысяч долларов. И вот тут понимаешь, что именно такая экономия — это “зов души”.

К сожалению, моя экономия ближе к первой. Не то, что денег нет, но тратить не глядя невозможно. Слоган “не нужно экономить, нужно больше зарабатывать” не работает в моём случае. Не скажу, что это невозможно, однако подразумевает большие, дорогостоящие и нервные перемены, на что идти пока не хочется. Так что я начал анализировать затраты и пытаться отрезать всё, от чего можно отказаться.

В прошлой заметке о Fastmail я писал, что “Hetzner мог стать третьей жертвой экономии, восстановил” (второй была подписка на Github). Всё же, когда начинаешь рубить всё, то начинаешь задумываться даже о копеечных оптимизациях. За Hetzner я платил что-то типа €2.60 в месяц. И у него было два назначения:

  1. Хостинг блога и поддержка уже давно неработающего подкаста “ZeroIQ”.
  2. Хост для быстрых проверок (например, нужно проверить, что очередное задеплоеное по работе приложение недоступно из публичного инета)
  3. Джампхост для доступа на домашний сервер.

Ключевая позиция — 3. Один сервер стоит на одной площадке (Plex + периодически запускаемый self-hosted Gitlab). Есть ещё вторая площадка, где работали пару Raspberry Pi для тестов по старому проекту. Проект, к сожалению (или к радости, не знаю), закрылся, распы остались. Первая площадка имеет статический IP, вторая — чисто динамика.

И вот тут фрагменты сложились:

  1. Рабочий VPN ходит из фиксированного сабнета. Можно организовать прямой whitelisting на первой площадке.
  2. Вторая площадка работает на динамиках, но ведь есть динамический DNS. Написал быстро скрипт, который проверяет адрес и добавляет его в ufw на первой площадке.
  3. Тесты #2 могу запускать и с домашних хостов, ведь доступ уже есть, а на случай проблем с одной всегда доступна вторая площадка
  4. Хост на Hetzner не нужен

Обновление UFW по динамическому DNS

Я уже давно перешёл от supervisord и crond на systemd/User. Очень доволен. И от гранулярности (cron нельзя заставить работать каждые 20 секунд, например) и от логирования (вывод из программ идёт сразу в syslog, не нужно писать враппер для логгинга).

Скрипт (претензия одна — старые адреса не чистит, мне лично пока ок):

$ cat /home/os/.local/bin/ufwdyn.sh
#!/usr/bin/env bash
ip=$(python -c 'import socket; print socket.gethostbyname("dyndns.domain.com")')
sudo ufw allow from $ip

Сервис:

$ cat ~/.config/systemd/user/ufwdyn.service
[Unit]
Description=Add dyndns.domain.com to UFW
After=network.target network-online.target dbus.socket

[Service]
Type=oneshot
ExecStart=/home/os/.local/bin/ufwdyn.sh

Таймер:

$ cat ~/.config/systemd/user/ufwdyn.timer
[Unit]
Description=Add dyndns.domain.com to UFW

[Timer]
OnCalendar=*-*-* *:*:00
Persistent=true
Unit=ufwdyn.service

[Install]
WantedBy=timers.target

Запуск:

$ systemctl --user daemon-reload
$ systemctl --user enable ufwdyn.timer
$ systemctl --user start ufwdyn.timer

Проверка:

$ systemctl --user list-timers
NEXT                          LEFT     LAST                          PASSED     UNIT                ACTIVATES
Sun 2018-08-19 12:14:00 CEST  19s left Sun 2018-08-19 12:13:25 CEST  15s ago    ufwdyn.timer        ufwdyn.service

Работает идеально. Да, до своих переделок я использовал бесплатный noip.com, раз в месяц нужно было подтверждать домен.

Перенос на Raspberry Pi

Рассказывать даже толком не о чем. Пользуюсь Raspbian Stretch Lite. Сам rPi — это Raspberry Pi 2 Model B. Воткнут по шнурку, хотя мог бы работать и по Wifi (до этого за месяца три только однажды он отваливался от Wifi), но я предпочитаю всё-таки LAN. Отвалится сейчас, когда я в отъезде — не страшно, неделя даунтайма для блога — это мелочи (кстати, это пример “попускания”, раньше я считал, что блог должен работать с надёжностью 99.999%, смешно).

Залил образ на MicroSD, загрузил rPi, сделал очевидные базовые настройки, включил OpenSSH. Заблокировал пользователя pi с паролем “raspberry”. Создал нового, в sshd оставил только аутентификацию по публичным ключам. На рутере открыл полный доступ к 22/tcp, 80/tcp, 443/tcp и внёс свои адреса в UFW.

Дальше скопировал каталоги с конфигами nginx, полностью letsencrypt, поставил эти пакеты. Обновление блога идёт через выделенного пользователя для деплоев, скопировал и его один-в-один. Рестарт — всё ок.

Запустил Gitlab CI — всё отработало, ничего менять не пришлось, кроме DNS для хоста.

Интересно то, что я не увидел разницы в скорости работы сайта. Понятно, что в rPi MicroSD, а не SSD и скачивание файлов в “параллель” будет мучительным, но при обычном использовании работает как на мой вкус очень достойно.

DNS

А вот с DNS произошла лёгкая засада. Но такие риски уже давно относятся к “профессиональным” привычкам, поэтому быстро придумал, как решать. Но с ненулевыми затратами.

Раньше домены у меня хостились на Fastmail. Стал искать варианты. Оказалось, что Digital Ocean даёт бесплатных хостинг DNS и даже необязательно иметь дроплеты (когда-то они не давали это сделать).

Итак, что может быть на первый взгляд проще, чем просто переключить DNS со старого сервера на новый? Даже с учётом “динамичности”? Ставишь CNAME на имя в DynDNS и наслаждаешься. Да. Для поддоменов типа “zeroiq.ctrld.me” всё ок. А вот для top’а (@) CNAME не поставишь и всё, “ctrld.me” изменить нельзя.

Ok, давай думать. О! У Digital Ocean есть API, весьма кстати. Начал смотреть. Но, блин, у API-токенов два режима — “read-only” и “full access”. Полный доступ подразумевает и создание дроплетов в том числе. Что произойдёт, если токен утечёт к тому, кто знает, как с ним обращаться? Правильно, если сразу не среагируешь, то влетаешь на деньги (лимит у меня 20 дроплетов, но если я не увижу это вовремя и этот кто-то подымет максимальные инстансы?).

Нет, как-то стрёмно. Хоть и вероятность “ухода” токена минимальна, и мой хост никому не нужен, но против “профессиональных” привычек не попрёшь.

Ok, что делать? У кого есть нормальное разграничение доступа для доступа? Конечно же у Amazon. Сколько денег? $0.50 в месяц за зону плюс грёбаный налог, который ввели в прошлом месяце из-за Amazon Europe. Ладно, в €0.51 впишусь.

Создаю IAM-пользователя с Programmatic Access, ставлю политику:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt0",
            "Effect": "Allow",
            "Action": "route53:ChangeResourceRecordSets",
            "Resource": "arn:aws:route53:::hostedzone/Z3CMCENSOREDLHQ"
        },
        {
            "Sid": "Stmt1",
            "Effect": "Allow",
            "Action": "route53:ListTagsForResource",
            "Resource": [
                "arn:aws:route53:::hostedzone/Z3CMCENSOREDLHQ",
                "arn:aws:route53:::healthcheck/*"
            ]
        },
        {
            "Sid": "Stmt2",
            "Effect": "Allow",
            "Action": "route53:ListResourceRecordSets",
            "Resource": "arn:aws:route53:::hostedzone/Z3CMCENSOREDLHQ"
        },
        {
            "Sid": "Stmt3",
            "Effect": "Allow",
            "Action": "route53:GetChange",
            "Resource": "arn:aws:route53:::change/*"
        }
    ]
}

Если Access Key/Secret уйдут, то “счастливчик” может наслаждаться изменениями зоны ctrld.me до полного удовлетворения. Пусть порадуется.

На rPi поставил aws cli (на mac’е понятно оно стоит давно), сделал профиль для этой задачи.

Написал быстро прототип скрипта (приводить не буду, там только моя специфика):

  1. Взять внешние IPv4 и IPv6 адреса c ipv4.icanhazip.com и ipv6.icanhazip.com соответственно
  2. Сравнить с предыдущими значениями, которые при апдейте пишутся в файлы состояний. Не хочу же я дёргать AWS API без необходимости
  3. Если есть изменения, обновить IPv4 и IPv6 для набора доменов, включая @. TTL, понятно, 60 секунд.

Проверил — работает.

Переключил блог на домашний rPi. Открывается. Добавил health check в бесплатный uptimerobot.com — глухо, Connection Timeout. Pingdom Website Speed Test тоже как-то не может соединиться. Хммм. А! Идиот! Свои-то адреса разрешил, а вот из внешнего мира 80/tcp и 443/tcp не открыл. Fixed, всё ок, открывается отовсюду.

Скрипт уже отработал несколько раз, проверил логи — трафик пошёл.

Сделал бекап с Hetzner rsync’ом на домашний Nuc на всякий (на случай если забыл пару скриптов забрать оттуда, через месяц удалю). И удалил сервер. Всё, минус €2.60 в месяц, плюс €0.51 в месяц, сплошная экономия. Тем более, что rPi и так был, а за домашний инет я и так плачу, тут сэкономить не получится.

icanhazip.com

Кстати, сервис icanhazip.com абсолютно потрясающий:

$ curl -I ipv4.icanhazip.com
HTTP/1.1 200 OK
Server: nginx
Date: Sun, 19 Aug 2018 10:46:19 GMT
Content-Type: text/plain; charset=UTF-8
Content-Length: 13
Connection: close
X-SECURITY: This site DOES NOT distribute malware. Get the facts. https://goo.gl/1FhVpg
X-RTFM: Learn about this site at http://bit.ly/icanhazip-faq and do not abuse the service.
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET

Посмотрите на заголовок X-RTFM и походите по сайту. Я просто наслаждался каждой страницей. А Résumé вообще сказка.

Golang

Уже давно хочу изучить Golang. С моей работой всё времени не было (если вы из подобной области, то и так понимаете, что “CD” обозначает на “Continuous Delivery”, а “Continuous Deadline”).

Пару недель назад добрался и написал простую программу, которая берёт курс валют c fixer.io для ledger-cli. До этого у меня отлично работала маленькая система на shell в связке с Redis, она собирала каждые сутки данные с Fixer и Нацбанка Украины, но Fixer поменял API и у меня налаженный процесс сломался. Переписал первую часть (fixer) на Golang, очередь за НБУ и преобразование в микросервис, “как было” на shell.

Тут же задача интереснее, потому как есть кросс-платформенность (пишу на macOS, работать будет на rPi). За день переписал полностью, слегка отрефакторил, стало уже не так больно смотреть, как в начальном варианте.

Отладил, вставил в Makefile кросс-компиляцию:

# Cross compilation
build-arm:
        CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=5 $(GOBUILD) -o $(BINARY_ARM) -v

Закинул бинарник на rPi. И оно работает!!! Это восторг.

В общем я супер-доволен. Хоть и экономия почти нулевая, зато куча удовольствия от процесса переноса, за время которого я научился куче вещей. И реально я это сделал за два дня.

Всё, хватит нёрдствовать, пойду спортом займусь.