Terraformを利用したCloudWatch Alarmの管理

はじめに

こんにちは、中山です。

最近CloudWatch Alarmを大量に設定する機会がありました。マネジメントコンソールでポチポチやるのはさすがにツライので、その設定作業にTerraformを利用しているのですが、なかなか便利だったので本エントリでご紹介したいと思います。

TerraformでCloudWatch Alarmを管理するメリット

以下に私が考えるメリットを記載します。

1. CIツール/サービスと連携できる

当然ですがTerraformのコードは単なるテキストファイルなのでVCSでバージョン管理可能です。つまり誰が/何時/何を目的に変更したのか管理できます。もちろんCloudTrailを利用すれば同じように変更履歴を管理できますが、検索性などを考慮するとこの用途にはVCSの方が向いていると思います。また、GitHubなどのリポジトリサービスで管理しておき、お好みのCIツール/サービスと連携させたデプロイフローの定義もできます。

2. 外部からの変更に追従できる

CIツール/サービスとの連携は便利なのですが、以下のような状況が発生した場合に少し困ることがあるかもしれません。

  • 障害対応時に急遽マネジメントコンソールからCloudWatch Alarmの設定を変更してしまった
  • Terraformで管理していることを知らずにCloudWatch Alarmの設定を削除してしまった

Terraformはリソースの状態を terraform.tfstate というJSONファイルで管理しています。その内容と実際の状態に差分が存在する場合、単純にコードの内容を適用してしまうと、Terraform側の設定、つまり古い設定で上書きしてしまいます。しかし、 refresh というサブコマンドを使えばこのJSONファイルへ実際の設定を反映してくれるので、変更に追従可能です。

$ terraform refresh

もしあるリソースをTerraformの管理外にしたい場合、以下のコマンドでJSONファイルから削除できます(コードからも削除する必要あり)。もちろんあくまでTerraformの管理下から外すだけで、実際のリソースは削除されません。

$ terraform state rm <リソースタイプ>.<リソース名>

Terraformで管理しているリソースの一覧とその状態は以下のコマンドで確認できます。

# 一覧
$ terraform state list
# 状態
$ terraform state show <リソースタイプ>.<リソース名>

terraform.tfstate ファイルの管理方法は以下の2つがあります。

  • このファイル自体もVCSに含める
  • Remote Stateを利用して例えばS3に保存しておく

それぞれメリット/デメリットがありますが、手っ取り早くTerraformを利用したい、あるいはこのファイルもVCSで管理したいという用途であればリポジトリに含めるとよいと思います。ただ、このファイルは頻繁に変更されるものなので、いちいち差分をコミットするのがシンドいといった場合にはRemote Stateを利用するとよいかと思います。

3. 外部リソースの参照

CloudWatch Alarmはアラート発生時に特定のアクションを実行可能です。例えば、オートスケーリングポリシーを実行する、SNSトピックに通知するといったことができます。これらのリソースは別のツール、あるいはマネジメントコンソールで設定しているので、TerraformはあくまでCloudWatch Alarmの管理のみに利用したいといった場合もあると思います。アクションの設定には基本的にARNを指定するのですが、結構長いのであまりコード中にハードコーディングしたくないはずです。また、ハードコーディングしてしまうと、ARNが変更された場合に追従するのがちょっとシンドいです。こういった用途にデータソースは便利です。この機能を利用するとTerraform管理外の情報を取得し、コードの中で参照可能です。つまり、長ったらしいARNをハードコーディングせずに済むというわけです。詳細については以下のエントリを参照してください。

データソースはv0.7.0から導入された比較的新しい機能なので、ものによってまだ対応していない場合があります(残念ながらアクションによく指定するタイプのものはまだほとんど未対応)。ただその場合でも、Externalデータソースを利用すれば対応することが可能です。例えば、AWS CLIのdescribe系コマンドで情報を取得し、その結果をTerraform側で利用することができます。より詳細な内容については以下のエントリを参照してください。

4. 変更を適用する前に差分を確認できる

Terraformには plan というサブコマンドで変更( apply ) を適用する前に、どういった内容が変わるのかを確認できます(逆に plan -destroy で削除される内容を確認可能)。例えば、しきい値を変更した場合は以下のような出力になります。

$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but
will not be persisted to local or remote state storage.

aws_cloudwatch_metric_alarm.test: Refreshing state... (ID: test)

The Terraform execution plan has been generated and is shown below.
Resources are shown in alphabetical order for quick scanning. Green resources
will be created (or destroyed and then created if an existing resource
exists), yellow resources are being changed in-place, and red resources
will be destroyed. Cyan entries are data sources to be read.

Note: You didn't specify an "-out" parameter to save this plan, so when
"apply" is called, Terraform can't guarantee this is what will execute.

~ aws_cloudwatch_metric_alarm.test
    threshold: "70" => "80"


Plan: 0 to add, 1 to change, 0 to destroy.

CodePipelineには手動承認機能があります。この機能と組み合わせることで、変更適用前に適切な知識/権限を持った人物の確認プロセスを挟むことが可能です。つまり、承認フローの管理もできるという訳です。

5. 既存の設定をインポートできる

新規にCloudWatch Alarmの設定をする場合は別ですが、すでに設定されている環境へTerraformを導入したい場合もあると思います。こういった場合でも、v0.7.0で導入されたImportを利用すれば対応できます。より詳細な内容については以下のエントリを参照してください。

