2
1

CloudWatchをTerraform化してみた

Last updated at Posted at 2024-09-10

本記事のテーマ

今回、AWSに構築している各種リソースを監視しているCloudWatchをTerraform化したので、その際の考慮ポイントを含めて紹介します。

Terraform導入の背景

AWSに構築している各種サービスをCloudWatchで監視していますが、以下の問題を解決(改善)するためTerraform化することにしました。

  • CloudWatchアラームを「東京リージョン」と「バージニアリージョン」に約900件!作成しており、管理が煩雑
  • 新規リソースの追加タイミングで監視、アラームを手動作成しているため、メンテナンス効率が悪く、設定漏れや設定ミスなどの発生リスクも高い

作業環境

以下の通りです。

  • クラウド環境:AWS
  • Terraformがインストール済み
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

構成図

image.png

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は共通化するためモジュールを分け、以下の構成としました。
image.png


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を保存しています。

#versions.tf
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ファイルで使用できる変数を指定します。
以下のサンプルコードでは、環境名をフルネームと省略形で記載しています。

#variables.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 ブロックでは、リソースのプロバイダー情報を記載します。

#main.tf
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
    tags = {
        Environment = var.environment
    }
    
    resourceブロックで作成するリソースに一律にタグを設定することができます。

  • alias
    alias   = "virginia"
    
    SNS Topicは東京リージョン(ap-northeast-1)とバージニアリージョン(us-east-1)にあるため、それぞれ記載します。東京リージョンは、Default Providerとして使用し、バージニアリージョンはalias= "virginia"として明示的に指定します。

resource ブロックではSNS Topicのリソース情報を記載します。

#main.tf
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 = aws.virginia
    
    バージニアリージョンのリソースには、providerとしてaws.virginiaを記載します。東京リージョンのリソースはDefault Providerとして使用しているので記載は不要です。

  • name
    name = "${var.env_3_letter}_warning"
    
    各環境でコードを流用しやすくするため、変数を含めて記載します。Develop環境へ流用する場合は、Terraformリソース名箇所を変更するのみでコードの流用が可能となります。
    #例:
    resource "aws_sns_topic" "prd_warning_tokyo" {
    
    resource "aws_sns_topic" "dev_warning_tokyo" {
    

  • policy
    policy = templatefile(
       "${path.module}/policy.json.tftpl",
       {
         Resource = [
           "arn:aws:sns:us-east-1:123456789:${var.env_3_letter}_warning",
         ]
       }
     )
    
    アクセスポリシーは、「Resource」以外は共通であるためtemplatefile 関数を使用し、ポリシーのjsonファイルをテンプレートとして参照します。
    ポリシーの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として参照する事が出来ます。

#output.tf
 
#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 "prd_warning_tokyo_arn" 
    
    outputブロックのTerraformリソース名には、リソース名称を記述します。
    なお、複数のSNS Topicがある場合は、リソースが特定出来る名称とします。

  • description
    description = "東京リージョン用 SlackのwarningチャンネルのSNS Topic"
    
    descriptionにリソースの説明を記載する事でより可読性が高まります。

  • value
    value       = aws_sns_topic.prd_warning_tokyo.arn
    
    valueには、他の構成から参照したい属性を指定します。属性とは、VPC IDやインスタンスID、今回のようにSNS TopicのARN情報の指定が可能です。SNS TopicのARN情報を参照する場合は、以下のように記載します。image.png

Tips
公式サイトを参考にしても具体的な記載内容が分からないという方は、import機能がおすすめです。
作成済みのAWSリソースを自動でコード化するTerraform 1.5 で追加された機能です。

  • 公式サイトのImportを参考にImportブロックを記載します。
  • 以下コマンドを実行するとgenerated.tfのファイルが生成されますので、生成されたコードを参考にコードを作成します。
    terraform plan -generate-config-out=generated.tf
    

CloudWatchのコード化

CloudWatchも同様に以下のHashiCorp社の公式ドキュメントを参考に作成します。
※Resource: aws_cloudwatch_metric_alarm

versions.tf

versions.tfは、SNS Topicと同様に作成します。
またvariables.tf には、Dimensionに指定する項目でDataSourceでは取得不可の項目を指定します。
下記サンプルコードでは、ElastiCacheのCacheClusterID情報を取得しています。

#variables.tf
 
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.tf
data "aws_instance" "Bastion" {
  filter {
    name   = "tag:Name"
    values = ["PrdBastion"]
  }
}

また、別構成で管理しているSNS TopicのARN情報を取得するため、terraform_remote_stateを記載します。
SNS Topicのbackend情報を記載する事でstateファイルで管理されているSNS Topic情報をDataSourceとして取得します。

#data.tf
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 を指定します。

#main.tf
provider "aws" {
  region  = "ap-northeast-1"
  profile = "xxx"
  default_tags {
    tags = {
      Environment = var.environment
    }
  }
}

resource ブロックでは、CloudWatchアラーム情報を記載します。

main.tf
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)
      

  • ignore_changes
    lifecycle {
        ignore_changes = [
          actions_enabled,
        ]
      }
    
    CloudWatchアクションの有効・無効設定をメンテナンス都度、Terraformコードを修正となると運用効率が低下するため、通知アクションのみAWSコンソールで変更する運用としました。
    通常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化したいと思います。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1