ApexとTerraformでCloudWatch EventsによりInvokeされるLambda関数をデプロイする

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

追記

以下のエントリに本エントリを書いてた時点では気付かなかった点などを記述しています。読んでみてください。

はじめに

こんにちは、中山です。

弊社では社員毎にAWSのアカウントが割り当てられ、検証用途に利用できます。私もいろいろな用途に利用しているのですが、EC2インスタンスを何度も作成していると知らない間に不要なAMIやそのSnapshotが溜まっていきます。

どうしたものかと考えていたのですが、弊社千葉がAMIの登録解除時にCloudWatch Events経由でSnapshotを削除するLambda関数を紹介するエントリを書いていました。

いいね!という訳で早速使おうと思ったのですが、せっかくなのでApexを利用してLambda関数をデプロイする仕組みを作成してみました。Apexそのものについては弊社八幡のエントリを参照してください。

ApexはあくまでLambda関数のデプロイに特化しており、AWSリソースを作成するにはTerraformを利用します。CloudWatch EventsもTerraformで作成するのですが、このあたりの情報が意外と少ないなと感じたのでご紹介します。

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

Tool Version
Apex v0.10.2
Terraform v0.16.0

コード

実際のコードをGitHubに上げておきました。リポジトリをcloneすれば利用可能です。Lambda関数のコードはそのままです。千葉さんありがとうございます。

リポジトリの構造

apex-auto-delete-snapshot
├── .gitignore
├── README.md
├── functions
│   └── apex-auto-delete-snapshot
│       └── _apex_main.py
├── infrastructure
│   ├── dev
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   └── modules
│       ├── cloudwatch_events
│       │   ├── cloudwatch_events.tf
│       │   └── variables.tf
│       └── iam
│           ├── iam.tf
│           └── outputs.tf
└── project.json

コードの解説

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

project.json

まずはトップディレクトリにある project.json を見てみましょう。このファイルでLambda関数に設定する関数名などを定義します。

{
  "name": "apex-auto-delete-snapshot",
  "description": "apex auto delete snapshot",
  "nameTemplate": "{{.Function.Name}}",
  "handler": "_apex_main.lambda_handler",
  "memory": 128,
  "timeout": 15,
  "defaultEnvironment": "dev",
  "runtime": "python"
}

それぞれのKeyの意味は以下の通りです。

Key 意味
name ApexのProject名(Lambda関数名に利用される)
description Lambda関数の説明
nameTemplate Lambda関数名のテンプレート(デフォルトではProject名_関数名という名前になる)
handler Lambda関数のhandler名
memory Lambda関数に割り当てるMemory
timeout Lambda関数に割り当てるTimeout値
defaultEnvironment Apexがデフォルトで利用する環境
runtime Lambda関数のRuntime

infrastructure/dev/main.tf

Terraformの大本となるファイルです。このファイル経由で各種Moduleが呼び出されます。

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

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

  aws_region              = "${var.aws_region}"
  lambda_function_role_id = "${module.iam.lambda_function_role_id}"
}

IAM関連のResourceは iam というModuleに、CloudWatch Events関連のResourceは cloudwatch_events というModuleにまとめています。Moduleの実態は infrastructure/modules 以下に配置しています。

infrastructure/modules/iam/iam.tf

Lambda関数に割り当てるIAM Roleを作成するためのファイルです。

resource "aws_iam_role" "lambda_function" {
  name = "apex-auto-delete-snapshot"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
}

resource "aws_iam_policy_attachment" "cloudwatchlogs_full_access" {
  name       = "CloudWatchLogsFullAccess"
  roles      = ["${aws_iam_role.lambda_function.name}"]
  policy_arn = "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess"
}

resource "aws_iam_policy_attachment" "ec2_full_access" {
  name       = "EC2FullAccess"
  roles      = ["${aws_iam_role.lambda_function.name}"]
  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2FullAccess"
}

IAM Roleはaws_iam_role Resourceによって作成可能です。このファイルでAssume Role Policyを作成し、aws_iam_policy_attachment ResourceでIAM Roleに割り当てるPolicyを指定します。

Lambda関数は起動時などにそのログをCloudWatch Logsへ書き込むため、それ用の権限が必要です。また、今回のLambda関数ではSnapshotの参照/削除を行っているためその権限も付与する必要があります。aws_iam_role_policy Resourceを利用すればInline Policyを指定して最小権限のみ付与することも可能ですが、今回は手を抜いてAWS Managed Policyを利用します。

infrastructure/modules/cloudwatch_events/cloudwatch_events.tf

CloudWatch Eventsを作成するためのファイルです。

resource "aws_cloudwatch_event_rule" "lambda" {
  name        = "apex-auto-delete-snapshot"
  description = "apex auto delete snapshot"

  event_pattern = <<EOT
{
  "detail-type": [
    "AWS API Call via CloudTrail"
  ],
  "detail": {
    "eventSource": [
      "ec2.amazonaws.com"
    ],
    "eventName": [
      "DeregisterImage"
    ]
  }
}
EOT
}

