ApexとTerraformでCloudWatch Events Schedule x Lambda x SNS を設定する

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

はじめに

こんにちは、中山です。

以前ApexとTerraformを利用してCloudWatch Events経由で起動するLambda関数のデプロイ方法を書きました。

CloudWatch Eventsには特定Event経由でのLambda関数の起動以外に、定期的にLambda関数を起動する機能があります。今回のエントリではこの設定方法をご紹介します。また、Apexの知見が多少深まったのでその辺りも触れたいと思います。

利用するApexとTerraformのバージョンは以下の通りです。

Tool Version
Apex v0.10.2
Terraform v0.16.0

利用するLambda関数

今回利用するLambda関数はijin/check_lambda_capacityです。詳細は作者の方のブログに詳しいですが、アップロードしたLambda関数のデプロイパッケージの合計サイズを集計することが可能です。Lambda関数にはRegionあたりのアップロード可能なデプロイパッケージに対して制限があります。この制限を超えてデータをアップロードすることはできません。

作者の方がこのLambda関数を作成した際には1.5GBだったようですが、2016/06/20現在75GBまで増加しています。そのため、わざわざ容量を監視する必要性は薄れていますが、そこは本エントリの主眼ではないので目をつむり、ありがたく利用させていただきます。

コード

実際のコードをGitHubに上げておきました。リポジトリをcloneすれば利用可能です。

リポジトリの構造

apex-check-lambda-capacity/
├── .apexignore
├── .gitignore
├── .gitmodules
├── README.md
├── functions
│   └── check_lambda_capacity
│       ├── <snip>
├── infrastructure
│   ├── dev
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   └── modules
│       ├── cloudwatch
│       │   ├── cloudwatch.tf
│       │   └── variables.tf
│       └── iam
│           ├── iam.tf
│           └── outputs.tf
└── project.json

コードの解説

主要なファイルの内容について以下に解説します。

project.json

{
  "name": "apex-check-lambda-capacity",
  "description": "apex check lambda capacity",
  "nameTemplate": "{{.Function.Name}}",
  "handler": "lambda_function.lambda_handler",
  "memory": 128,
  "timeout": 5,
  "runtime": "python",
  "defaultEnvironment": "dev",
  "hooks": {
    "build": "[[ -d placebo ]] || pip install -r requirements.txt -t ./"
  }
}

以前のエントリとほぼ同じ内容ですが、 hooks が加わっています。ApexにはLifeCycle hookという機能があり、特定のEvent時にShellコマンドを実行可能です。現在サポートされているhookは以下の3つです。

hook 機能
build run before a function zip is built (use this to compile binaries or transform source)
deploy run before a function is deployed (useful for testing, linting)
clean run after a function is deployed (useful for cleaning up build artifacts)

今回のLambda関数は標準では含まれていない外部パッケージを利用しているので、デプロイパッケージをzip化する前に pip コマンドでそれを取得する必要があります。

infrastructure/dev/main.tf

module "iam" {
  source = "../modules/iam"
}

module "sns_email_topic" {
  source = "github.com/deanwilson/tf_sns_email"

  display_name  = "${var.display_name}"
  email_address = "${var.email_address}"
  owner         = "${var.owner}"
  stack_name    = "${var.stack_name}"
}

module "cloudwatch" {
  source = "../modules/cloudwatch"

  apex_function_check_lambda_capacity = "${var.apex_function_check_lambda_capacity}"
  lambda_function_role_id             = "${module.iam.lambda_function_role_id}"
  sns_email_topic_arn                 = "${module.sns_email_topic.arn}"
}

主な変更点は module "sns_email_topic" です。Terraformにはaws_sns_topic_subscription Resourceを利用してSNS Topicの購読ができます。しかし、Email/SMSには対応していません。これらのProtocolは購読の許可をしなければならずTerraformの処理とマッチないためです。

この問題を解決するためにdeanwilson/tf_sns_emailを利用しています。このModuleはaws_cloudformation_stack Resourceを利用してCFn経由でSNS Topicを作成します。CFnの場合はProtocolがEmailの場合でもSNS Topicの購読処理に対応ているのでこの問題を解決することができます。

infrastructure/modules/cloudwatch/cloudwatch.tf

resource "aws_cloudwatch_event_rule" "lambda" {
  name                = "apex-check-lambda-capacity"
  description         = "apex check lambda capacity"
  schedule_expression = "rate(1 minute)"
}

resource "aws_cloudwatch_event_target" "lambda" {
  rule      = "${aws_cloudwatch_event_rule.lambda.name}"
  target_id = "apex-check-lambda-capacity"
  arn       = "${var.apex_function_check_lambda_capacity}"
}

resource "aws_lambda_permission" "lambda" {
  statement_id  = "AllowExecutionFromCloudWatch"
  action        = "lambda:InvokeFunction"
  function_name = "${aws_cloudwatch_event_target.lambda.arn}"
  principal     = "events.amazonaws.com"
  source_arn    = "${aws_cloudwatch_event_rule.lambda.arn}"
}

resource "aws_cloudwatch_metric_alarm" "lambda" {
  alarm_name          = "apex-check-lambda-capacity"
  alarm_description   = "Lambda capacity usage alert"
  namespace           = "lambda"
  metric_name         = "size"
  statistic           = "Average"
  period              = "300"
  unit                = "Bytes"
  evaluation_periods  = "2"
  threshold           = "${var.threshold}"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  alarm_actions       = ["${var.sns_email_topic_arn}"]
}

