LambCIを利用してPull RequestベースのCloudFormation環境を構築する

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんにちは、中山です。

前回の記事に続いてLambCIの2回目のエントリです。今回はCloudFormation(以下CFn)をPull Request(以下PR)ベースで管理し、LambCIからデプロイする方法をご紹介します。

CFnの管理

みなさんはCFnをどのように管理されているでしょうか。インフラ担当の方が1名の場合は、その人にスタックの作成/アップデートをお願いすればいいかもしれません。しかし、チームでCFnを管理する場合はやはり使い慣れたPRベースの開発フローに乗せられると便利だと思います。この開発フローを採用したい場合、今回ご紹介するLambCIを利用した実装は1つの選択肢になるかもしれません。

もちろん、Service Catalogを利用したCFnの管理もとても便利だと思います。細かく権限の設定が可能なので、よりきめ細かな統制管理が可能です。弊社のブログにも多くのエントリがあるので参照いただけると便利かと思います。

あるいは、最近アップデートのあったCodepipelineを利用したデプロイを利用する手もあります。

それぞれpros/consがありますが、GitHubを利用した一般的なデプロイフローと合わせられるという点では今回ご紹介する手法が合うかもしれません。

利用するツール

CFnの作成にはみんな大好きawscliを利用します。LambCIはデフォルトでpipがバンドルされているのでインストールは簡単です。 pip コマンドの --user オプションを利用することでローカル環境にawscliをインストールできます。ただし、awscliのコードベースはそれなりのサイズなため、現時点(2016年11月14日)のLambdaの /tmp書き込める制限である512MBがネックになる場合があるかもしれません。awscliをLambdaにインストールしたところ50MB近くありました。その場合はNode.jsでawscliライクにCFnを実行可能なocelotconsulting/aws-sdk-cliの利用を検討してみてください。もちろん各種言語用AWS SDKを利用するのもOKです。

LambCIの各種操作にはlambci/cliを利用します。LambCIはDynamoDBのテーブルに各種情報を保存し、Lambdaからその情報を読み込み/書き込みしています。利用しているテーブルは以下の2つです(いずれも現状北部バージニアに作成されます)。

  • <スタック名>-config
    • GitHubトークンなどの情報を保持
  • <スタック名>-builds
    • ビルドの情報(コミットハッシュ値など)を保持

lambci/cliはこれらテーブル情報の参照や設定をローカルから実行できるcliツールです。コンフィグ用テーブルに格納された secretEnv というアイテムの属性をLambda上から環境変数として参照可能です。また、lambci/cliを利用して参照することも可能です。

# 個々の属性を参照
$ lambci config secretEnv.<属性名>
# 全ての属性を参照
$ lambci config secretEnv

さらに、 rebuild サブコマンドによりローカルから特定のビルドを再実行可能なので、デバッグ時などに有用です。例えば1番目のビルドを再実行したい場合は以下のように実行します( --stack オプションはデフォルトで lambci )。

$ lambci rebuild --stack <スタック名> gh/<GitHubのユーザ名>/<リポジトリ名> 1

その他のコマンドやインストール方法はREADMEを参照してください。

使い方

今回もサンプルとなるリポジトリを作成しておきました。ご自由にお使いください。

前回のエントリと重複するので以下の作業は完了したものとして説明を進めます。

  • GitHub tokenの作成
  • Slack tokenの作成
  • LambCIのセットアップ

LambCIの実行方法も基本的に同じです。リリース用ブランチにpushすれば bin 以下にある各種シェルスクリプトがLambda上で実行され、スタックの作成/テストが開始されます。作成するAWSリソースも前回とほぼ同じです。

以下では今回の場合特有の設定を解説します。

CFn実行用権限の作成

Lambdaにひも付けたIAM RoleにAdministrator権限を付与しても良いのですが、最小権限で実行させたいので以下のエントリを参考にCFn実行権限の設定を実施します。

まず、Lambdaにひも付けたIAM Roleに以下の権限を付与してください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "cloudformation:*",
                "iam:ListRoles",
                "iam:PassRole"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

続いて、このLambdaがassume roleするCFn用のIAM Roleを作成します。具体的な手順は上記エントリを参照してください。以下のようなIAM Roleを作成すればOKです。

lambci-cfn-1

secretEnv の設定

あまりリポジトリへ書き込みたくない情報は secretEnv に保存しておきます。今回の例であればキーペア名とスタック名、そしてassume roleするIAM RoleのARNです。lambci/cliを利用して以下のようにDynamoDBに保存しておきます。

# キーペアの設定
$ lambci config secretEnv.KEY_PAIR_NAME <キーペア名>
# スタック名の設定
$ lambci config secretEnv.STACK_NAME <スタック名>
# IAM Roleの設定
$ lambci config secretEnv.CFN_ROLE_ARN <ロールのARN>

最終的に以下のような設定になっていればOKです。

$ lambci config secretEnv
{ CFN_ROLE_ARN: '<ロールのARN>',
  GITHUB_TOKEN: '****************************************',
  KEY_PAIR_NAME: '<キーペア名>',
  SLACK_TOKEN: '******************************************',
  STACK_NAME: '<スタック名>' }

スタックの作成

注意点としてスタックはLambCIで実行する前に一度作成しておいてください。 bin 以下のスクリプトはスタックがすでに作成済みであることを前提にしています。ない場合の処理を書くのがちょっと面倒だったので省略しちゃいました。。。

解説

  • .lambci.json

今回は以下のようにしてみました。

