LambdaでCodeDeployの自動デプロイ機能を実装する

2014.12.13

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

2014/12/16更新 : lambchopがアップデートされ正常に実行できるようになったので、実行結果を更新

ども、大瀧です。
本記事はAWS Lambda Advent Calendar 2014の13日目です。昨日は@shot6さんのLambdaでのここまでの利用パターンまとめでした。これで「Lambda入門」と題して半日セミナーができそうなくらい、素晴らしいまとめだと思います!

さて、最近加わったAWSのデプロイサービスCodeDeployは、EC2インスタンスにアプリケーションをシンプルにデプロイすることができる便利なサービスです。弊社ブログでもいくつかのエントリーで紹介しているので、CodeDeployについて知りたい方は、以下のリンクを参照ください。

CodeDeployでのアプリケーションのデプロイは、AWS Management ConsoleないしCLIから手動で実行する方法とAuto Scalingインスタンスの作成、起動時にデプロイする方法の2通りがあります。Auto Scalingで管理していないインスタンスや起動済みのインスタンスに対する自動デプロイは、CIツールなど外部サービスの力を借りるしかありません。
そこで、Lambdaを活用しなんとかしてCodeDeployの自動デプロイを実現しよう!というのが本エントリーの趣旨です。

構成

というわけで考えてみました。構成どーん。

lambda-sdsautodeploy

組み合わせているゆえ少し複雑に見えますが、Lambda、CodeDeployともに利用する機能はとても簡素です。以下に自動デプロイの流れを示します。

  • CodeDeployでは、アプリケーションコードおよびデプロイ時の実行スクリプトをリビジョンという単位でまとめて扱います。リビジョンのリポジトリは、現在S3とGitHubが選択できます。S3の場合、AWS CLIのaws deploy pushコマンドでローカルのリビジョンのファイルがZipでアーカイブされ、S3バケットにアップロードされます
  • そのS3バケットにイベント通知をあらかじめセットし、Lambda Functionを実行します
  • Lambda Functionでは、AWS SDK for JavascriptでCodeDeployのデプロイ実行(createDeployment)を行います
  • CodeDeployのデプロイ実行により、CodeDeployエージェントがリビジョンのアーカイブをインスタンスにコピー、展開しスクリプトを実行します。

CodeDeployの準備

今回は、あらかじめCodeDeployのチュートリアルに沿ってCloudFormationでEC2インスタンス3台およびIAMロールを準備し、サンプルのリビジョンで1回デプロイを実行しました(詳しい手順はこちらのブログエントリーを参照ください)。CodeDeployDemoという名前のインスタンスが今回のデプロイ対象になります。

lambda-sdsautodeploy01

EC2インスタンスの管理画面では、デプロイ対象の3台のEC2インスタンスが確認できます。

lambda-sdsautodeploy02

自動デプロイは開発・テスト用に適用すると思うので、実際のEC2インスタンスは1台で良いと思います。

Lambdaの構成

続いて、自動デプロイを実行するLambdaを構成します。まずは、IAM管理画面からLambdaの実行ロール(lambda_exec_role)を以下のポリシーで作成します。本来はcodedeploy:createDeployアクションのみの許可でいいと思うのですが、評価する時間がなかったため今回は大雑把な設定で妥協しました。

lambda_exec_role

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["logs:*"],
      "Resource": "arn:aws:logs:*:*:*"
    },
    {
      "Effect": "Allow",
      "Action": ["codedeploy:*"],
      "Resource": ["*"]
    }
  ]
}

Lambda Functionは本来、npmパッケージなどを含めたZipアーカイブを作成しDeployment Packageとしてアップロードするのですが、今回はLambchopというツールで代用します。LambchopはZip化とアップロードを単一コマンドで一発実行できるツールで、RubyGemsで提供されているのでgemコマンドであらかじめインストールしておきます。

$ gem install lambchop --no-ri --no-rdoc

lambchopコマンドを引数なしで実行するとLambchopのファイルのヘッダが表示されるので、ヘッダの後ろにコードを追加してLambda Functionを記述していきます。ヘッダ部分は、function_nameはデフォルトのファイル名から引用するため削除、handler行はファイル名.handlerrole行を作成したLambdaの実行ロールのARNに変更します。

sdsautodeploy.js

#!/usr/bin/env lambchop
/*
runtime: nodejs     # default: nodejs
mode: event         # default: event
description: ''     # default: (empty)
timeout: 3          # default: 3
memory_size: 128    # default: 128
role: arn:aws:iam::XXXXXXXXXXXX:role/lambda_exec_role
handler: sdsautodeploy.handler
# Handler module name is filename.
# `handler:` is `index.handler` when filename is `index.js`
*/
var AWS = require('aws-sdk');

console.log('Loading event');

exports.handler = function(event, context) {
  var codedeploy = new AWS.CodeDeploy({ region: 'us-west-2' });
  codedeploy.createDeployment({
    applicationName:      'DemoApplication',
    deploymentConfigName: 'CodeDeployDefault.AllAtOnce',
    deploymentGroupName:  'DemoFleet',
    description:          'Lambda auto deploy',
    revision: {
      revisionType: 'S3',
      s3Location: {
        bucket:  event.Records[0].s3.bucket.name,
        key:     event.Records[0].s3.object.key,
        eTag:    event.Records[0].s3.object.eTag,
        version: event.Records[0].s3.object.versionId,
        bundleType: 'zip'
      }
    }
  }, function (err, data) {
    if (err) console.log(err, err.stack); // an error occurred
    else     console.log(data);           // successful response
  });
};