例えばアラーム名がtestという名前の設定をインポートしたい場合は以下のように実行します。このコマンドを実行すると terraform.tfstate ファイルに情報を追記してくれます(以前の状態は .backup という拡張子付きで保存しておいてくれる)。

$ terraform import aws_cloudwatch_metric_alarm.test test
aws_cloudwatch_metric_alarm.test: Importing from ID "test"...
aws_cloudwatch_metric_alarm.test: Import complete!
  Imported aws_cloudwatch_metric_alarm (ID: test)
aws_cloudwatch_metric_alarm.test: Refreshing state... (ID: test)

Import success! The resources imported are shown above. These are
now in your Terraform state. Import does not currently generate
configuration, so you must do this next. If you do not create configuration
for the above resources, then the next `terraform plan` will mark
them for destruction.

ただし、上記ドキュメントにも記載されていますが、執筆時点(2017/02/05)の最新バージョンであるv0.8.5では状態のインポートのみに対応しています。つまり、コード( .tf ファイル )自体は手動で作成する必要があります。また、一度にインポート可能なのは1つのリソースのみです。複数の設定をインポートしたい、コードも自動で生成したいという場合はdtan4/terraformingが便利です。両方の要件にマッチするとても便利なツールです(ただCloudWatch Alarmのインポート程度であれば以下のようなワンライナーでもよいかと思います)。

$ for alarm_name in $(aws cloudwatch describe-alarms --query 'MetricAlarms[].AlarmName' --output text); do terraform import aws_cloudwatch_metric_alarm.$alarm_name $alarm_name; done

CloudWatch Alarmの設定をコードに落とし込みたい場合は以下のように実行します。

$ terraforming cwa > cloudwatch.tf
$ cat cloudwatch.tf
resource "aws_cloudwatch_metric_alarm" "test" {
  alarm_name          = "test"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  evaluation_periods  = "2"
  metric_name         = "CPUUtilization"
  namespace           = "AWS/EC2"
  period              = "60"
  statistic           = "Average"
  threshold           = "70.0"
  alarm_description   = ""
  alarm_actions       = ["arn:aws:sns:ap-northeast-1:************:test", "arn:aws:autoscaling:ap-northeast-1:************:scalingPolicy:4c115f36-be0b-44a5-b57c-9f0443f25985:autoScalingGroupName/tf-asg-00b3bc61f025dcae8d847eb0e3:policyName/test"]
}

このままでも使えるのですが、さすがに変数までは設定してくれないので、その修正は手動でする必要があります。

Terraformのサンプル

能書きはいいとして実際どういった感じで管理できるのか、サンプルとなるコードを以下に記載します。Terraformはカレントディレクトリにあるコードを全て実行してくれます(逆に -target オプションで実行対象を絞ることも可能)。1つのファイルに全ての設定を定義してしまうとコードの見通しが悪くなるので、基本的に各種リソース毎にファイルを分離しておくことをオススメします。

  • variables.tf
variable "region" {
  default = "ap-northeast-1"
}

variable "sns_topic" {
  default = "test"
}

variable "policy_name" {
  default = "test"
}

data "aws_caller_identity" "current" {}

data "aws_region" "current" {
  current = true
}

aws_caller_identityでAWSアカウントIDを、aws_regionでAWSリージョン名を取得しています。また、別のファイルで利用するSNSトピック名、スケーリングポリシー名を定義しています。

  • external.tf
data "external" "scaling_policy" {
  program = ["python", "${path.module}/external/scaling_policy.py"]

  query = {
    policy_name = "${var.policy_name}"
  }
}

Externalデータソースを利用してPythonスクリプトを呼び出しています。 query 引数でスクリプトの標準入力にデータを渡すことが可能です。

  • external/scaling_policy.py
#!/usr/bin/env python

from __future__ import print_function
import boto3
import json
import sys


def main():
    policy_name = json.loads(sys.stdin.readline())['policy_name']
    scaling_policy = boto3.client('autoscaling').describe_policies(PolicyNames=[policy_name])
    print(json.dumps({'policy_arn': scaling_policy['ScalingPolicies'][0]['PolicyARN']}))


if __name__ == '__main__':
    main()

Externalデータソースから渡されたデータをもとにboto3を利用してスケーリングポリシーのARNを検索し、標準出力に表示しています。

  • cloudwatch.tf
resource "aws_cloudwatch_metric_alarm" "test" {
  alarm_name          = "test"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  evaluation_periods  = 2
  metric_name         = "CPUUtilization"
  namespace           = "AWS/EC2"
  period              = 60
  statistic           = "Average"
  threshold           = 70

  alarm_actions = [
    "arn:aws:sns:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:${var.sns_topic}",
    "${data.external.scaling_policy.result["policy_arn"]}",
  ]
}

今回はよく利用されているであろうCPU使用率の設定にしてみました。アラームの設定にはaws_cloudwatch_metric_alarmを利用しています。 alarm_actions 引数でSNSトピックARNとスケーリングポリシーのARNを指定しています。SNSトピックARNの方はわざわざExternalデータソースで取得する必要もないので、 variables.tf で定義した各種データソースを利用しました。Externalデータソースは result という属性にマップで結果を保存しています。そのため、 result["key"] といった形でデータにアクセス可能です。

まとめ

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

Terraformを利用したCloudWatch Alarmの管理方法についてご紹介しました。もちろんTerraformではなく別のツールを利用して同等のことは可能です。例えば、CloudFormationで管理する、あるいはwinebarrel/radiosondeを使うなどです。それぞれ得意/不得意があると思うので、自分の環境にあった方法を検討されるとよいのではないでしょうか。

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