githubへpushすると自動ビルドしてくれるシステムの構築

10月からエウレカに参加している、インターンの粟本です。
インターンではPairsのインフラチームで、ansible周りの改善をしたりしています。
ちなみに、今ご覧になってる技術ブログの環境構築(WordPress以下のレイヤー)をしたのは私です。私の予想に反して、このページが見えているでしょうか?

さて、現在Pairsチームでは本番環境、及びステージング環境の両方でBlue-Green Deploymentを行っています。つまり新しいバージョンがリリースされる度に、新しくサーバーを用意し、ビルド&デプロイを走らせ、完全に新しい環境を用意した上でそちらに切り替えているわけです。しかしながら、ステージング環境では頻繁にコードの修正が入るため、毎回手動でビルド&デプロイを掛けるのは面倒ですし、時間も掛かってしまいます。そこでgithubのmasterブランチにpushされた時に、それを起点にしてビルド&デプロイを全て自動でやる仕組みがあれば良いという話になり、それをインターン中に作りました。

今回の記事ではそれについての解説と、実装上詰まった所について書きたいと思います。

目次

  • github webhookを利用した自動ビルド&デプロイの実装
    • pushの検知
    • ネットワーク構成
    • goサーバーを書く
  • slackにターミナル出力を表示する
    • slackへの通知
    • JSONに含められない文字
    • 色付き文字の処理

github webhookを利用した自動ビルド&デプロイの実装

pushの検知

githubにはwebhookという、リポジトリへのpushをHTTP経由で通知してくれる仕組みがあります。

詳細:https://developer.github.com/webhooks/

これを使うと、HTTPサーバーを用意しておけば、誰かがpushした瞬間にサーバーに対してPOSTリクエストを飛ばしてくれます。ちなみに、今回は試していませんがpush以外のアクションも取得する事ができます。

今回、ビルド&デプロイスクリプトについては既にエウレカで運用されているものが存在していたので、HTTPサーバーについてはこのスクリプトを叩くだけで後はよしなにやってくれるものと仮定します。

ネットワーク構成

ブログ

今回、ネットワーク構成は上記のようにしました。中継のgateway serverを起き、nginxのリバースプロキシを噛ませてあります。deploy serverは運用中のサーバーを触れるネットワーク空間にあり、セキュリティ上外部にオープンにできないため、このような構成となりました。

deploy server上に置くHTTPサーバーはgo言語で書いたもので、単純にPOSTリクエストを受け取ったらコマンドを叩くだけのものです。(実際にはビルド中にリクエストを受け取ってもサーバーで止めておいて、ジョブが終わってから新しいビルドを走らせるという実装にしていますが、ここでは単純化のために割愛)

goサーバーを書く

HTTPサーバーを作ってコマンド叩くだけなので難しい話は全くないのですが、何も考えずに受け取ったPOSTリクエストを全て受け入れてビルドを走らせてしまうと、githubと関係ない所から送られてきたリクエストを区別する事ができません。もちろん、区別しなかったからといってビルド&デプロイが無駄に走るだけで深刻なトラブルにはならないのですが、DoS攻撃されても迷惑なので、POSTリクエスト中にある認証情報のvalidationをきちんとやりましょう。

ちなみにここら辺の話は以下のページに公式の解説があります。
https://developer.github.com/webhooks/securing/

また、github webhookが特定のリポジトリに対するpushのみを通知する事ができないので、とりあえず全てのpushを受け取った上で、リポジトリ名で処理を分ける必要があります。

以下、コードになります。

// githubから送られてくるJSONをgo言語内で構造体として扱うための定義
type commiter struct {
Name string
}

type headCommit struct {
Url       string
Timestamp string
Committer commiter
}

type pusher struct {
Name string
}

type repository struct {
Name string
}

type data struct {
Ref         string
Head_commit headCommit
Pusher      pusher
Repository  repository
}

func Handler(w http.ResponseWriter, r *http.Request) {
status := 400
defer func() {
w.WriteHeader(status)
}()

// POSTでやってくる
if r.Method == "POST" {
rhash := make([]byte, 20)
// ヘッダー内のX-Hub-Signatureにセキュリティシグネチャが格納されている
hex.Decode(rhash, []byte(r.Header.Get("X-Hub-Signature")[5:]))

bufbody := new(bytes.Buffer)
bufbody.ReadFrom(r.Body)
buf := bufbody.Bytes()

// tokenはgithub webhookの設定ページで指定したもの
mac := hmac.New(sha1.New, []byte("token"))
mac.Write(buf)
ehash := mac.Sum(nil)

// bodyを上のtokenを用いてsha1で暗号化した結果が送られてきたシグネチャと一致するか検証
if !hmac.Equal(ehash, rhash) {
return
}

// JSON化
var d data
err := json.Unmarshal(buf, &d)
if err != nil {
return
}

status = 200
if d.Ref == "refs/heads/master" {
go Launch(d)  // masterの時はgoルーチンを起動してビルド&デプロイを開始
}
}
}

何度も言いますが、そんな難しい事はやってないです。上記のコードもコメントだけで十分ですよね。

slackにターミナル出力を表示する

slackへの通知

コマンドを投げたのは良いのですが、投げたら投げっぱなしはマズイですよね。ビルドがいつ終わったか、結果が成功か失敗か(失敗していたらそのログも)を通知する必要があります。エウレカではコミュニケーションツールとしてslackを多用しているため、そこに投げてみる事にしました。

