2018年11月3日土曜日

ホームページを自動デプロイできるようにしてみました

概要

久しぶりのテックネタの投稿です
自分のホームページ https://kakakikikeke.com は Ruby で書かれており docker コンテナとして動作しています
https に対応しているのですが内部で nginx コンテナも動いておりそこで SSL を受け付けています

デプロイは git push 後にホストマシンに SSH ログインして docker-compose build -> up という王道の流れなのですがいちいち SSH ログインするのが面倒でした
まぁコマンドも 2, 3 ほどなのでそこまで面倒な作業でもないのですが今回はそのデプロイ作業を自動化してみました

仕組みとしては bitbucket の webhook 機能を使ってホストマシンで webhook を受け取るアプリを起動します
そして、そのアプリに今まで実行していたデプロイコマンドを実行してもらうという何のひねりもない仕組みです

環境

  • CentOS 7.5
  • docker 18.06.1-ce
  • Ruby 2.5.0p0
  • bitbucket

bitbucket webhook にエンドポイントを追加する

まずは webhook にエンドポイントを追加します
homepage_autodeploy1.png

各リポジトリごとにエンドポイントを追加できます
今回はホストマシンでデプロイ用のアプリも動かします
Sinatra を使うので 9292 ポートに対して webhook を投げてもらうようにエンドポイントを追加します
/webhook にしていますがここは好きなものに変更してもらって OK です

スクショだと Enable request history collection にチェックがありませんがどんな webhook が飛んだかあとから確認したい場合はここにチェックを入れておくと履歴が確認できます

IP を許可

bitbucket のホワイトリスト IP は以下の通りです
ファイアウォールなどに設定の許可を入れましょう

  • 104.192.136.0/21
  • 34.198.203.127
  • 34.198.178.64
  • 34.198.32.85

今回の場合であれば 9292 ポートへのアクセスだけを許可すれば OK です

ライブラリインストール

ではアプリを作っていきます

  • bundle init
  • vim Gemfile
gem "sinatra"
  • bundle install --path vendor

webhook 用のアプリ

  • vim config.rb
require './app'
run Webhook
  • vim app.rb
require 'sinatra'
require 'json'
require 'open3'

class Webhook < Sinatra::Base
  helpers do
    def deploy
      File.open("./deploy.txt", mode = "r"){ |f|
        f.each_line{ |line|
          o, e, s = Open3.capture3(line)
          halt 500, "missed: #{e}" unless s.success?
        }
      }
    end
  end
  post '/webhook' do
    body = JSON.parse request.body.read
    p body
    body['push']['changes'].each { |push|
      deploy if push.has_key? 'new'
    }
    'deployed'
  end
end
  • vim deploy.txt
cd /root/ruby-homepage && git pull
cd /root/ruby-homepage && docker-compose down
cd /root/ruby-homepage && docker rmi rubyhomepage_web
cd /root/ruby-homepage && docker-compose up -d

少し解説

特に解説は不要かなと思いますが一応

まず POST /webhook が bitbucket から送られてくる webhook を受け取る URI になります
ここは bitbucket に設定したエンドポイントと同じ URI にしてください
ボディに push 時のいろいろな情報が格納されてきます
コミットした人やコミット番号などがあります
詳しくは上記のリンクからサンプルリクエストなどが確認できるます

今回は特に条件はないですが、new という新規のコミットがある場合にデプロイを実行するようにしました
new がないケースとしてはブランチを削除した場合になります
デプロイ時の条件を厳密にしたい場合はここに条件を加えれば OK です

deploy は helpers を使って実現しています
デプロイコマンドを書き連ねた deploy.txt を読み込み open3 で逐一実行してるだけです
open3 であればコマンドの結果 (0 とか 127 とか) をチェックできるので、もし成功じゃない場合は halt でエラーを返却します

クソ簡単です

アプリ起動

  • bundle exec rackup config.ru -o 0.0.0.0 &

外部から受付できるように 0.0.0.0 で LISTEN します
また、バックグラウンドで動作させるようにします
tmux バッファ上でフォアグラウンドで動作させても良いと思います
もっとちゃんとやるならデーモン化させたり systemd 配下で動かすようにしても良いと思います
停止するときはプロセス番号を調べて kill してください

という感じで開発自体は完了です
あとは push して挙動を確認したりしてみてください

デメリット

今回の場合、直接ホストに対してコマンドを実行するのでコンテナで動かすとコンテナ内にコマンドを実行してしまいます
なのでホストマシンに ruby + bundler をインストールしなければいけません
これまですべて docker で動かしていた環境にいきなり謎の勢力のためにバイナリをインストールしなければいけないのは納得いかない点があるかなと思います

また git pull をするときに認証されないことを想定しています
git-credential を使ってホストマシンに認証情報を保存していることが前提になっています (もしくは .git/config に ID/PW が書かれていても OK)

代替策

今回は webhook + コマンドにしましたが他にもいろいろな方法があると思います
例えば Bitbucket Pipeline を使う方法です
ここにコマンドを記載できるので push 後にホストに SSH してコマンドを実行することでデプロイすることも可能です
これであれば web アプリを作る必要もありません

あとはどうしても webhook 用のアプリをコンテナで動かしたい場合はコンテナからホストに SSH ログインしてコマンドを実行するか sidekiq などを使ってコンテナからホストにデプロイの支持を渡してそこからホスト側の worker でデプロイするなどでしょうか
ただ、これもコンポーネントが 1 つ増えるので微妙です

エージェントを配置してエージェントに指示を送る方法でもいいですが、今回の webhook アプリ方式とあまり変わらないかなと思います

そもそもイメージをお腹に抱えてしまっているのでそれもあまりよくない気がします
docker build -> docker push でどこかのレポジトリにアップロードして、ホストマシン側では pull -> up だけする感じのほうが良いかなと思います
ただ、その場合でも結局ホストマシン側でコマンドを実行する仕組みは必要かなと思います 

k8s の Deployment と ReplicaSet を使えばローリングアップデートが使えるのでそれも手かもしれません
ただその場合は k8s 環境の構築から必要になりますが、、

今回の方法はリポジトリからの push 型なのでそうでなくホストマシンからの pull 型で良い感じに簡単にできる仕組みにすると良いのかなと思います
やはりコマンドをホストマシンで直接実行するのは何か微妙なやり方なような気がします

むしろ何か良い代替策があれば教えていただきたいです

参考サイト