Warning
2024年現在無料プランではDocker hubのBuild automationは使えない可能性があります。
久々にこのブログを更新するついでに、masterにpushするだけで自動デプロイする環境を整理した。
シンプルで便利な形に押し込めたので記事としてまとめておく。
サーバーで動かすDockerイメージ
以下のとおりである。
イメージ名 | 説明 |
---|---|
endaaman/endaaman.me |
本ブログのNuxt製フロントエンド。endaaman.me |
endaaman/api.endaaman.me |
本ブログのGo製バックエンド。api.endaaman.me |
nextcloud:stable-fpm |
俺用Nextcloudのapp image |
mariadb:10 |
俺用NextcloudのDB image |
nginx:1.17 |
俺用Nextcloudのfrontend image |
jwilder/nginx-proxy |
endaaman/endaaman.me 、endaaman/api.endaaman.me 、nginx:latest をバーチャルホストとして束ねる |
jrcs/letsencrypt-nginx-proxy-companion |
nginx-proxy の配下をまとめてSSL化するすごいやつ |
これらをdocker-compose.yml
で全部まとめて、
$ docker-compose.yml pull && docker-compose.yml up -d --build
で更新&再起動できるようにしている。
デプロイの流れ
- masterのpushをトリガーとしてDocker Hubで
Dockerfile
をビルド - Webhookでサーバーにビルド完了通知としてPOSTリクエストが飛ぶ
- POSTをもらったらイメージをpullしてコンテナを再起動
以上の工程となっている。
1. masterのpushをトリガーとしてDocker HubでDockerfile
をビルド
GitHubのリポジトリとDocker Hubのリポジトリを連携させる(Builds → Link to GitHub)。
Configure Automated Buildsで設定を画面を開き、BUILD RULESから連携したリポジトリのmasterへのpush時に自動的にビルドが走るように設定できる。
手動でビルドを回すこともできる。ここまではやってる人も多いかもしれない。
2. Webhookでビルド完了を通知
Docker HubのWebhookはイメージビルド完了時に雑にPOSTを投げてくれる。リポジトリページのWebhooksでそのPOSTの発行先を登録しておく。
3. サーバーでイメージをpullしてコンテナを再起動
ビルドが終わるとPOSTが飛んでくるので、それを受け付けるスクリプトを書いた。
import argparse
import logging
import os
import subprocess as sp
import http.server
fmt = '[%(asctime)s]%(levelname)s: %(message)s'
logging.basicConfig(level=logging.INFO, format=fmt, datefmt='%Y-%m-%d %H:%M:%S',)
logger = logging.getLogger(__name__)
parser = argparse.ArgumentParser()
parser.add_argument('--port', type=int, default=8080)
parser.add_argument('--host', type=str, default='127.0.0.1')
parser.add_argument('--script', type=str, required=True)
args = parser.parse_args()
HOST = args.host
PORT = args.port
SCRIPT = os.path.abspath(args.script)
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.log_client()
self.respond(501)
def do_POST(self):
self.log_client()
self.log_message(f'Starting {SCRIPT}')
result = sp.Popen(['bash', SCRIPT]).wait()
if result == 0:
self.log_message(f'Done script.')
status = 200
else:
self.log_error(f'Script failed.')
status = 500
self.respond(status)
def respond(self, status):
self.send_response(status)
self.send_header('Content-length', 0)
self.end_headers()
def log_client(self):
host, port = self.client_address
self.log_message(f'{self.requestline} from {host}:{port}')
def log_request(self, code='-', size='-'):
return
def log_message(self, fmt, *args):
logger.info(fmt % args)
def log_error(self, fmt, *args):
logger.error(fmt % args)
if __name__ == '__main__':
if not os.path.exists(SCRIPT):
logger.error(f'Target script ({SCRIPT}) does not exist.')
exit(1)
httpd = http.server.HTTPServer((HOST, PORT), Handler)
logger.info(f'Starting server at {HOST}:{PORT}')
logger.info(f'Target script: {SCRIPT}')
httpd.serve_forever()
logger.info(f'Server closed.')
ログ周りのごちゃごちゃが多いが大したことはしてない。
たとえばpython webhook.py --script hoge.sh
で起動すると、POSTをもらうとbash hoge.sh
が実行される。Pythonの標準ライブラリだけで動くようにしてるのがミソ。
systemdでデーモン化
まずdocker-compose
で最新イメージをpullしてコンテナを再起動するスクリプトを用意する。
set -eu
cd $(realpath $(dirname "$0"))
docker-compose pull -q
docker-compose up -d --build --quiet-pull
echo done
POSTをトリガにこのスクリプトを実行するPythonスクリプトをデーモン化するsystemd serviceを作る。ただし、スクリプトのディレクトリを決め打ちしたくないのでテンプレートを書く。
[Unit]
Description=webhook
[Service]
WorkingDirectory=${DIR}
ExecStart=python webhook.py --host '0.0.0.0' --port '45454' --script ./restart-docker-compose.sh
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
このようにワーキングディレクトリを$DIR
変数にしておいて、envsubst
であとから流し込むようにする。
このテンプレートに変数を埋め込んでserviceファイルとして配置するスクリプトを書く。
set -eu
DEST_DIR=$HOME/.config/systemd/user
DEST_PATH=$DEST_DIR/webhook.service
mkdir -p $DEST_DIR
export DIR=$(realpath $(dirname "$0"))
cat ./files/webhook.template.service | envsubst > $DEST_PATH
echo Wrote "$DEST_PATH"
install-systemd-unit.sh
がrestart-docker-compose.sh
と同じ場所にあるのを確認してから、bash install-systemd-unit.sh
でserviceファイルを配置して、webhook.service
を有効化&起動するだけ。
さらに、ユーザーレベルのサービスはログアウト時に潰れてしまうので、それを回避するために
$ sudo loginctl enable-linger <USER NAME>
しておく。これであとは何も考えずにGitHubにソースをpushするだけ。
Tip
Webhook余談
読めば分かるとおり、POSTに対して認証などのフィルタ機構がないので、誰か勝手に野良POSTしてもリスタートのスクリプトが走ってしまう。
docker-composeなので実際は<container name> is up-to-date
となり再起動はかからないが、どうにも気持ち悪い。
今回は無視しているがWebhooksDocker Hub Webhooks | Docker Documentationの中身を見ても認証に使えそうなものはないし、POSTを発行するホストも普通のAWSのサーバーなのでどれが正式なリスタート要求のPOSTなのか識別するすべがない。
一応"callback_url"
フィールドがあるのでこのエンドポイントにPOSTした可否でもって実際にリスタートするかどうか決めてもいいが、そこまで実装するのも手間なので端折った。ただ、WebhooksのView HistoryでPOSTリクエストの履歴を見るとこのPOSTの成功の可否自体が記録されているので、コールバックを使って認証するのも違うようにも思われる。
どうするのが正解なのだろうか。
感想
動いているのはしょうもないPythonスクリプトだけでやっていることも単純なので気楽。ただしDocker Hubのビルドがくっそ遅いので、気の短い人は手元でdocker build
しとdocker push
を叩けば時短になる。