こんにちは。照井@さっぽろです。
先日、JAWS Days 2017でお話させていただいたのですが、思った以上に反響が大きく、とても嬉しいです。私がAWSを本格的にやる気になったのも4年前のJAWS Daysでした。そう考えると感慨深いものがあります。
さて、ここ最近Rezeptというツールを作り、色々と機能拡張などしてきました(下記参照)が、その中で一つの目的であったSSM DocumentsのCI環境を作ることができたので、公開したいと思います。
- EC2 Systems Manager (SSM) Documentを管理するツール「rezept」を公開しました
- EC2 Systems ManagerのInventoryを検索して対象のインスタンスにRunCommandを実行したい!
まずはSSM DocumentsやEC2 Systems Managerについて知りたいという方はこちらをご確認ください。
SSM DocumentsをCIする上での課題
SSM DocumentsをCIするためにはいくつかの課題があります。
①RunCommandの実行単位はEC2インスタンス
つまり、Dockerコンテナを使うことができないということです。RunCommandを実行する場合には対象のEC2インスタンスを指定するInstanceIDまたはTagを指定する(RezeptならInventoryも可)必要があり、その単位での実行しかサポートされていません。そのため、Dockerコンテナを利用した一時的なCI環境は使用することが出来ません。
②RunCommandの実行は非同期
RunCommandの実行をリクエストした場合、受け付けられた時点でレスポンスは返され、実際のインスタンス上での実行はバックグラウンドで行われます。そのため、いつ実行され結果がどうであったかは後ほど確認するしかありません。
③RunCommandの実行結果は終了コードで決まる
RunCommandの実行結果はそのコマンド(スクリプト)の終了コードが0(正常)かそれ以外(失敗)で決まります。つまり、正常に終了したとしてもインスタンスが目的の状態になっているかどうかは別途確認が必要です。
解決するツール
今回は下記のツール群を使うことで上記3つの問題を解決します。
①kitchen-ec2
test-kitchenとそのプラグインであるkitchen-ec2を使います。基本的にはChefなどのプロビジョニングツールを実環境で実行してテストするためのツールですが、OSの組み合わせとテストのパターンがYAMLで簡潔に書け、結果のハンドリングも良い感じにやってくれて楽なのでこれを使います(CFnで環境を作ることも考えたのですが、テストパターンが増えることまで考えると手間が大きいので)
また、厳密にはプロビジョニングツールを使わずに手元でrezeptのコマンドを実行だけであるため、そういった用途のために昔私が作ったkitchen-provisioner-local_shellというプラグインも使用します。
②Rezept
Rezeptの run_command
というコマンドには --wait-results
というオプションが指定でき、これを付けると実行対象のインスタンス全ての結果が得られるまでSSMのAPIを定期的にポーリングして各々の結果を出力した上で、全て成功すれば終了コード0(成功)、一つでも失敗していれば終了コード1(失敗)を返して終了してくれます。まさにこのためにあるような機能ですね!(ステマ)
③Serverspec
公式サイトはこちらです。サーバの状態をテストするといえば定番ですね。test-kitchenを使うと自動でServerspecをインストールして実行してくれます。ただ、(test-kitchen的な)前提としてChef自体は要らないのにRuby実行環境としてChefをインストールする必要があったりするので冗長な感じはします。LinuxとWindows混在でOS認証情報のハンドリングが面倒なので一旦test-kitchenの標準に乗っていますが、ただでさえ時間がかかる状態であるため、CircleCI上からServerspecをSSH/WinRM越しに実行することで時間短縮したい気持ちがあります。そういった用途のための機能(Shell Verifier)もあるので。
構成や仕組み
上記を組み合わせた簡単な構成図がこちらです。
厳密にはRezeptはAWSのSSM APIに対してアクセスしてssm-agentがコマンドを実行するのですが、公式アイコン集にSystems Manager系のアイコンが存在しないので簡易的に書いています。AWSさん、アイコンをお願いします!(切実)
簡単に仕組みを解説すると、まずGitHubのPushやPull Requestに反応してCircleCIが動き出し、test-kitchen(+ kitchen-ec2)がそれぞれのテストに必要なEC2インスタンスをssm-agentやRuby(を同梱したChef)のインストールやWindowsならWinRMの設定などを行うUserDataを流して起動します。
その後、テスト用のSSM Documentを更新して、それを実行するRezeptコマンドを実行します。apply
コマンドに --prefix
というオプションを付けると更新対象のドキュメント名を絞ることができるので、これにテスト用のprefixを指定します。ちなみに対象のインスタンスでssm-agentが起動してSystems Managerにインスタンスが登録される前にRunCommandを実行しようとするとエラーになってしまいますが、これもRezeptの run_command
コマンドの --wait-entries
というオプションを付けるとRunCommandが実行可能となるまで待機させることができます。便利ですね!(ステマ)
そして、最後にServerspecのテストを実行します。ここのハンドリングはtest-kitchenが良い感じにやってくれます。
こういったテストなどを通り、更新対象のブランチがmasterである場合は rezept apply
を実行して本番用のDocumentsを更新する、という流れがこのような簡易な circle.yml
で定義できるという感じです。
---
machine:
ruby:
version: "2.2"
test:
override:
- bundle exec rake comment_diff
- REZEPT_ENV=test bundle exec rezept apply --prefix "Test-"
- KITCHEN_YAML=.kitchen.linux.yml bundle exec kitchen test
- KITCHEN_YAML=.kitchen.windows.yml bundle exec kitchen test
deployment:
master:
branch: master
commands:
- bundle exec rezept apply
また、レビュー用にPull Request時のみ rezept apply --dry-run
の実行結果をPull Requestのコメントに投稿するようなこともしています(↓参考)
設定方法
設定方法はCircleCIの設定方法や各ツールのインストールなどを1から説明するとものすごい分量となってしまうため、ポイントのみ解説します。まず、今回の実際に動くコードは以下になります。
https://github.com/marcy-terui/ssm-documents-ci
このコードを利用する上でいくつかポイントがあります。
- 環境変数を設定する
いくつかの環境依存項目を環境変数で指定するようにしています。具体的にはAWS_REGION
(リージョン名)、AWS_SSH_KEYID
(EC2インスタンスの起動時に指定するSSHキーのID)の指定が必要です。CircleCIのプロジェクト設定画面にあるBUILD SETTINGS → Environment Variablesから設定してください。また、Pull Request時のコメント投稿をする場合はGitHubのTokenを取得してGITHUB_ACCESS_TOKEN
に設定が必要です。 - SSHキーの配置
上記のAWS_SSH_KEYID
で指定したSSHの秘密鍵をCircleCIのプロジェクト設定画面にあるPERMISSIONS → SSH Permissionsからdefault
というホスト名で登録してください。test-kitchenに鍵の配置位置を指定する必要があり、ホスト名を決めると配置位置が固定できるためです。 - Security GroupでSSH(22)とWinRM(5985)を開けておく
今回のコードはDefault VPCのDefault Security Groupを使用する形なので、それ以外を使用する場合は併せて.kitchen.(windows|linux).yml
の修正が必要です。
最後に
普通に考えると少しオーバースペックな感じもしますが、私達のような沢山のお客様の環境をお預かりする立場で安定的かつスピードを損なわずにSystems Managerを使った運用体制を作っていくにあたってはこういった仕組みは必要であろうということで、今回は少し頑張ってCI環境を整備してみました。この形が最善とは思ってはいないので、今後もブラッシュアップしながら、よりスピードを損なわずに安定して運用できる環境を整備していきたいと思います。