Jenkinsの自動デプロイにAnsibleを使ってみた

f:id:watass:20150211233737p:plain

ありがたいことに、以前のJenkins自動デプロイ記事がそれなりに多くの反応をいただきまして、冷静に見直してみたのですが、ちょっとデプロイ処理が雑だなと。
最近ではデプロイをやるにも、Capistranoやfabricなどのツールがあり、多様化するデプロイ要件に柔軟に対応できるような工夫が施されています。これらのツールを用いることで、シェルスクリプトで記述されたぐちゃぐちゃなオレオレデプロイを回避できたり、デプロイ失敗時のロールバックなどがしやすくなるメリットがあります。

現在開発しているアプリでは、git pullだけでデプロイできるようにしてあるので、前回の記事のようにJenkinsのジョブにシェルスクリプト直書きでもいいのですが、

  • デプロイ先の変更やスケールアウトに対応しにくい
  • 認証鍵の場所をコマンドで直書きするのが、個人的にちょっと抵抗ある
  • 書き方がスマートじゃない ←重要

という問題があり、後々のことも考えてデプロイツールの導入を決心しました。
今回はAnsibleを使い、GitBucketで管理されているリポジトリへのプッシュをトリガにして、Jenkinsで自動デプロイするまでの流れをまとめてみます。なお、前回のシェルスクリプトによる自動デプロイは以下の記事です。

Ansibleとは

Ansibleとは、ChefやPuppetと並ぶ構成管理ツールです。今話題の"Infrastructure as code"と絡めて話題にあがることが多く、仮想サーバのセッティングなどを自動的に、かつ再利用可能に実現できることが最大のメリットです。

最近はChefが主流になっている印象がありますが、やれることが幅広い分、学習コストが高いと言われるため、より簡素なAnsibleも根強い人気があります。個人的にはサーバにPythonさえ入っていれば、どのサーバにも利用できるのが嬉しいです。
Ansibleは構成管理ツールですので、デプロイツールとは目的が微妙に異なるのですが、上記に挙げているシェルスクリプトによるデプロイの問題を解決できるだけのスペックを秘めていることと、将来的にサーバの構成管理をやるとき、Ansibleの経験があると役立ちそう(結局ChefやOpsWorkを使うことになったとしても)だと思ったことから、今回の採用を決定しました。

サーバ構成

f:id:watass:20150201154035p:plain

構成自体は前記事と変わらず。差分はJenkinsがAnsibleを使ってデプロイするくらいです。
ちなみに、SecurityGroupに注目して見ると、本番環境はELBからのHTTPとCIサーバからのSSHのみ許可すればよく、CIサーバは22,80,8080,8090番ポートを開放しなくてはいけないものの、すべてのポートはIP制限を掛けることができます。せいぜいGitBucketから本番環境へのプルを実現するために、SecutiryGroupを指定して8090番ポートを開けるだけなので、セキュアな環境が実現できているといえるでしょう。

使用しているバージョン

Ansibleの実行にはPythonが必要ですが、Amazon Linux AMI 2014.09には既にAnsibleを動作させるのに十分最新なPythonがインストールされているため、そのまま利用します。各サーバで用いるバージョンは以下の通りです。

CIサーバ

本番サーバ

Ansibleのインストール

さっそくAnsibleをインストールします。CIサーバにログインしてpipでインストール。

# easy_install pip
# pip install ansible

無事インストールできたか、バージョン確認をしてみましょう。

$ ansible --version
/usr/lib64/python2.6/site-packages/Crypto/Util/number.py:57: PowmInsecureWarning: Not using mpz_powm_sec.  You should rebuild using libgmp >= 5 to avoid timing attack vulnerability.
  _warn("Not using mpz_powm_sec.  You should rebuild using libgmp >= 5 to avoid timing attack vulnerability.", PowmInsecureWarning)
ansible 1.8.2
  configured module search path = None

む、Warningが出ていますね。
ちょっとググってみましたが、Pythonの内部パッケージが要求するライブラリのバージョンが古いことを指摘しているようです。動作自体は問題ないことと、現状Amazonから提供されているリポジトリには該当の最新バージョンがないことから、とりあえずこのまま進めていくことにしましょう。
参考:Using Ansible on AWS – EC2インスタンスを作成する | Developers.IO

configurationファイルの作成

まず、Ansibleの基本設定を行うために、configurationファイルを作成します。
configurationファイルは複数用意でき、カレントのconfigurationファイルが優先的に適用されるなどのルールがあるのですが、とりあえず全般的に適用される/etc/ansible/ansible.cfgを以下のように作成します。

[defaults]
host_key_checking=False
inventory=/etc/ansible/hosts
private_key_file=/var/lib/jenkins/.ssh/mykey.pem

[defaults]でデフォルトの設定を記述できます。項目は以下の通りです。

host_key_checking

接続先の確認有無の設定です。Falseにすることでknown_hostsに追加されていない接続先への接続時にもエラーが発生しなくなりますが、セキュリティ的にはバットプラクティスです。今回は同じネットワーク内での接続に留めるため、Falseにすることを許しています。