resource "aws_cloudwatch_event_target" "lambda" {
  rule      = "${aws_cloudwatch_event_rule.lambda.name}"
  target_id = "apex-auto-delete-snapshot"
  arn       = "arn:aws:lambda:${var.aws_region}:${element(split(":", var.lambda_function_role_id), 4)}:function:apex-auto-delete-snapshot"
}

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}"
}

cloudwatch_event_rule ResourceでCloudWatch Event Ruleを作成します。 event_pattern にどのEventをアクションの起点にするのかをJSONで指定します。

cloudwatch_event_target Resourceで先程作成したEventによって起動するターゲットを指定します。 arn にデプロイしたLambda関数のARNを指定しています。Lambda関数のARNは arn:aws:lambda:<region>:<account-id>:function:<function-name> という形式です。Apexは aws_region にAWSのRegionを自動で設定してくれるのでそれを利用しています。 lambda_function_role_id に自分のAWS Account IDが記述されているので elementsplit 関数を利用して抜き出しています。正直あまりスマートな記述方法ではないですね。

lambda_permission ResourceでLambda関数をInvokeするAWSリソース(今回の例ではCloudWatch Events)を指定します。

コードの実行

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

ModuleへのSymlink作成

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

$ apex infra get

Lambda関数用IAM Roleの作成

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

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

-target=module.iam というオプションを指定することによりTerraformの実行範囲をそのResourceのみに制限することが可能です。このオプションを指定する理由はこの時点ではLambda関数をデプロイしていないためです。CloudWatch Eventsの登録時にLambda関数のARNを指定する必要があるのですが、まだデプロイされていないためそんな関数はないと怒られてしまいます。

では、最初にLambda関数をデプロイすればいいのではと考えた方もいるかと思いますが、Lambda関数に割り当てるIAM Roleを作成しないとデプロイできません。そのため、Terraform内でLambda関数のARNを参照する場合は、以下の手順を踏む必要があります。

  1. Lambda関数用IAM Roleの作成
  2. Lambda関数のデプロイ
  3. 残りのTerraformを実行

このあたりもう少し上手い仕組みがあればいいですね。Apex作者の方も認識しているようですがまだ根本解決していないようです。今後に期待しましょう。

Lambda関数のデプロイ

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

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

CloudWatch Eventsの作成

最後にCloudWatch Eventsを作成します。この時点ではLambda関数のデプロイが完了しているので infra サブコマンドにオプションを指定する必要はありません。

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

Lambda関数のInvoke

Apexには logs サブコマンドでLambda関数がCloudWatch Logsに書き込んだログを標準出力に表示する機能があります。更に、 --follow オプションを指定することで tail -f のようにログを表示し続けることが可能です。ログへの書き込みでLambda関数の動作を確認したいので実行しておきましょう。

$ apex logs --follow

今回のLambda関数はAMIを登録解除(Deregister)した際にCloudWatch Events経由でInvokeされます。awscliで適当なAMIをDeregisterしてみましょう。もちろんAMCから実行してもOKです。

$ aws ec2 deregister-image \
  --image-id "$(aws ec2 describe-images \
    --owners self \
    --query 'Images[0].ImageId' \
    --output text)"

AMIのDeregister後、しばらくするとLambda関数がInvokeされSnapshotを削除した旨、ログに表示されると思います。例えば以下のような感じです。

/aws/lambda/apex-auto-delete-snapshot START RequestId: f55438e1-351d-11e6-95ca-f96c10b66cc3 Version: $LATEST
/aws/lambda/apex-auto-delete-snapshot [INFO]    2016-06-18T06:29:07.325Z        f55438e1-351d-11e6-95ca-f96c10b66cc3    Found credentials in environment variables.
/aws/lambda/apex-auto-delete-snapshot [INFO]    2016-06-18T06:29:08.916Z        f55438e1-351d-11e6-95ca-f96c10b66cc3    Starting new HTTPS connection (1): ec2.ap-northeast-1.amazonaws.com
/aws/lambda/apex-auto-delete-snapshot [INFO]    2016-06-18T06:29:10.916Z        f55438e1-351d-11e6-95ca-f96c10b66cc3    Delete target:snap-3421f6da, Description:Created by CreateImage(i-ab9e1234) for ami-7bba5e1a from vol-2b6fb5d5
/aws/lambda/apex-auto-delete-snapshot END RequestId: f55438e1-351d-11e6-95ca-f96c10b66cc3
/aws/lambda/apex-auto-delete-snapshot REPORT RequestId: f55438e1-351d-11e6-95ca-f96c10b66cc3    Duration: 3594.02 ms    Billed Duration: 3600 ms     Memory Size: 128 MB     Max Memory Used: 35 MB

上記ログでは snap-3421f6da というSnapshotが削除されたようです。やりましたね。

まとめ

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

Apexを利用することによりLambda関数のデプロイが簡単に実行できることを確認しました。また、Terraformをそのまま利用できる点も嬉しいポイントですね。

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