本記事のテーマ
今回、AWSに構築している各種リソースを監視しているCloudWatchをTerraform化したので、その際の考慮ポイントを含めて紹介します。
Terraform導入の背景
AWSに構築している各種サービスをCloudWatchで監視していますが、以下の問題を解決(改善)するためTerraform化することにしました。
- CloudWatchアラームを「東京リージョン」と「バージニアリージョン」に約900件!作成しており、管理が煩雑
- 新規リソースの追加タイミングで監視、アラームを手動作成しているため、メンテナンス効率が悪く、設定漏れや設定ミスなどの発生リスクも高い
作業環境
以下の通りです。
- クラウド環境:AWS
- Terraformがインストール済み
$ terraform --version
Terraform v1.6.6
on darwin_arm64
+ provider registry.terraform.io/hashicorp/aws v5.32.1
Tips
チームで運用をする場合、Terraformのバージョン固定するtfenvを使用するとバージョン管理が楽になるかと思います
brew install anyenv
anyenv install tfenv
tfenv install 1.6.6
tfenv use 1.6.6
構成図
CloudWatch監視の構成は、トリガー発火後にSNS Topic・Lambda経由でSlackへメッセージ通知する構成です。
また、アラームレベルを3段階(Warning/Alert/Critical)に分け、それぞれ通知先となるSlackチャンネルも分けていますので、SNS Topic・Lambdaも3種類作成しています。
上記構成では省略していますが、CloudFrontはバージニアリージョンに作成されるためCloudFront用としてCloudWatchアラーム、SNS Topic、Lambdaはバージニアリージョンにも作成しています。
今回は、Terraform導入ということをもあり、CloudWatchとSNS TopicのみTerraform化しました。
なお、上記構成には記載していませんが、Develop環境、Staging環境はProduction環境と同等なので省略しています。
Terraform導入してみた
ディレクトリ構成の検討
ディレクトリ構成は、CloudWatchアラームのメンテナンスのしやすさを考慮してstateファイルを細分化する方針としました。
ディレクトリ構成の要素は以下の3点です。
- 環境
- CloudWatchアラームレベル
- AWSサービス
また、CloudWatchから呼び出されるSNS Topicは共通化するためモジュールを分け、以下の構成としました。
Terraformコード作成
各ディレクトリに配置する主なtfファイルとコード内容は以下の通りです。
tfファイル | 内容 |
---|---|
versions.tf | Terraformバージョンやstateファイルの保存先情報を記載 |
variables.tf | tfファイル内で使用する変数を記載 |
data.tf | Terraformリソースとして管理していないAWSリソースを記載 |
main.tf | Provider、Resourceブロックおよび、localブロックを記載 |
Terraformコードを作成する際の方針は以下の通りとしました。
- Develop環境、Production環境でコードを流用出来るように環境依存する箇所は変数化する
- 通知アクションの有効/無効は、AWSコンソールで変更するため「actions_enabled」を「ignore_changes」とする
- しきい値は、各リソースでチューニングすることから変数化しない
- SNS Topicは、各アラームから参照を一元化 出来るようにterraform_remote_state として作成する
SNS Topicのコード化
以下のHashiCorp社の公式ドキュメントを参考に作成します。
※Resource: aws_sns_topic
versions.tf
Terraformのバージョンや利用するプロバイダを指定します。
また、backendにstateファイルの保存先としてS3バケットを指定します。
Backendにはローカルとリモートを指定する事が出来ますが、ローカルにstateを保存するとリソース操作が競合してしまうのでS3にstateを保存しています。
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.32.1"
}
}
backend "s3" {
bucket = "objects"
key = "terraform/sns/production.tfstate"
region = "ap-northeast-1"
profile = "xxx"
}
required_version = ">= 1.5.5"
}
variables.tf
tfファイルで使用できる変数を指定します。
以下のサンプルコードでは、環境名をフルネームと省略形で記載しています。
variable "environment" {
default = "Production"
type = string
}
variable "env_3_letter" {
default = "prd"
type = string
}
main.tf
SNS Topicリソースを記載します。provider ブロック
とresource ブロック
で構成されています。
サンプルコードは、東京リージョンとバージニアリージョンのWarningレベルのSNS Topicです。
provider ブロック
では、リソースのプロバイダー情報を記載します。
provider "aws" {
region = "ap-northeast-1"
profile = "xxx"
default_tags {
tags = { #タグ記載箇所
Environment = var.environment
}
}
}
provider "aws" {
region = "us-east-1"
profile = "xxx"
alias = "virginia" #alias指定箇所
default_tags {
tags = {
Environment = var.environment
}
}
}
ポイントとなる箇所を解説します。
- tags
resourceブロックで作成するリソースに一律にタグを設定することができます。
tags = { Environment = var.environment }
- alias
SNS Topicは東京リージョン(ap-northeast-1)とバージニアリージョン(us-east-1)にあるため、それぞれ記載します。東京リージョンは、Default Providerとして使用し、バージニアリージョンは
alias = "virginia"
alias= "virginia"
として明示的に指定します。
resource ブロック
ではSNS Topicのリソース情報を記載します。
resource "aws_sns_topic" "prd_warning_tokyo" {
display_name = "${var.env_3_letter}_warning" #変数を含め汎用性を持たせます
name = "${var.env_3_letter}_warning"
content_based_deduplication = false
fifo_topic = false
policy = templatefile( #テンプレートから呼び出す形式とする
"${path.module}/policy.json.tftpl",
{
Resource = [
"arn:aws:sns:ap-northeast-1:123456789:${var.env_3_letter}_warning",
]
}
)
}
resource "aws_sns_topic" "prd_warning_virginia" {
provider = aws.virginia #バージニアリージョンは"provider"を指定する
display_name = "${var.env_3_letter}_warning"
name = "${var.env_3_letter}_warning"
content_based_deduplication = false
fifo_topic = false
policy = templatefile(
"${path.module}/policy.json.tftpl",
{
Resource = [
"arn:aws:sns:us-east-1:123456789:${var.env_3_letter}_warning",
]
}
)
}
ポイントとなる箇所を解説します。
- provider
バージニアリージョンのリソースには、providerとして
provider = aws.virginia
aws.virginia
を記載します。東京リージョンのリソースはDefault Providerとして使用しているので記載は不要です。
- name
各環境でコードを流用しやすくするため、変数を含めて記載します。Develop環境へ流用する場合は、Terraformリソース名箇所を変更するのみでコードの流用が可能となります。
name = "${var.env_3_letter}_warning"
#例:resource "aws_sns_topic" "prd_warning_tokyo" { ↓ resource "aws_sns_topic" "dev_warning_tokyo" {
- policy
アクセスポリシーは、「Resource」以外は共通であるためtemplatefile 関数を使用し、ポリシーのjsonファイルをテンプレートとして参照します。
policy = templatefile( "${path.module}/policy.json.tftpl", { Resource = [ "arn:aws:sns:us-east-1:123456789:${var.env_3_letter}_warning", ] } )
ポリシーのjsonファイルは同じ階層に以下のpolicy.json.tftpl
として作成します。
「Resource」箇所は${jsonencode("${Resource}")}
としてmain.tfから引用するようにします。#policy.json.tftpl{ "Id": "__default_policy_ID", "Statement": [ { "Action": [ "SNS:Publish", "SNS:RemovePermission", "SNS:SetTopicAttributes", "SNS:DeleteTopic", "SNS:ListSubscriptionsByTopic", "SNS:GetTopicAttributes", "SNS:AddPermission", "SNS:Subscribe" ], "Condition": { "StringEquals": { "AWS:SourceOwner": "123456789" } }, "Effect": "Allow", "Principal": { "AWS": "*" }, "Resource": ${jsonencode("${Resource}")}, "Sid": "__default_statement_ID" } ], "Version": "2008-10-17" }
output.tf
Terraform管理したSNS Topicを別構成のCloudWatchアラームから参照出来るようにoutput.tfに記載します。
これによりstateファイルにSNS Topic情報がoutputとして記載され、他のResourceからDataSourceとして参照する事が出来ます。
#tokyo
output "prd_warning_tokyo_arn" { #リソース名が特定出来る名称とする
description = "東京リージョン用 SlackのwarningチャンネルのSNS Topic" #説明を追加する事で可読性を高める
value = aws_sns_topic.prd_warning_tokyo.arn #SNS TopicのARN情報を指定
}
#virginia
output "prd_warning_virginia_arn" {
description = "バージニアリージョン用 SlackのwarningチャンネルのSNS Topic"
value = aws_sns_topic.prd_warning_virginia.arn
}
ポイントとなる箇所を解説します。
- output
outputブロックのTerraformリソース名には、リソース名称を記述します。
output "prd_warning_tokyo_arn"
なお、複数のSNS Topicがある場合は、リソースが特定出来る名称とします。
- description
descriptionにリソースの説明を記載する事でより可読性が高まります。
description = "東京リージョン用 SlackのwarningチャンネルのSNS Topic"
- value
valueには、他の構成から参照したい属性を指定します。属性とは、VPC IDやインスタンスID、今回のようにSNS TopicのARN情報の指定が可能です。SNS TopicのARN情報を参照する場合は、以下のように記載します。
value = aws_sns_topic.prd_warning_tokyo.arn
CloudWatchのコード化
CloudWatchも同様に以下のHashiCorp社の公式ドキュメントを参考に作成します。
※Resource: aws_cloudwatch_metric_alarm
versions.tf
versions.tfは、SNS Topicと同様に作成します。
またvariables.tf には、Dimensionに指定する項目でDataSourceでは取得不可の項目を指定します。
下記サンプルコードでは、ElastiCacheのCacheClusterID情報を取得しています。
variable "env_3_letter" {
default = "Prd"
type = string
}
variable "alarm_level" {
default = "ALERT"
type = string
}
variable "elasticache-redis-01" { #変数としてElastiCacheのCacheClusterID情報を取得
default = "prd-redis-01"
type = string
}
data.tf
DataSourceを使用する事でTerraformリソースとして管理していないAWSリソースをTerraformリソースとして参照することができます。
data.tfにTerraform管理としなかった監視対象リソースを記載します。
下記サンプルコードでは、EC2情報を取得しています。
data "aws_instance" "Bastion" {
filter {
name = "tag:Name"
values = ["PrdBastion"]
}
}
また、別構成で管理しているSNS TopicのARN情報を取得するため、terraform_remote_stateを記載します。
SNS Topicのbackend情報を記載する事でstateファイルで管理されているSNS Topic情報をDataSourceとして取得します。
data "terraform_remote_state" "sns" {
backend = "s3"
config = {
bucket = "objects"
key = "terraform/sns/production.tfstate"
region = "ap-northeast-1"
profile = "xxx"
}
}
main.tf
CloudWatchリソースを記載します。provider ブロック
とresource ブロック
で構成されています。
サンプルコードは、東京リージョンで実行中のEC2に対するCPU使用率監視です。
provider ブロック
では、監視対象のメトリクスが取得されているプロバイダー情報を記載します。
該当メトリクスは、東京リージョンで取得しているため、region には、ap-northeast-1
を指定します。
provider "aws" {
region = "ap-northeast-1"
profile = "xxx"
default_tags {
tags = {
Environment = var.environment
}
}
}
resource ブロック
では、CloudWatchアラーム情報を記載します。
resource "aws_cloudwatch_metric_alarm" "_ALERT_EC2_Bastion_CPUUtilization" {
alarm_name = "(${title(var.alarm_level)})[Relux][${title(var.env_3_letter)}Bastion]CPUUtilization" #アラーム名を指定
actions_enabled = false
alarm_actions = data.terraform_remote_state.sns.outputs.prd_warning_tokyo_arn #DataSourceで取得したSNS Topicのbackend情報から取得
ok_actions = data.terraform_remote_state.sns.outputs.prd_warning_tokyo_arn
dimensions = {
InstanceId = data.aws_instance.Bastion.id #DataSourceから取得
}
evaluation_periods = 1
datapoints_to_alarm = 1
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = 300
statistic = "Average"
threshold = 85 #しきい値を指定
comparison_operator = "GreaterThanOrEqualToThreshold"
treat_missing_data = "breaching"
lifecycle { #lifecycle にignore_changesを指定する事でAWSコンソールの変更を無視
ignore_changes = [
actions_enabled,
]
}
}
ポイントとなる箇所を解説します。
- alarm_name
アラーム名を指定します。
alarm_name= "(${title(var.alarm_level)})[Relux][${title(var.env_3_letter)}Bastion]CPUUtilization"
SNS Topicと同様に各環境でコードを流用しやすくするため、変数を含めて記載します。
- alarm_actions
アラーム発生時のアクション指定します。
alarm_actions = data.terraform_remote_state.sns.outputs.prd_warning_tokyo_arn
SNS TopicのARN情報は、DataSourceで取得したSNS Topicのbackend情報から取得します。
コードの各ブロックの説明です。- terraform_remote_state.sns ・・ DataSourceで指定した定義名
- prd_warning_tokyo_arn・・SNS Topicのoutput.tfで指定した定義名
※ok_actionsも同様です
- dimensions
ディメンションを指定します。
dimensions = { InstanceId = data.aws_instance.Bastion.id }
EC2のCPUUtilizationのディメンションでは、インスタンスIDが必要となります。
該当インスタンスの情報はDataSourceで取得していますので、その中でインスタンスID情報のid
を末尾に指定する事で取得が可能となります。
その他取得可能な属性の詳細は、HashiCorp社の公式ドキュメントを参考ください。
※Resource: aws_instance > Attribute Reference
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance
- threshold
しきい値を指定します。
threshold = 85
サンプルコードでは、85%以上の時にアラームとなる設定で視覚的に分かりやすいですが、RDSの空きメモリ監視(FreeableMemory)などは、byteで指定するため判読性が下がります。
一例ですが、以下の方法を取ることで判読性を上げることが出来ます。- variables.tfにしきい値を記載
variables.tf
variable "alarm_threshold_percent_factor" { default = 0.05 type = number }
- main.tfにlocalsブロックとして総メモリ数を記載
main.tf
locals { rds_spec = tomap({ "db_m6g_large" = { total_memory_bytes = 8589934592 } }) }
- thresholdにceil関数を使用してしきい値を算出
main.tf
threshold = ceil(local.rds_spec.db_m6g_large.total_memory_bytes * var.alarm_threshold_percent_factor)
- variables.tfにしきい値を記載
- ignore_changes
CloudWatchアクションの有効・無効設定をメンテナンス都度、Terraformコードを修正となると運用効率が低下するため、通知アクションのみAWSコンソールで変更する運用としました。
lifecycle { ignore_changes = [ actions_enabled, ] }
通常AWSリソースを変更した場合、stateファイルとの差分が発生しTerraformコードも変更されます。
そこで、AWSコンソールで変更した場合でもTerraformコード側を無視するようlifecycleにactions_enabledを設定します。
lifecycle にignore_changes
を指定する事でAWSコンソールの変更を無視する 挙動になります。
Tips
コードを記載しているとインデントがずれる事がありますが、以下のコマンドで自動フォーマットしてくれます。
terraform fmt
※Command: fmt
https://developer.hashicorp.com/terraform/cli/v1.5.x/commands/fmt
終わりに
今回は、CloudWatchのTerraform化のコード事例について紹介しました。
Terraform化により、環境毎のメンテナンス性も向上しチームメンバーのコードレビュが入ることで設定ミスのリスク低下に繋がりました。
今回はCloudWatchとSNS Topicのみが対象でしたので、今後はDataSourceで定義したAWSサービスもTerraform化したいと思います。