scramble cadenza

技術ネタのガラクタ置き場

CircleCI から deploy させる話

イントロ

CircleCI からテストが通ったら capistrano 使って deploy させたい。
けど、対象サーバーは IP 制限がかかっている。CircleCI の IP なんてコロコロ変わるし、どうしたらいいんや... って話。

これができると、PR を github ボタンでマージしたら、自動(当然テストも通した後)で deploy されるので、非常に嬉しい。

方針

before deploy

  • awscli を使い security group の inbound に自分自身の ip を付与
    • CircleCI の内部から IP address を割り出して、コマンドラインで一時的にinboud に追加させる

deploy

  • コマンド(bundle exec cap deploy)を実行

after deploy

  • awscli を使い security group から ip を取り外す
    • before deploy の逆をやればいい

前準備

IAM user や security group の名前は適当に決めてます。
やること多いです。

  1. IAM user circle_ci を作成
    • ec2 に関する権限を全て付与(今回に限って言えば、全て付与しなくても動くはず)
    • 詳細は後述
  2. CircleCI 用の security group を作成する
    • 名前は circle_ci
    • 普段は inbound の欄は空
    • 一時的に CircleCI の IP が許可される用の security group
  3. deploy したいサーバーに security group circle_ci を attach する
  4. AWS環境変数を CircleCI の設定画面上で予め登録する
    • IAM user circle_ci の credential を登録
  5. deploy 対象のサーバーにログインするための秘密鍵を CircleCI の設定画面上で登録する

実装

CircleCIからCapistranoを利用してAWS(EC2)にデプロイする - Qiita に大体書いてある。

deploy するコマンドとして、deploy.sh みたいなシェルスクリプトを作って、それを deployment で実行させればいい。

参考記事とは少し違っているけど、自分が作成した deploy.sh は以下のようになった。

#!/bin/sh
set -ex

SECURITY_GROUP_NAME="circle_ci"
IP=`curl -f -s ifconfig.me`

trap "aws ec2 revoke-security-group-ingress --group-name ${SECURITY_GROUP_NAME} --protocol tcp --port 22 --cidr ${IP}/32" 0 1 2 3 15
aws ec2 authorize-security-group-ingress --group-name ${SECURITY_GROUP_NAME} --protocol tcp --port 22 --cidr ${IP}/32
bundle exec cap prod deploy

追記

コメントを受け、修正しました

Before

IP=`curl-s ifconfig.me`

After

IP=`curl -f -s ifconfig.me`

-f は man で見てみると、以下のような意味だそうです。
確かエラーになった時の事を考えると、入れておいたほうが良いかもですね。

       -f, --fail
              (HTTP) Fail silently (no output at all) on server errors. This is mostly done to better  enable  scripts  etc  to  better  deal  with  failed
              attempts. In normal cases when an HTTP server fails to deliver a document, it returns an HTML document stating so (which often also describes
              why and more). This flag will prevent curl from outputting that and return error 22.

              This method is not fail-safe and there are occasions where non-successful response codes will slip through, especially when authentication is
              involved (response codes 401 and 407).

ポイント

set -ex

-x は毎度おなじみ debug 用途。実行したコマンドを標準出力するオプション。
-e はコマンドに失敗したら、即時にスクリプトを終了する。一行づつ && で挟んでいるイメージ。
コマンドが 3 つ書かれているけど、どれか一つでも失敗したら、deploy せず即時失敗扱いで良いので、このようにしている。

trap コマンド

何かしら異常があった場合、変更を加えた Security Group を元に戻さないといけない。
でないと、ある IP から接続可能な状態になってしまい、セキュリティ上良くないから。

つまり、deploy に失敗しようが成功しようが、revoke-security-group-ingress は実行させたい、ということ。
そのために trap コマンドでシグナルを補足しています。

0 1 2 3 15 を書いておけば、大抵のシグナルには反応してコマンドが発動する。

肝は最終的な exit コードは trap コマンドの実行結果ではなく、trap 以前に実行した最後のコマンドであること。
つまり bundle exec cap prod deploy で exit コード 1 が返ってきた場合、trap が発動して、revoke され、exit コード 1 でシェルスクリプトが終了する。
(CircleCI では exit コード 1 ならば、build 失敗扱いになる)

circle.yml

circle.yml は以下のようになる。
pip install しないと awscli のバージョンが古く、コマンド実行に失敗する。

machine:
  timezone: Asia/Tokyo
  ruby:
    version: 2.1.3
dependencies:
  pre:
    - sudo pip install awscli
deployment:
  master:
    branch: master
    commands:
      - ./deploy.sh

まとめ

ここまで準備して、ようやく master へのマージ + テスト通ったら deploy が自動化されます。

とはいえ、これでも revoke-security-group-ingress に失敗する可能性があり、CircleCI の IP address が許可されたままになる可能性があります。が、そのリスクには目を瞑っています。
revoke-security-group-ingress に失敗したら build にも失敗するはずなので、すぐに調査すればいい話だから。
circle_ci の inbound を削除すれば元通りなので、復旧作業も楽勝。と思ってる。

なお、Circles EC2 IP addresses and AWS security group - CircleCI なんてのがあって、この情報をうまく使えば deploy できるんや... って思いがちだけど、これは孔明の罠なので要注意。

できてないこと

  • pip install awscli をキャッシュさせること
    • 頑張れば出来そうだけど、まだやってない

参照