Amazon CloudWatch Alarmでアラーム発生時にSNSからLambdaを起動してみる
はじめに
最近仕事でよくCloudWatch Alarmでアラーム発生時に〇〇するみたいなことをしていたので手順をまとめようと思います。
概要
今回は適当に作ったEC2インスタンスのメトリクスでアラームを設定してSNS topic経由でLambdaを実行してみようと思います。
まずは実際にコンソール上で作成しその後Terraform化してみたいと思います。
AWS Lambdaの設定
まずは最終的に実行されるLambda関数を作成します。
今回は関数名を「alarm-test」とし、ランタイムは「python3.11」を選択して「関数の作成」をクリックします。
関数が作成されました。今回は呼び出されることを確認したいだけなので最初から入力済みのコードをそのまま使います。
次にLambdaの呼び出し元となるSNSを作成します。
Amazon SNSの設定
「トピックの作成」をクリックします。
タイプ「スタンダード」を選択し、名前を「alarm-test」として「トピックの作成」をクリックします。
トピックが作成できました。次にサブスクリプションを作成します。
プロトコルで「AWS Lambda」を選択します。
エンドポイントには先ほど作成したLambdaのARNを入力します。「サブスクリプションの作成」をクリックします。
サブスクリプションが作成されました。
今回はトピックに対してLambdaのサブスクリプションだけを作成しましたがそのほかにも
プロトコルで「Eメール」を選んでメールで通知したり「HTTPS」を選んで外部サービスのエンドポイントを呼び出すことも可能です。
これで今回用のSNSができたので次にCloudWatch Alarmの設定を行なっていきます。
Amazon CloudWatch Alarmの設定
「アラームの作成」をクリックします。
「メトリクスの選択」をクリックします。
今回はEC2のメトリクスを対象とするので「EC2」をクリックします。
「インスタンス別メトリクス」をクリックします。
インスタンス毎のメトリクスが一覧で表示されるます。
今回はEC2のCPUの使用率を監視するので「CPUUtilization」を選択して「メトリクスの選択」をクリックします。
「メトリクスと条件の指定」画面に切り替わります。
今回はCPUの使用率が1分間の間の最大値が80よりも大きい値があった場合にアラームを実行するように設定します。
「メトリクス名」を「CPUUtilization」にします。
「統計」を「最大」にします。
期間を「1分」にします。
条件は「静的」、「より大きい」、閾値は「80」を設定して「次へ」をクリックします。
次に「アクションの設定」 の画面になります。
「アラーム状態のトリガー」では「アラーム状態」を選びます。
SNSトピックの選択では先ほど作成したSNSを指定したいので「既存のトピックを選択」を選び、
「通知の送信先」で先ほど作成したトピックの「alarm-test」を指定します。
「次へ」をクリックします。
「名前と説明を追加」画面になるので「アラーム名」を入力して「次へ」をクリックします。
「プレビューと作成」画面になるので内容を確認して「アラームの作成」をクリックします。
アラームが作成されました。
これで一通り設定できたので動作を確認してみます。
動作確認
既存のEC2インスタンスに接続してCPUに負荷を加えるコマンドを実行してみます。
しばらく待っているとアラーム状態になりました。
Lambdaのログを見てみると実行されていることがわかりました。
これで正しく動作していることが確認できました。
次は今作った内容をTerraform化していきます。
Terraform化
ざっくりこんな構成で作成してみたいと思います。
├── main.tf
├── modules
│ ├── alarm
│ │ ├── alarm.tf
│ │ ├── function.tf
│ │ ├── functions
│ │ │ └── sample
│ │ │ └── main.py
│ │ ├── logs.tf
│ │ ├── permission.tf
│ │ ├── provider.tf
│ │ ├── role.tf
│ │ ├── topic.tf
│ │ └── variables.tf
│ └── target
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
└── variables.tf
modules/targetはアラームの対象となるEC2を作成します。
それに対してmodules/alarmでアラームのリソースを作成していきます。
リポジトリはこちらです。
targetモジュールの作成
先ほどの手順にはなかったですが、stressコマンドをインストール済みのEC2インスタンスを用意してみます。
variable "project" {
description = "project name"
type = string
}
variable "env" {
description = "environment type"
type = string
}
variable "security_group_ids" {
description = "security group ids"
type = list(string)
}
variable "subnet_id" {
description = "subnet id"
type = string
}
ここはSSMで接続できてstressコマンドがあればいいのでざっくり設定します。
# -------------------------------------
# Terraform configuration
# -------------------------------------
terraform {
required_version = ">= 0.14.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.17"
}
}
}
# ******************************
# EC2 Instance - テスト用
# ******************************
data "aws_ssm_parameter" "amzn_ami" {
name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}
resource "aws_instance" "main" {
ami = data.aws_ssm_parameter.amzn_ami.value
instance_type = "t2.micro"
vpc_security_group_ids = var.security_group_ids
subnet_id = var.subnet_id
iam_instance_profile = aws_iam_instance_profile.main.name
# EBSのルートボリューム設定
root_block_device {
# ボリュームサイズ(GiB)
volume_size = 8
# ボリュームタイプ
volume_type = "gp3"
# EBSのNameタグ
tags = {
Name = "${var.project}-${var.env}-ebs-test"
}
}
tags = {
Name = "${var.project}-${var.env}-ec2-test"
}
}
resource "aws_ssm_association" "main" {
association_name = "${var.project}-${var.env}-association-test"
name = "AWS-RunShellScript"
targets {
key = "InstanceIds"
values = [aws_instance.main.id]
}
parameters = {
"commands" = <<EOF
sudo yum install -y stress
EOF
}
}
# ******************************
# IAM Role
# ******************************
resource "aws_iam_role" "main" {
name = "${var.project}-${var.env}-role-ec2-test"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Sid = ""
Principal = {
Service = "ec2.amazonaws.com"
}
},
]
})
path = "/"
}
resource "aws_iam_policy_attachment" "main" {
name = "${var.project}-${var.env}-policy-attachment-ec2-test"
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
roles = [aws_iam_role.main.name]
}
# ******************************
# IAM InstanceProfile
# ******************************
resource "aws_iam_instance_profile" "main" {
name = "${var.project}-${var.env}-instance-profile"
role = aws_iam_role.main.name
}
alarmモジュール側で使うのでインスタンスのidを出力しておきます。
output "ec2_instance_id" {
value = aws_instance.main.id
}
ssm接続できてstressコマンドが打てればいいだけなのでこんな感じです。
alarmモジュールの設定
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.17"
}
}
}
インスタンスのIDを受け取るので変数を用意しておきます。
variable "project" {
description = "project name"
type = string
}
variable "env" {
description = "environment type"
type = string
}
variable "ec2_instance_id" {
type = string
}
Lambda関連
lambdaのログを出力するCloudWatchのロググループの作成
resource "aws_cloudwatch_log_group" "sample" {
name = "/aws/lambda/${var.project}-${var.env}-sample"
retention_in_days = 7
}
lambdaのロールの作成
data "aws_caller_identity" "current" {}
resource "aws_iam_role" "lambda_sample" {
name = "${var.project}-${var.env}-lambda-sample-role"
assume_role_policy = data.aws_iam_policy_document.assume_lambda.json
inline_policy {
name = "${var.project}-${var.env}-sample"
policy = data.aws_iam_policy_document.sample.json
}
}
data "aws_iam_policy_document" "sample" {
statement {
effect = "Allow"
actions = [
"logs:CreateLogStream",
"logs:PutLogEvents"
]
resources = [
"${aws_cloudwatch_log_group.sample.arn}:*",
]
}
}
data "aws_iam_policy_document" "assume_lambda" {
statement {
actions = [
"sts:AssumeRole"
]
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
}
}
lambda function本体を配置
import json
def lambda_handler(event, context):
# TODO implement
return {
'statusCode': 200,
'body': json.dumps('Hello from Lambda!')
}
lambdaリソースの作成
data "archive_file" "sample" {
type = "zip"
source_dir = "${path.module}/functions/sample"
output_path = "${path.module}/outputs/sample.zip"
}
resource "aws_lambda_function" "sample" {
depends_on = [
aws_cloudwatch_log_group.sample
]
function_name = "${var.project}-${var.env}-sample"
runtime = "python3.11"
handler = "main.lambda_handler"
filename = data.archive_file.sample.output_path
source_code_hash = data.archive_file.sample.output_base64sha256
role = aws_iam_role.lambda_sample.arn
}
lambdaのリソースベースポリシー作成
管理画面から実行すると自動で作成されてしまって気づかないのですが、
外部リソース(SNS)からLambda関数へのアクセス権限を与える必要があります。
resource "aws_lambda_permission" "sample" {
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.sample.function_name
principal = "sns.amazonaws.com"
source_arn = aws_sns_topic.sample.arn
}
SNS topicとsubscriptionの作成
resource "aws_sns_topic" "sample" {
name = "${var.project}-${var.env}-sample"
}
resource "aws_sns_topic_subscription" "sample" {
topic_arn = aws_sns_topic.sample.arn
protocol = "lambda"
endpoint = aws_lambda_function.sample.arn
}
CloudWatch Alarm alarmの作成
ここのdimensionsの書き方を間違っててしばらく悩みました・・・。
resource "aws_cloudwatch_metric_alarm" "sample" {
alarm_name = "${var.project}-${var.env}-AlarmSample"
alarm_description = "CPUの負荷をみてLambdaを実行します。"
evaluation_periods = 1
namespace = "AWS/EC2"
metric_name = "CPUUtilization"
dimensions = {
InstanceId = var.ec2_instance_id
}
period = 60
statistic = "Maximum"
threshold = 80
comparison_operator = "GreaterThanThreshold"
alarm_actions = [aws_sns_topic.sample.arn]
}
main.tfなど
# -------------------------------------
# Terraform configuration
# -------------------------------------
terraform {
required_version = ">= 0.14.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.17"
}
}
}
# -------------------------------------
# Provider configuration
# -------------------------------------
provider "aws" {
region = var.region
default_tags {
tags = {
project = var.project
env = var.env
managed_by = "terraform"
}
}
}
# -------------------------------------
# Sample EC2
# -------------------------------------
module "target" {
source = "./modules/target"
project = var.project
env = var.env
security_group_ids = var.security_group_ids
subnet_id = var.subnet_id
}
# -------------------------------------
# Sample Alart
# -------------------------------------
module "alarm" {
source = "./modules/alarm"
project = var.project
env = var.env
ec2_instance_id = module.target.ec2_instance_id
}
variable "region" {
description = "AWS region"
default = "ap-northeast-1"
type = string
}
variable "project" {
description = "project name"
default = "terraform-alart-sample"
type = string
}
variable "env" {
description = "environment type"
default = "dev"
type = string
}
variable "security_group_ids" {
description = "security group ids"
type = list(string)
}
variable "subnet_id" {
description = "subnet id"
type = string
}
ディレクトリ構成や名称などかなり適当ではありますが一旦できたので試してみたいと思います。
実行してみる
セキュリティグループIDとサブネットIDは変数で渡すようにしているので以下のようにplanを実行します。
terraform plan -var 'security_group_ids=[ "xxxxxxxxxxx" ]' -var 'subnet_id=xxxxxxxxxxxxxxxxx'
内容に問題がなければapplyします
terraform plan -var 'security_group_ids=[ "xxxxxxxxxxx" ]' -var 'subnet_id=xxxxxxxxxxxxxxxxx'
applyが完了したらEC2インスタンスに接続してstressコマンドを打ってしばらく待ちます。
検知されました!
Lambdaも実行されました!
まとめ
今回は管理画面上でアラームを設定してからTerraform化を行なってみました。
画面上でポチポチやってる時に気にしていなかったリソースについてもTerraformでは気にする必要があるので良い勉強になりました。
監視対象など違うのでこのTerraformはそのまま使えないですが参考にでもなれば幸いです。
おまけ
実はTerraformでpython3.11を使おうとして以下のメッセージがでていました。
% terraform validate
╷
│ Error: expected runtime to be one of [nodejs nodejs4.3 nodejs6.10 nodejs8.10 nodejs10.x nodejs12.x nodejs14.x nodejs16.x java8 java8.al2 java11 python2.7 python3.6 python3.7 python3.8 python3.9 dotnetcore1.0 dotnetcore2.0 dotnetcore2.1 dotnetcore3.1 dotnet6 nodejs4.3-edge go1.x ruby2.5 ruby2.7 provided provided.al2 nodejs18.x python3.10 java17 ruby3.2], got python3.11
│
│ with module.alarm.aws_lambda_function.sample,
│ on modules/alarm/function.tf line 13, in resource "aws_lambda_function" "sample":
│ 13: runtime = "python3.11"
│
╵
いつもお世話になっているブログを参考に修正しました 🙇♂️
正式リリースになった AWS SAM CLI の Terraform サポート機能を試す
Discussion