{
  "cmd": "./bin/install.sh && ./bin/deploy.sh",
  "build": "false",
  "branches": {
    "master": "true",
    "release/cfn": "true"
  },
  "env": {
    "AWS_DEFAULT_REGION": "ap-northeast-1"
  },
  "pullRequests": {
    "fromSelfPublicRepo": "true",
    "fromSelfPrivateRepo": "true",
    "fromForkPublicRepo": {
      "build": "true",
      "inheritSecrets": "true",
      "allowConfigOverrides": [
        "cmd",
        "env"
      ]
    },
    "fromForkPrivateRepo": "false"
  },
  "s3PublicSecretNames": "false"
}

LambCIはlambci/utils/config.jsでデフォルトの挙動を指定しています。それを .lambci.json などの設定ファイルで上書きすることが可能です。

env ではLambda上で参照可能な環境変数の設定をしています。awscliでリージョンの指定を省くため AWS_DEFAULT_REGION の設定をしています。

pullRequests でPR時の挙動を設定しています。詳細はドキュメントに詳しいですが、デフォルトからの変更点はパブリックリポジトリがforkされた場合の inheritSecrets の設定です。これは、 secretEnv の設定をPRで利用するかを指定しています。今回 true なので利用する設定にしています。 bin/deploy.shsecretEnv で設定された環境変数を参照するために必要だからです。この設定によりPRに対しても正常にテストが実行可能です。

こちらのドキュメントに書かれていますが、この値を true にする場合、LambCIが生成するS3のビルド結果が記載されたhtmlファイルへのURLを漏らさないように注意してください。AWSクレデンシャルなどの各種情報が漏れてしまう可能性があります。このファイルはデフォルトで誰に対しても参照可能になっています。もし、この点を懸念される場合は s3PublicSecretNames の値を false にしてください。この場合、該当のhtmlファイルはプライベートになります。と、ドキュメントに書かれているのですが上手く動いてないような。。。あるいは、ビルド結果を保存するS3バケットそのものを削除する方式もあるようです。この場合、マネジメントコンソールから結果を確認することになります。

  • bin/install.sh

pip コマンドでawscliをインストールしています。冒頭でも軽くお伝えしたように、LambCIは /tmp にホームディレクトリを設置してくれているため、Lambdaからデータを書き込むことが可能です。

#!/usr/bin/env bash

pip install --user awscli --quiet
aws --version
  • bin/deploy.sh
#!/usr/bin/env bash

aws cloudformation validate-template \
  --template-body file://cfn.yml
[[ $? == 0 ]] || exit 1

aws cloudformation wait stack-exists \
  --stack-name "$STACK_NAME"
[[ $? == 0 ]] || exit 1

change_set_id="$(aws cloudformation create-change-set \
  --stack-name "$STACK_NAME" \
  --change-set-name "change-set-${LAMBCI_BUILD_NUM}" \
  --template-body file://cfn.yml \
  --capabilities "CAPABILITY_IAM" \
  --role-arn "$CFN_ROLE_ARN" \
  --parameters ParameterKey="KeyPair",ParameterValue="${KEY_PAIR_NAME}" \
  --query "Id" \
  --output "text")"

execution_status=
count=1
while true; do
  execution_status="$(aws cloudformation describe-change-set \
    --change-set-name "$change_set_id" \
    --query '[ExecutionStatus,StatusReason]' \
    --output "json")"

  # if not changes, then break immediately
  [[ "$execution_status" =~ "The submitted information didn't contain changes" ]] && break
  # if returned strings contain 'AVAILABLE', then pass test
  [[ "$execution_status" =~ AVAILABLE ]] && break

  # retry
  echo "Wainting for AVAILABLE... $count retry"
  # currently no waiters for change set
  sleep 10
  count=$(( $count + 1 ))
done

if [[ "$LAMBCI_BRANCH" == "release/cfn" && -z "$LAMBCI_PULL_REQUEST" ]]; then

  status=
  count=1
  while true; do
    status="$(aws cloudformation describe-change-set \
      --change-set-name "$change_set_id" \
      --query 'Status' \
      --output "text")"

    [[ "$status" == "CREATE_COMPLETE" ]] && break

    echo "Wainting for CREATE_COMPLETE... $count retry"
    sleep 10
    count=$(( $count + 1 ))
  done

  aws cloudformation execute-change-set \
    --stack-name "$STACK_NAME" \
    --change-set-name "$change_set_id"
fi

少し長いので順を追って説明します。

  • テンプレートのバリデーション

validate-template でテンプレートのバリデーションを実施しています。

  • スタックの存在確認

waitersを利用してスタックの存在確認をしています。waitersそのものについては以下のエントリを参照してください。

Change setを利用してスタックに対する変更が正常に実施されるかを確認しています。 ExecutionStatusStatusReason をそれぞれパースして、 ExecutionStatusAVAILABLE となった場合にChange setの作成が完了したと判断しています。コミットの内容によってはスタックの変更を伴わない場合もあるので、Change setが作成できない場合があります。その場合のために、 StatusReasonThe submitted information... となったらテストをパスしたものとして扱っています。

  • Change setの実行

最後に、ブランチが release/cfn かつPRでない場合は execute-change-set でChange setを実行し、デプロイを実施しています。作成したChange setのステータスが CREATE_COMPLETE となるまで while で回して待機しています。。。

まとめ

いかがだったでしょうか。

最後に結論となりますが、正直あまりCFnは一般的なGitHubを利用したPRベースの開発フローには合わない気がします。。。awscliのパース処理がシンドいです。私が作成したスクリプトはとりあえず動いたレベルでお世辞にもできの良いものとはいえません。個人的には --query オプションのパース処理をシェルスクリプトでし始めたら、代替手段がないか検討した方が良い気がします。そういえば以前からあまりCFnのCI環境の話は少なかった印象でした。こういうことなのかぁ。。。とはいえ、「こういう使い方もある」と頭の片隅にでも置いていただければと思います。

本エントリがみなさんの参考になれば幸いです(投げやり)。