タイトルの通り、Cloud Watch Alarm -> SNS Topic -> Lambdaという構成のAWSリソースを構築しました。
当初の構成ではCloud Watch AlarmからLambdaをOUで絞って呼び出して実行させる予定でしたが、諸々問題があってSNS Topicを追加して対応した流れになります。
自身の振り返りも含めて、類似構成をプライベートのAWSアカウントで作成してみましたので、動作検証の様子も含めて記事にまとめてみたいと思います。
注:私が担当したのは要件や全体的な構成図が出来上がった状態から、必要なAWSリソースの構築や不具合の対応等です。前提となるアプリケーション設計や背景などの詳細は記載しません。
要件
実現したい事
- Cloud Watchでアプリケーション用ロードバランサー(NLB)の死活監視をする
- ステータス変更をアラーム条件として検知した際にLambdaを起動して通知をさせる
- アクセス制限方法としてOU(組織単位)による権限制御をする
サービス概要
<登場人物>
- EKS Pod (アプリケーション基盤) ※この記事では具体的に触れません
- AWS NLB (アプリケーション基盤のTCP通信を担う)
- Cloud Watch Alarm (NLBの死活監視・状態変更を検知して通知)
- SNS Topic (Cloud Watch Alarmの通知を検知してLambdaを起動する)
- Lambda (アプリケーションの再起動/停止時の通知処理を起動する)
■どのようなサービス?
→アプリケーションの通知機能
■何に対する通知機能?
→アプリケーションの再起動/停止時にLBの稼働ステータスを検知して通知する機能
■稼働ステータスの検知方法
→Cloud Watch Alarmを利用する
■再起動/停止時の通知方法
→Lambdaから通知処理を実行する
■アプリケーション用のLBについて
→AWS NLBを利用
→対象サービスの起動/停止と共に作成・削除される仕様
■Cloud Watch Alarmの仕様
→対象サービスの停止時は削除される為、ARNが動的に変化する(固定の値を保持できない)
→起動/停止を検知してアクションアイテムとして指定したSNS Topicに通知する
■SNS Topicについて
→Cloud Watch Alarmの通知を受け取りLambdaを起動する
■Lambdaについて
→対象アプリケーションの起動/停止をトリガーに通知処理を実行する
→作成したSNS Topicからの通知のみで起動する仕様
■OUについて
→AWSアカウントを組織単位でグループ化するサービス (Organizational Unitの略)
→aws:SourceOrgPathsというGlobal Condition Keyを用いてチェックする
システム構成
以下の構成で実装できました
しかし、最初からこの構成が考案された訳ではなく、当初は以下の構成で実装予定でした
SNS Topicを間に挟まず、
Cloud Watch AlarmにLambda関数のARNを指定
Cloud Watch Alarmからの呼び出しを許可する
↓
LambdaにCloud Watch Logsへの書き込み権限を付与
↓
Cloud Watch Logsでイベントを検知するとLambdaに内容が書き込みされて処理が実行される
という構成を想定していました。が、残念ながら実現できませんでした。
(理由は次の項目で記載します)
Cloud Watch Alarm -> Lambda構成の問題点
Cloud Watch AlarmにLambda関数のARNを指定
Cloud Watch Alarmからの呼び出しを許可する
上記の構成はごく一般的な設定です。そのまま採用したい所ですが、今回の仕様の中にARNが動的に変化する(固定の値を保持できない)という要件があるので、ARNを直接指定することができません。
そこでOUによるアクセス制限をかける方針となりましたが、ここで予期せぬ問題が発覚しました。
■権限設定が仕様的に不可
以下のような構成でアクセス制限をかけようとしました
Cloud Watch Alarm -> Lambda (aws:PrincipalOrgID
を許可する)
しかし、class method様の記事の通りサービスプリンシパルでは使えないことがわかりました。
https://dev.classmethod.jp/articles/aws-principalorgid/
代わりに、aws:SourceOrgPaths
を許可してはどうか試したところ、こちらも残念ながら不可でした。(AWSサポートの方に確認済み)
https://aws.amazon.com/jp/blogs/security/use-scalable-controls-for-aws-services-accessing-your-resources/
Lambdaリソースポリシーはaws:SourceOrgPathsをサポートしていないようです。
代わりの案としてご紹介いただいたのが、SNS Topicを経由してLambda起動を実行する構成です
https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html#alarms-and-actions
サンプルリソースを実装してみる
プライベート環境でTerraformを使って実装してみましたので、実際に利用したサンプルコードを記載します
# LB
resource "aws_lb" "alarm_test_lb" {
name = "alarm-exec-nlb"
internal = true
load_balancer_type = "network"
subnets = ["${var.subnet_id_1}", "${var.subnet_id_2}"]
enable_deletion_protection = false
}
resource "aws_lb_target_group" "alarm_nlb_tg" {
name = "alarm-exec-nlb-tg"
port = 80
protocol = "TCP"
vpc_id = var.vpc_id
}
# cloudwatch alarm
resource "aws_cloudwatch_metric_alarm" "error_rate" {
alarm_name = "error-rate"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "1"
metric_name = "UnHealthyHostCount"
namespace = "AWS/NetworkELB"
period = "300"
statistic = "Sum"
threshold = "0"
datapoints_to_alarm = 1
alarm_description = "This metric monitors Error rate"
insufficient_data_actions = []
dimensions = {
LoadBalancer = aws_lb.alarm_test_lb.id
}
alarm_actions = [aws_sns_topic.alarm_topic.arn]
}
# SNS Topic
resource "aws_sns_topic" "alarm_topic" {
name = "cloudwatch-alarm-sns-topic"
}
resource "aws_sns_topic_policy" "alarm_topic_policy" {
arn = aws_sns_topic.alarm_topic.arn
policy = data.aws_iam_policy_document.sns_topic_policy.json
}
resource "aws_sns_topic_subscription" "lambda_subscription" {
topic_arn = aws_sns_topic.alarm_topic.arn
protocol = "lambda"
endpoint = aws_lambda_function.example_lambda.arn
}
data "aws_iam_policy_document" "sns_topic_policy" {
statement {
actions = [
"SNS:Publish",
]
effect = "Allow"
resources = [aws_sns_topic.alarm_topic.arn]
condition {
test = "StringEquals"
variable = "aws:SourceOrgID"
values = [var.ouid]
}
principals {
type = "Service"
identifiers = ["cloudwatch.amazonaws.com"]
}
}
}
# Lambda
resource "aws_lambda_function" "example_lambda" {
filename = "lambda_function_payload.zip"
function_name = "example_lambda_function"
role = aws_iam_role.lambda_exec.arn
handler = "index.handler"
runtime = "python3.8"
source_code_hash = filebase64sha256("lambda_function_payload.zip")
environment {
variables = {
HELLO = "World"
}
}
}
resource "aws_lambda_permission" "allow_sns" {
statement_id = "AllowExecutionFromSNS"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.example_lambda.arn
principal = "sns.amazonaws.com"
source_arn = aws_sns_topic.alarm_topic.arn
}
# IAM Role for Lambda
resource "aws_iam_role" "lambda_exec" {
name = "lambda_execution_role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
},
]
})
}
resource "aws_iam_policy" "lambda_logs" {
name = "lambda_logging_policy"
description = "IAM policy for logging from a lambda"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
Effect = "Allow",
Resource = "arn:aws:logs:*:*:*"
},
]
})
}
resource "aws_iam_role_policy_attachment" "lambda_logs" {
role = aws_iam_role.lambda_exec.name
policy_arn = aws_iam_policy.lambda_logs.arn
}
作成したリソースについて解説します
※NLBはあくまで動作確認用の為割愛します
Cloud Watch Alarm
- メトリクスでUnHealthyHostCountを指定(ステータス異常となったホスト数を検知する)
- threshold:アラートがトリガーされる閾値。0とすることで、1でも異常を検知したらアラート状態にする
- GreaterThanThreshold:メトリクス値が閾値を超えた場合にアラームを発火させる
- evaluation_periods:アラームの評価期間 (1の場合、1回のデータポイントでアラームが評価される)
- datapoints_to_alarm:アラームを発火させるために必要なデータポイントの数
SNS Topic
- SNS Topicを構築
- サブスクリプションのプロトコル・エンドポイントでLambdaを指定
- IAM PolicyでCloud Wath AlarmにSNS TopicへのPublish(メッセージ公開)する権限を付与
- conditionでaws:SourceOrgIDと一致する場合のみ(指定したAWSアカウントのみ)ポリシーを適用するよう設定
Lambda
- Lambda実行用のIAM roleをアタッチ
- Lambda関数の実行を許可
- 許可されるLambda関数のARNを指定
- permisson設定でSNS Topicを許可設定
- Lambda関数のログをCloud Watch Logsに書き込める権限(ロググループ、ログストリーム、ログイベント等)を付与
動作確認してみる
コマンドから対象のLBにUnHealthyHostCountを擬似的に追加してみて、アラームを発火させてみます
[cloudshell-user@ip-**** ~]$ aws cloudwatch put-metric-data --namespace AWS/NetworkELB --metric-name UnHealthyHostCount --dimensions LoadBalancer=arn:aws:elasticloadbalncing:ap-northeast-1:****:loadbalancer/net/alarm-exec-nlb/**** --value 1 --unit Count
アラーム状態となりました
次に、Lambdaが実行されたか確認してみます
メトリクスから実行が確認できました
続けて、Lambdaの実行ログを確認します
ログイベントは以下のようになっていました
Helle World! を出力するだけの関数ですが、正常に動作していることが確認できました。
以上、Cloud Watch AlarmをトリガーにOUでアクセス制御してSNS Topic経由でLambda起動させる事例の紹介でした。
似たようなアーキテクチャを構築する際や、OUでアクセス制御してLambdaを起動したいケース等で、何かの参考になりましたら幸いです。