inventory

後に作成するinventoryファイルのパスです。基本的には/etc/ansible/hostsを指定します。

private_key_file

接続時に使用する秘密鍵のパスです。今回はJenkinsのジョブとしてAnsibleを実行するため、秘密鍵はJenkinsの.ssh以下に配置しておきます。


configurationファイルでは、他項目の設定も可能ですが、今回は以上にとどめます。これらの項目はansibleコマンド実行時のオプションとして記述することもできますが、設定ファイルに落としこむことでコマンドが簡素になるのと、使い回しができるというメリットがあります。

inventoryファイルの作成

次に、Ansibleで接続する接続先を設定するinventoryファイルを作成します。
/etc/ansible/hostsを以下のように作成します。

[develop-server]
192.168.0.3

[developer-server]はグループ名の明示です。接続先をまとめてグループとして定義できます。今回は開発環境(兼本番環境)にssh接続するため、develop-serverと名前をつけました。
ここでポイントなのは接続先のインスタンスをプライベートIPで指定していることです。パブリックIPは再起動する度に変更されますが、プライベートIPは変更されません。加えて、プライベートIPでの通信は同一AZ内ならば通信料が無料なので、明示的にプライベートIPを指定したほうがお得です。

Playbookの作成

さて、これで設定関係は完了ですので、後は接続先に対する操作を記述するPlaybookを作成します。/var/lib/jenkins/git-deploy.ymlを以下のように作成します。

- hosts: develop-server
  user: ec2-user
  vars:
    app_dir: /home/ec2-user/apps
    branch: master
  tasks:
    - git: repo=http://ci.example.com:8090/gitbucket/git/watass/myapp.git
           dest={{ app_dir }}
           version={{ branch }}
           update=yes

Ansibleで使用するPlaybookはYAML形式で記述できます。上記はgitでデプロイするためのPlaybookで、詳細な項目は以下の通りです。

hosts

接続先のグループを指定します。inventoryファイルのdevelop-serverグループを指定します。

user

接続時に使用するユーザ名です。デフォルトのec2-userを使用します。

vars

Playbook内で使用する変数の定義です。Playbookが肥大化しても管理しやすくなります。

tasks

接続先に行う操作の詳細です。今回はgitによるデプロイのため、gitモジュールを用います。


このようにYAML形式でPlaybookを記述することで、シェルスクリプトのようなオレオレ記述を排除でき、誰が見ても明確な記述ができるようになります。

Ansibleによるデプロイテスト

さて、それでは作成したPlaybookを使用してコマンドベースでデプロイのテストをやってみましょう。設定情報はconfigurationファイルにすべて記載してありますので、ansible-playbookコマンドでPlaybookを指定するだけでOKです。

$ ansible-playbook /var/lib/jenkins/git-deploy.yml
/usr/lib64/python2.6/site-packages/Crypto/Util/number.py:57: PowmInsecureWarning: Not using mpz_powm_sec.  You should rebuild using libgmp >= 5 to avoid timing attack vulnerability.
  _warn("Not using mpz_powm_sec.  You should rebuild using libgmp >= 5 to avoid timing attack vulnerability.", PowmInsecureWarning)

PLAY [develop-server] ********************************************************* 

GATHERING FACTS *************************************************************** 
ok: [192.168.0.3]