コード自体は、S3イベントを捕捉するドキュメントのサンプルの一部を抜粋し、AWS SDK for JavascriptでCodeDeployのcreateDeployment APIのコールを追加したものです。CodeDeployのデプロイ実行にはリビジョンを指定するためにS3オブジェクトのKeyの他にETagとVersion IDが必要なので、S3のドキュメントを参考にイベントオブジェクトを参照するようにしました。至ってシンプルですね。

あとは、AWS SDKをnpm installでカレントディレトリ以下にインストールしておきます。

$ npm install aws-sdk
npm http GET https://registry.npmjs.org/aws-sdk
npm http 304 https://registry.npmjs.org/aws-sdk
npm http GET https://registry.npmjs.org/xml2js/0.2.6
npm http GET https://registry.npmjs.org/xmlbuilder/0.4.2
npm http 304 https://registry.npmjs.org/xmlbuilder/0.4.2
npm http 304 https://registry.npmjs.org/xml2js/0.2.6
npm http GET https://registry.npmjs.org/sax/0.4.2
npm http 304 https://registry.npmjs.org/sax/0.4.2
aws-sdk@2.1.0 node_modules/aws-sdk
├── xmlbuilder@0.4.2
└── xml2js@0.2.6 (sax@0.4.2)
$ ls
node_modules/     sdsautodeploy.js*
$

Zipアーカイブに含めるファイルが揃ったら、Lambchopファイルに実行権限を与えてそのまま実行します。

$ chmod +x sdsautodeploy.js
$ ./sdsautodeploy.js
Function was uploaded:
{
  "function_name": "sdsautodeploy",
  "function_arn": "arn:aws:lambda:us-west-2:XXXXXXXXXXXX:function:sdsautodeploy",
  "configuration_id": "bec93202-9a85-4677-86d0-0653ec9e64d7",
  "runtime": "nodejs",
  "role": "arn:aws:iam::XXXXXXXXXXXX:role/lambda_exec_role",
  "handler": "sdsautodeploy.handler",
  "mode": "event",
  "description": "",
  "timeout": 3,
  "memory_size": 128,
  "last_modified": "2014-12-16 20:40:26 +0900"
}
Node modules:
[
  "aws-sdk"
]
(待ち状態)

Lambda Functionのログを表示する待ち状態になるので、そのままにしておきます。Management ConsoleのLambda Function一覧を確認すると、アップロードしたLambda Function sdsautodeployが登録されているのでよしとします。

lambda-sdsautodeploy03

S3バケットの作成とイベント通知設定

続いてリビジョンを格納するS3バケットを作成し、オブジェクトの作成時にLambda Functionをキックするイベント通知を登録します。この辺りは、Lambdaのドキュメントの例と特に変わりません。Lambda Functionの詳細表示から[Create event source]ボタンをクリックします。

lambda-sdsautodeploy04

作成したバケットを選択し、Lambdaの発火ロールを割り当てます。デフォルトのポリシーで構いません。

lambda-sdsautodeploy05

これで準備OKです。

CodeDeployリビジョンのアップロード

それでは、CodeDeployのリビジョンをS3にアップロードしてみます。S3へのアップロードは通常のS3の操作ではなく、aws deploy pushコマンドを実行します。

$ cd ../SampleApp_Linux
$ aws deploy push \
  --region us-west-2 \
  --application-name DemoApplication \
  --description '' \
  --ignore-hidden-files \
  --s3-location s3://sdsautodeploy/SampleApp_Linux.zip \
  --source .
To deploy with this revision, run:
aws deploy create-deployment --application-name DemoApplication --s3-location bucket=sdsautodeploy,key=SampleApp_Linux.zip,bundleType=zip,eTag="abc470b8277eecd3bdcaf470c729ef1a" --deployment-group-name <deployment-group-name> --deployment-config-name <deployment-config-name> --description <description>
$

アップロードされたリビジョンからS3のイベントが発火し、Lambda Functionが実行されるはずです。CodeDeployのDeployments画面を表示すると、確かに自動でDeploymentが作成されデプロイが実行されていることが確認できます!

lambda-sdsautodeploy06

Lambdaの実行ログは、CloudWatch Logsで確認できます。CloudWatch Logsの画面で確認することもできますが、Stream名を確認しておくとAWS CLIのaws logs get-log-eventsコマンドでもログを表示できます。

$ aws logs get-log-events \
  --log-group-name /aws/lambda/sdsautodeploy \
  --log-stream-name XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX \
  --query events[].message
2014-12-12T15:39:13.070Z	9uu5ipphn5nqpder	Loading event
	START RequestId: 036a75e5-8215-11e4-ad76-bd01a46d05ef
	2014-12-12T15:39:14.470Z	036a75e5-8215-11e4-ad76-bd01a46d05ef	{ deploymentId: 'd-N7BI74I06' }
	Process exited before completing request

	END RequestId: 036a75e5-8215-11e4-ad76-bd01a46d05ef
	REPORT RequestId: 036a75e5-8215-11e4-ad76-bd01a46d05ef	Duration: 1530.67 ms	Billed Duration: 1600 ms 	Memory Size: 128 MB	Max Memory Used: 33 MB
$

CodeDeployのDeploymentIDが確認できますね!

まとめ

Lambdaのユースケースとして、CodeDeployの自動デプロイ機能を実装してみました。CI的に利用するのであれば、リビジョンのスクリプトにテスト実行などを含ませてもいいかもしれないですね。Lambdaの活用方法をみんなで考えていきましょう!