aws_cloudwatch_event_ruleschedule_expression Argumentで定期処理の記述をしています。書式は公式ドキュメントを参照してください。

aws_cloudwatch_metric_alarmでCloudWatch Alarmの設定をしています。内容はLambda関数のリポジトリに含まれているCFnを参考にしました。

コードの実行

以下ではコードを実行する方法について解説します。

Submoduleの取得

Lambda関数をInvokeする前に肝心のLambda関数をGitHubからpullします。Submoduleとして取り込んでいるので以下のコマンドを実行すればOKです。

$ git submodule update --init

ApexのLifeCycle hookで実行できないかと試してみたのですが、そのそもこの時点ではApexから認識できるLambda関数が存在しない状態なのでエラーになってしまいました。残念。

ModuleへのSymlink作成

以下のコマンドを実行してModuleへのSymlinkを infrastructure/dev/.terraform/modules 以下に作成してください。

$ apex infra get

Lambda関数用IAM Roleの作成

続いてLambda関数に割り当てるIAM Roleを作成します。以下のコマンドを実行してください。

# 確認
$ apex infra plan -target=module.iam -var apex_function_check_lambda_capacity=aaa
# 実行
$ apex infra apply -target=module.iam -var apex_function_check_lambda_capacity=aaa

以前のエントリを書いた時点では気付かなかったのですが、ApexはTerraformに以下の変数を自動で渡してくれます。

変数 内容
aws_region the AWS region name such as us-west-2
apex_environment the environment name such as prod or stage
apex_function_role the Lambda role ARN
apex_function_NAME Lambda function ARNs by NAME

しかし、この変数はLambda関数をApex経由でデプロイした後に設定されるようです。この時点ではまだデプロイしてないので、変数が設定されていません。そのため -var で適当な値を変数に設定しておく必要があります。辛みある。

ちなみに、ApexがTerraformに渡す変数は以下のコマンドで確認可能です( aws_region は表示されないようです)。

$ apex list --tfvars
apex_function_check_lambda_capacity="arn:aws:lambda:ap-northeast-1:************:function:check_lambda_capacity"

Lambda関数のデプロイ

IAM Roleの作成が完了したらLambda関数をデプロイしましょう。

# 確認
$ apex deploy --dry-run
# 実行
$ apex deploy

残りのTerraformを実行

この時点ではLambda関数のデプロイが完了しているので infra サブコマンドにオプションを指定する必要はありません。以下のコマンドを実行するとSNS Topicを作成します。購読用Emailは infrastructure/modules/cloudwatch/variables.tf に設定してあるので事前に修正してください。

# 確認
$ apex infra plan
# 実行
$ apex infra apply

Terraformの実行後該当のEmailにSNS Topicの確認メールが届くはずです。購読してください。

Lambda関数のInvoke

今回のLambda関数はCloudWatch Events Scheduleによって定期的に実行されます。本当に実行されているのかログへの書き込みをもって確認したいので以下のコマンドを実行しておきましょう。

$ apex logs --follow

例えば以下のようなログが表示されます。

/aws/lambda/check_lambda_capacity START RequestId: 47ea14fb-35d8-11e6-b276-1761d2680933 Version: $LATEST
/aws/lambda/check_lambda_capacity 9092950
/aws/lambda/check_lambda_capacity {'ResponseMetadata': {'HTTPStatusCode': 200, 'RequestId': '4801e34d-35d8-11e6-9baa-014115757042'}}
/aws/lambda/check_lambda_capacity END RequestId: 47ea14fb-35d8-11e6-b276-1761d2680933
/aws/lambda/check_lambda_capacity REPORT RequestId: 47ea14fb-35d8-11e6-b276-1761d2680933        Duration: 69.02 ms      Billed Duration: 100 ms      Memory Size: 128 MB     Max Memory Used: 47 MB

CloudWatch Metricの出力も見てみましょう。

$ aws cloudwatch get-metric-statistics \
  --namespace lambda \
  --metric-name size \
  --start-time "$(date -v1H '+%Y-%m-%dT%TZ')" \
  --end-time "$(date '+%Y-%m-%dT%TZ')" \
  --statistics Average \
  --period 60 \
  --query 'Datapoints[0]'
{
    "Timestamp": "2016-06-19T04:47:00Z",
    "Average": 9092950.0,
    "Unit": "Bytes"
}

ちゃんとMetricが表示されているようです。やりましたね。

CloudWatch Alarmの確認

正常にCloudWatch Alarmが実行されるか確認してみます。 infrastructure/modules/cloudwatch/variables.tfthreshold で閾値を定義しています。1MB(1000000Bytes)に変更してTerraformを再度実行してみます。

$ apex infra plan
<snip>
~ module.cloudwatch.aws_cloudwatch_metric_alarm.lambda
    threshold: "100000000" => "1000000"


Plan: 0 to add, 1 to change, 0 to destroy.
$ apex infra apply

しばらくすると「[Alert] apex-check-lambda-capacity」という件名のSNSの通知が来ると思います。

まとめ

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

ApexとTerraformを利用することによりCloudWatch Eventsだけではなく、CloudWatch Events Scheduleの設定もコード化できることを確認しました。

本エントリがみなさんの参考になれば幸いです。