TASK: [git repo=http://ci.example.com:8090/gitbucket/git/watass/myapp.git dest={{ app_dir }} version={{ branch }} update=yes] *** 
changed: [192.168.0.3]

PLAY RECAP ******************************************************************** 
192.168.0.3                : ok=2    changed=1    unreachable=0    failed=0   

okが2つ、changedが1つ発生しました。SSH接続とタスクの実行が成功したことと、タスクによって1つの変更が発生したことを示しています。実際にブラウザからアプリケーションにアクセスしてみると、最新のバージョンに更新されていることがわかりますね。

Jenkinsのジョブ設定

Ansibleのコマンドベースでデプロイができることが確認できましたので、後はJenkinsのジョブとして実行するだけです。フリースタイルプロジェクトのビルドを作成し、ビルド処理としてシェルの実行でansible-playbookコマンドを記述します。URLのリクエストベースでジョブを実行する予定なので、申し訳程度にトークンを指定しておくとよいでしょう。

f:id:watass:20150212023427p:plain

ジョブを作成したら、手動でジョブを実行してデプロイできるか確認しておきましょう。

GitBucketのWebhook設定

最後に、リポジトリの更新をトリガにJenkinsのジョブを走らせるために、GitBucketのWebhookを設定します。リポジトリのページからSetting→Service HooksでJenkinsのジョブURLを追加しましょう。

f:id:watass:20150212023711p:plain

今回は同一インスタンス上にJenkinsとGitBucketが乗っているので、ジョブURLにlocalhostが使用できます。Webhookはジョブを外部から叩けるようにしないといけないことがセキュリティ的にネックになりがちですが、GitBucketのようにローカルで展開できると、localhost指定で外に穴を開けなくて済むのでGoodです。
ジョブ名を"gitbucket-auto-deploy"に指定すると、以下のようなURLになります。トークンはジョブ作成時に設定したものです。

http://localhost:8080/job/gitbucket-auto-deploy/build?token=[トークン]&cause=git-push

ここで、URLからジョブが実行できるか確認しておきましょう。Jenkinsのグローバルセキュリティを厳しく設定していると、ビルドできないことがあります。匿名ユーザにビルド権限を与えるだけでなく、ジョブの閲覧権限なども設定しておかないとダメなようです。ログアウト時に、URLから実行できるか確認しておくといいでしょう。

匿名ユーザがログインなしでジョブを実行できることに不安はありますが、インスタンス自体にIP制限のSecurityGroopをかけていますし、ジョブの編集や削除などの権限は一切ないので、セキュリティ的にそこまでまずい、というわけではないと思います。

自動デプロイを試してみる

これで準備完了です。早速ローカルからリポジトリにプッシュして、自動デプロイできるか試してみましょう。Jenkinsのジョブの実行履歴を見てみると・・・

f:id:watass:20150212024031p:plain

お、成功したようですね。
コンソール出力を見てみましょう。

Started by remote host 127.0.0.1 with note: git-push
Building in workspace /var/lib/jenkins/workspace/gitbucket-auto-deploy
[gitbucket-auto-deploy] $ /bin/sh -xe /tmp/hudson5176095097434713106.sh
+ ansible-playbook /var/lib/jenkins/git-deploy.yml
/usr/lib64/python2.6/site-packages/Crypto/Util/number.py:57: PowmInsecureWarning: Not using mpz_powm_sec.  You should rebuild using libgmp >= 5 to avoid timing attack vulnerability.
  _warn("Not using mpz_powm_sec.  You should rebuild using libgmp >= 5 to avoid timing attack vulnerability.", PowmInsecureWarning)

PLAY [develop-server] ********************************************************* 

GATHERING FACTS *************************************************************** 
ok: [192.168.0.3]

TASK: [git repo=http://ci.example.com:8090/gitbucket/git/watass/myapp.git dest={{ app_dir }} version={{ branch }} update=yes] *** 
changed: [192.168.0.3]

PLAY RECAP ******************************************************************** 
192.168.0.3                : ok=2    changed=1    unreachable=0    failed=0   

Finished: SUCCESS

成功していますね。ブラウザから確認しても最新版になっていると思います。
以上でAnsibleを使用したデプロイ自動化の完了です。

まとめ

  • Ansibleはconfiguration、inventory、Playbookで設定可能
  • AWS上の接続先指定は、プライベートIPがおすすめ
  • JenkinsとGitBucketを同一インスタンスに置けば、Webhookにlocalhostを指定できる

おまけ:SourceTreeの認証を自動化する

さて、自動デプロイできるようになったので、どんどんコード書いてプッシュしましょう。

f:id:watass:20150212024420p:plain


(#^ω^)ビキビキ


GitHubのアカウントがSourceTree登録することで認証自動化ができたので、同じ要領で、どっかで設定できるんじゃないかと考えたのですが、残念ながらダメでした。とはいえ、これはあまりにも面倒臭い。自動デプロイの恩恵が薄まってしまいます。

Mac OS XではKeychainを使うことでこの問題が解決できます。git本体にMacのkeychainを使用する機能がありますので、ターミナルから以下のコマンドを叩いてみましょう。

git credential-osxkeychain
git config --global credential.helper osxkeychain

これで一度認証を行うことで、認証をスキップできます。素晴らしい!助かりました。


さて、今回はAnsibleで一元管理できるようになりましたが、Auto Scalingのように新しくインスタンスが作られるようなパターンには対応できていません。
そのようなケースでは、きっとCodeDeployのようなAWSから提供されているデプロイサービスを利用するべきだと思いますが、AWSに依存しないプラクティスを理解することもきっと意義が深いと思います。でも、CodeDeployの東京リージョン開放は早くお願いします(切実)

参考

【AWS】ansibleでEC2インスタンスにプロビジョニングする【ansible】 - くどはむと猫の窓
# AWS上でのAnsible使用イメージがつかめました。ありがとうございます。
SourceTree + Git + Ansible + Jenkns で 継続的デリバリお試し環境をつくってみた(その2) | My diary for @halchiyo
# 構成が似ているため、全体像のイメージを考えるときに大変役立ちました。
はじめてAnsibleを使う人が知っておきたい7つのモジュール | 株式会社インフィニットループ技術ブログ
# Ansibleを利用するときによく使うモジュールが紹介されていて非常に有益です。
Ansibleのドキュメントを読んでみたメモ - Qiita
# Ansibleで色々できることが紹介されています。公式ドキュメント読みにくいですよね。
Ansible Documentation — Ansible Documentation
# やっぱり最後は公式ドキュメントですね。よく使うモジュール系は読んでおくといいと思います。