Рестарт Passenger без простоя из Capistrano

Когда у вас достаточно нагруженный проект на RoR и для запуска вы используете Passenger, то рестарт приложения может вызывать небольшой (а иногда и простой) простой. Это не есть хорошо.

Passenger имеет две версии - Open Source и Enterprise. Так вот в Enterprise версии есть опция --rolling-restart, которая и позволяет не перезапускать сразу все инстансы приложения, а делать это поочереди.

В Capistrano это настраивается элементарно:

set :passenger_restart_options,
  -> { "#{deploy_to} --ignore-app-not-running --rolling-restart" }

Мониторим перезапуск вот так:

passenger-status

Деплой на много хостов с Capistrano

Не так часто, но бывает надо сделать деплой на, скажем, 20 хостов, которые находятся за bastion'ом.

Если у вас 5 хостов и все работает как часы, то нет проблем, казалось бы. Увеличиваем количество хостов, запускаем деплой, profit.

Но чаще всего вы получите ошибку и будете очень долго удивляться, как же так...

Ошибка будет вида:

cap aborted!
Errno::EPIPE: Broken pipe
/Users/khiem-nguyen/workspace/ruby/nenga-onepiece/cl-chef/deploy/vendor/bundle/ruby/2.3.0/gems/net-ssh-3.2.0/lib/net/ssh/transport/server_version.rb:44:in `write'
/Users/khiem-nguyen/workspace/ruby/nenga-onepiece/cl-chef/deploy/vendor/bundle/ruby/2.3.0/gems/net-ssh-3.2.0/lib/net/ssh/transport/server_version.rb:44:in `negotiate!'
/Users/khiem-nguyen/workspace/ruby/nenga-onepiece/cl-chef/deploy/vendor/bundle/ruby/2.3.0/gems/net-ssh-3.2.0/lib/net/ssh/transport/server_version.rb:32:in `initialize'
/Users/khiem-nguyen/workspace/ruby/nenga-onepiece/cl-chef/deploy/vendor/bundle/ruby/2.3.0/gems/net-ssh-3.2.0/lib/net/ssh/transport/session.rb:84:in `new'
/Users/khiem-nguyen/workspace/ruby/nenga-onepiece/cl-chef/deploy/vendor/bundle/ruby/2.3.0/gems/net-ssh-3.2.0/lib/net/ssh/transport/session.rb:84:in `initialize'
/Users/khiem-nguyen/workspace/ruby/nenga-onepiece/cl-chef/deploy/vendor/bundle/ruby/2.3.0/gems/net-ssh-3.2.0/lib/net/ssh.rb:232:in `new'
/Users/khiem-nguyen/workspace/ruby/nenga-onepiece/cl-chef/deploy/vendor/bundle/ruby/2.3.0/gems/net-ssh-3.2.0/lib/net/ssh.rb:232:in `start'
/Users/khiem-nguyen/workspace/ruby/nenga-onepiece/cl-chef/deploy/vendor/bundle/ruby/2.3.0/gems/sshkit-1.11.2/lib/sshkit/backends/connection_pool.rb:59:in `call'
/Users/khiem-nguyen/workspace/ruby/nenga-onepiece/cl-chef/deploy/vendor/bundle/ruby/2.3.0/gems/sshkit-1.11.2/lib/sshkit/backends/connection_pool.rb:59:in `with'
/Users/khiem-nguyen/workspace/ruby/nenga-onepiece/cl-chef/deploy/vendor/bundle/ruby/2.3.0/gems/sshkit-1.11.2/lib/sshkit/backends/netssh.rb:155:in `with_ssh'
/Users/khiem-nguyen/workspace/ruby/nenga-onepiece/cl-chef/deploy/vendor/bundle/ruby/2.3.0/gems/sshkit-1.11.2/lib/sshkit/backends/netssh.rb:49:in `upload!'
/Users/khiem-nguyen/workspace/ruby/nenga-onepiece/cl-chef/deploy/vendor/bundle/ruby/2.3.0/gems/capistrano-3.6.1/lib/capistrano/tasks/git.rake:24:in `block (3 levels) in <top (required)>'
/Users/khiem-nguyen/workspace/ruby/nenga-onepiece/cl-chef/deploy/vendor/bundle/ruby/2.3.0/gems/sshkit-1.11.2/lib/sshkit/backends/abstract.rb:29:in `instance_exec'
/Users/khiem-nguyen/workspace/ruby/nenga-onepiece/cl-chef/deploy/vendor/bundle/ruby/2.3.0/gems/sshkit-1.11.2/lib/sshkit/backends/abstract.rb:29:in `run'
/Users/khiem-nguyen/workspace/ruby/nenga-onepiece/cl-chef/deploy/vendor/bundle/ruby/2.3.0/gems/sshkit-1.11.2/lib/sshkit/runners/parallel.rb:12:in `block (2 levels) in execute'
Tasks: TOP => git:check => git:wrapper
The deploy has failed with an error: Broken pipe
** Invoke deploy:failed (first_time)
** Execute deploy:failed


** DEPLOY FAILED
** Refer to log/capistrano.log for details. Here are the last 20 lines:

И дальше еще большой стэк-трейс, который вам ровным счетом ничего не даст.

Опытным путем вы прийдете к тому, что 10 хостов деплоятся нормально, а вот 11 уже нет. И дело тут совсем не в Capistrano, а в вашем ssh сервере на bastion'е.

Во всем виноваты значения по умолчанию опции MaxStartups. Увеличьте ее первое значение и все у вас полетит, как вы того ожидаете.

Итак, было:

# MaxStartups 10:30:60

Стало:

MaxStartups 30:30:60

Ошибка при установке gem mysql2

Если вы получаете ошибку типа:

Error installing mysql2: ERROR: Failed to build gem native extension.

то решение очень простое. Сначала ставим openssl

brew install openssl

А потом ставим gem необходимой версии:

gem install mysql2 -v '0.5.2' -- --with-ldflags=-L/usr/local/opt/openssl/lib --with-cppflags=-I/usr/local/opt/openssl/include

Версию gem'a поправьте под свою.

apg в Mac OS

apg - это генератор паролей как pwgen, только они запоминаются сильно легче.

Под Ubuntu он есть в репах, а в MacOS ставим так:

brew tap jzaleski/homebrew-jzaleski
brew install apg

Собираем логи из docker в GrayLog

logspout

Последнее время я все чаще стал ходить на сервера и смотреть логи руками. Но доккер так устроен, что найти что-то в логах крайне сложно. В результате я пришел к выводу, что пора собирать логи где-то в ElasticSearch.

Классический ELK я не очень люблю, а потому стал искать варианты, которые мне бы понравились. В итоге, пришел к GrayLog.

Оставался вопрос сборщика логов, для отправки их в GrayLog, который сложит их в ElasticSearch.

Перебрал кучку малую и остановился на Logspout. Он умеет отправлять логи в syslog, GELF и, что не маловажно, он умеет multiline logs. Т.е. Tracebacks будут не построчно, а целиком, как положено.

Ну что ж, поднимаем.
Читать далее...

Копируем S3 баккет между AWS аккаунтами

S3

Предположим, что нам надо скопировать данные из баккета в одном аккаунте, в баккет в другом аккаунте, да еще и в другом регионе.

Что нам понадобится

  • Два AWS аккаунта (один для исходного баккета, второй для баккета назначения)
  • IAM пользователь в аккаунте с баккетом, куда мы будем копировать данные (тут есть документация, как создать IAM пользователя в аккаунте)
  • Настроенная утилита aws-cli на локальной машине с ключами ранее созданного пользователя (документация по утилите)

Шаг 1: Получаем номер аккаунта

Заходим в аккаунт назначения. Переходим в SupportSupport center и копируем номер аккаунта.

Шаг 2: Настраиваем исходный S3 баккет

Заходим в аккаунт-источник. Создаем баккет источник (если его еще не было, то что вы тут делаете? ;) документация как создать баккет). Аттачим полиси, которое представлено ниже (как приаттачить полиси). Если вы только создали баккет, то залейте несколько файлов туда для теста.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "DelegateS3Access",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::DESTINATION_BUCKET_ACCOUNT_NUMBER:root"
            },
            "Action": [
                "s3:ListBucket",
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::SOURCE_BUCKET_NAME/*",
                "arn:aws:s3:::SOURCE_BUCKET_NAME"
            ]
        }
    ]
}

Шаг 3: Настраиваем баккет назначения

Заходим в аккаунт назначения. Создаем баккет, куда будем копировать данные.

Шаг 4: Аттачим полиси к созданному пользователю

Аттачим к созданному пользователю полиси, которое представлено ниже (как приаттачить полиси пользователю).

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket",
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::SOURCE_BUCKET_NAME",
                "arn:aws:s3:::SOURCE_BUCKET_NAME/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket",
                "s3:PutObject",
                "s3:PutObjectAcl"
            ],
            "Resource": [
                "arn:aws:s3:::DESTINATION_BUCKET_NAME",
                "arn:aws:s3:::DESTINATION_BUCKET_NAME/*"
            ]
        }
    ]
}

Шаг 5: Синхронизируем данные

Если вы сделали все шаги, описанные выше, то можно начать синхронизацию.

aws s3 sync s3://SOURCE-BUCKET-NAME s3://DESTINATION-BUCKET-NAME --source-region SOURCE-REGION-NAME --region DESTINATION-REGION-NAME

Добавляем memcached в стэк Wordpress by Bitnami

wordpress

Понадобилось мне тут в проекте добавить поддержку Memcached в Wordpress, развернутый на Amazon. Кто не в курсе, то там используется предустановка от Bitnami. Поскольку в этом стэке используется не системный PHP, а кастомный, то и добавить туда модуль PHP не тривиальная задача. При попытке использовать доку от Bitnami я получил кучу ошибок и перерыл кучу форумов, чтоб их решить. В результате все мои изыскания вылились в простой скрипт, который все ставит и перезапускает php-fpm.

export LIBMEMCACHED_VERSION='1.0.18'
export PHPMEMCACHED_VERSION='2.2.0'

# Install dependencies

sudo apt update
sudo apt install -y memcached
sudo apt install -y build-essential libtool autoconf unzip wget git pkg-config

# Create folder

sudo mkdir /opt/bitnami/common
sudo chmod 777 /opt/bitnami/common/

# Setup libmemcached

cd /tmp
wget https://launchpad.net/libmemcached/1.0/${LIBMEMCACHED_VERSION}/+download/libmemcached-${LIBMEMCACHED_VERSION}.tar.gz
tar -zxf libmemcached-${LIBMEMCACHED_VERSION}.tar.gz
cd libmemcached-${LIBMEMCACHED_VERSION}
./configure --prefix=/opt/bitnami/common
make
sudo make install

# Setup php extension

cd /tmp
export PHP_AUTOCONF=/usr/bin/autoconf
export PHP_PREFIX=/opt/bitnami/php
wget http://pecl.php.net/get/memcached-${PHPMEMCACHED_VERSION}.tgz
tar -xzf memcached-${PHPMEMCACHED_VERSION}.tgz
cd memcached-${PHPMEMCACHED_VERSION}
/opt/bitnami/php/bin/phpize
./configure --enable-memcached --with-zlib-dir=/opt/bitnami/common --with-libmemcached-dir=/opt/bitnami/common --with-php-config=/opt/bitnami/php/bin/php-config --disable-memcached-sasl
make
sudo make install
echo 'extension=memcached.so' | sudo tee -a /opt/bitnami/php/etc/php.ini

# Restart php-fpm

sudo /opt/bitnami/ctlscript.sh restart php-fpm

iRedMail и “+” в адресе

iredmail

Я как-то давно отказался от услуг Google в плане почты и переехал на свой сервер. Одно время пользовался решением в docker’e, но там было не все гладко. В результате поднял свой сервер и поставил туда iRedMail. Это оказалось сильно дешевле, чем платить сторонним сервисам.

Но там мне не хватало одной очень удобной возможности — добавление символа “+” после имени пользователя, чтоб была возможность фильтрации по получателю. Это очень удобно, т.к. для каждого сервиса можно завести свой индивидуальный почтовый ящик. Что, в свою очередь, помогает идентифицировать утечку. ;)

Итак, что надо сделать, чтоб получать почту в свой ящик, отправленную на адреса вида “[email protected]”.

Открываем файл /etc/postfix/mysql/virtual_alias_maps.cf и меняем там одну строку.

Было:

query       = SELECT ... WHERE forwardings.address='%s' AND ...

Стало:

query       = SELECT ... WHERE forwardings.address=CONCAT(SUBSTRING_INDEX('%u', '+', 1), '@%d') AND ...

Перезагружаем Postfix и наслаждаемся. :)

Merge git-crypted files

git-crypt

Давно искал как получить возможность мерджить файлы, которые зашифрованы git-crypt.

В результате нашел вот такое решение.

Во-первых, добавьте в .gitattributes параметр merge=git-crypt для всех файлов, управляемых git-crypt:

crypt/** filter=git-crypt diff=git-crypt merge=git-crypt

затем добавьте в конец файла .git/config следующее:

[merge "git-crypt"]
    name = A custom merge driver used to merge git-crypted files.
    driver = ./my-merge-tool.sh %O %A %B
    recursive = binary

и, наконец, создать файл в корне repo my-merge-tool.sh содержащий:

ancestor_decrypted="$1__decrypt"
current_decrypted="$2__decrypt"
other_decrypted="$3__decrypt"
echo ""
echo "###########################"
echo "# Git crypt driver called #"
echo "###########################"
echo ""

echo "Decrypting ancestor file..."
cat $1 | git-crypt smudge > "${ancestor_decrypted}"
echo "Decrypting current file..."
cat $2 | git-crypt smudge > "${current_decrypted}"
echo "Decrypting other file..."
cat $3 | git-crypt smudge > "${other_decrypted}"
echo ""

echo "Merging ..."
git merge-file -L "current branch" -L "ancestor branch" -L "other branch" "${current_decrypted}" "${ancestor_decrypted}" "${other_decrypted}"
exit_code=$?
cat "${current_decrypted}" | git-crypt clean > $2

echo "Removing temporary files..."
rm "${other_decrypted}" "${ancestor_decrypted}" "${current_decrypted}"

if [ "$exit_code" -eq "0" ]
then
    echo "@@@ No conflict!"
else
    echo "@@@ You need to solve some conflicts..."
fi

exit $exit_code

и убедитесь, что он выполним:

chmod +x my-merge-tool.sh

Это все, теперь вы можете делать merge, cherry-pick, использовать свой любимый mergetool, как обычно!

Заставляем GitX подписывать коммиты

Я часто использую GitX, как вариант замены коммандной строки, т.к. в нем удобно добавлять несколько коммитов один за одним, выбирая разные файлы или группы файлов. Но вот незадача, он не умеет ставить на commit GPG подпись. Т.е. получается в истории коммитов часть подписана, а часть нет. Некрасиво.

Чтож, научить его это делать несложно, если написать простой wrapper.

Для начала в .gitconfig добавляем такие строки:

[commit]
  gpgsign = true

Теперь создаем враппер по пути что-то типа ~/bin/gitx-signed-commit с содержимым:

1
2
3
4
5
6
7
8
9
#!/usr/bin/env bash

args=("[email protected]")

if [[ "$1" = "commit-tree" ]] && [[ "$(git config --get commit.gpgsign)" = "true" ]]; then
  args=("commit-tree" "-S" "${args[@]:1}")
fi

git "${args[@]}"

Даем права на запуск:

$ chmod 755 ~/bin/gitx-signed-commit

Осталось подключить в GitX. Для этого идем в настройки, выбираем вкладку General, тыкаем на Git Executable и выбираем наш скрипт.

Все. С этого момента все коммиты будут подписываться корректно.