HTTPサーバーから直接slackに対してAPIを叩いても良かったのですが、amazon sns経由でslackに通知を飛ばせる仕組みが既にあったので、これを利用しています。slackに直接APIを叩くのは別に難しくないのですが、slackのtokenを管理するのが面倒だったのと、折角既存の資産があるなら活用すればいいじゃないかと割と安直に考えた結果、このような構成になりました。

・・・が、間に変な物を挟んだ結果、この後デバッグに苦労する事になるのです。。。

JSONに含められない文字

デプロイが成功した時のメッセージは何の問題もなくslack上に表示されました。しかし、エラー発生時の標準出力の内容を載せると、amazon lambdaのコードが例外を吐いて上手くslack上に表示されませんでした。

私はしがないインターンの身なので、amazon lambdaのコードを弄る事ができず(正確には僕自身がlambdaのコードを読むのがめんどくさかったのと、そのコードを変更する事で生じる各種修正の責任を取りたくなかった)、lambda側で対応するのではなくgoサーバー側で対応する事になりました。

問題の例外はJSON.parse()(javascriptのJSON.parse()です)で発生しており、不正な文字列をJSONに変換しようとしているという事で、そういった文字を潰してあげる必要があります。JSONを文字列で扱った事がある場合のあるある案件ですが、一応簡単に解説しておきます。

例えば、amazon SNSに以下のようなJSONを投げているとしましょう。

{ "Payload": {"text": "terminal output"} }

ところがもしここで、エラー出力の中にダブルクォートがあったらどうでしょうか。

出力:Gopher said "I wanna play"!
JSON文字列:{ "Payload": {"text": "Gopher said "I wanna play"!"} }

"Gopher said "I wanna play"!"で文字列としたいのに、JSONパーサーは"Gopher said "で切ってしまい、parse errorが発生してしまうというわけです。こういう場合は以下のように変換を掛ける必要があります。

出力:Gopher said "I wanna play"!
変換後:Gopher said \"I wanna play\"!
JSON文字列:{ "Payload": {"text": "Gopher said \"I wanna play\"!"} }

これなら問題なくJSONにパースできます。

とまあこんな感じで幾つか地雷のようにある文字全てを適切に変換しないといけません。
変換方法として一番簡単なのは、空文字に置換してしまう事ですが、/foo/bar/hogeみたいなファイルパスがfoobarhogeに置換されてしまうとなかなか残念なので、ある程度真面目に変換してあげる必要があります。

もう一つ簡単な方法は全角文字にしてしまう事です。この場合、誰かがslack上に表示された文字列をコピペしたりすると悲劇が起きます。半角スペースをランダムでこっそり全角スペースに変換しておいてあげたいですね。

今回は以下の文字列を変換しました。

  • バックスラッシュ
  • スラッシュ
  • 改行
  • ダブルクォート
  • バッククォート
  • シングルクォート
  • セミコロン
  • アスタリスク
  • チルダ
  • 不等号

幾つか、「あれ、こんなのまで置換しなきゃいけないの?」という文字がありますが、slack上で表示する際にレイアウトが崩れないようにとかいろいろ考えた結果こうなりました。(といっても、完璧に検証しているわけではないので、もしかしたら不要な置換があるかもしれません)

余談ですが、上記の置換の中で一番醜悪なのは、チルダを全角チルダへ変換した事だと個人的に思っています。

色付き文字の処理

さて、これだけちゃんと変換すれば動くだろうと踏んだのですが、実際には例外が飛んでしまいました。

JSONのパースエラーの検証は、ブラウザのJavaScriptコンソールにターミナル出力を貼り付けて、JSON.parse()を掛ける事によって行っていたのですが、この検証は通るのに、実際に動かすと走らないという事態が発生していました。

この問題の原因は見出しにもある通り、色付き文字を適切に処理していなかった事が原因で、まあこれは当然上述のような検証には引っかからないわけです。わかってしまえば凄く当たり前な話ですがこれを突き止めるのに1日潰しました。

さて、ターミナルに出力されるカラフルな文字達は、その始まりと終わりに制御文字が入っています。色によって入っているバイトコードは異なるのですが、今回はそんなの全部要らないので、以下のような正規表現置換コードで全部消しました。

func Replace(regex, target_str, replace_string string) string {
re, _ := regexp.Compile(regex)
return re.ReplaceAllString(target_str, replace_string)
}

output_str = Replace("\\x1b\\[[0-9]+m", output_str, "")

ここまでやってようやくslack上にエラーログが表示されました。ちゃんちゃん。

感想的ななにか

構成自体は凄く簡単で、大してプログラムを書く必要もなかったため、一瞬で終わるタスクかと思っていたら、文字列処理という非本質的な部分で詰まり時間を浪費してしまいました。エンジニアあるあるです。

まだエウレカにジョインしてから日がないので、このような軽い内容ばかり扱ってますが、これからエウレカのサービスをよく知り、もっと深い所を弄っていけたらいいなぁ、なんて思ってます。個人的にはもっと泥臭い部分がやりたいですね。

まあそういった話はまた何時の日か。その日まで僕がエウレカに残ってるかはわかりませんが。クビになってるかも(笑)

  • このエントリーをはてなブックマークに追加

エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!

Recommend

ClojureScript & Golangでチャットアプリを作ってみた

エウレカ社内のhubotで生き残っている機能3選