LoginSignup
0
0

Cloud Watch AlarmをトリガーにOUでアクセス制御してSNS Topic経由でLambda起動させる

Posted at

タイトルの通り、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を用いてチェックする

システム構成

以下の構成で実装できました

スクリーンショット 2024-05-06 16.03.50.png

しかし、最初からこの構成が考案された訳ではなく、当初は以下の構成で実装予定でした

スクリーンショット 2024-05-06 16.23.51.png

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

スクリーンショット 2024-04-30 18.29.51.png

  • メトリクスでUnHealthyHostCountを指定(ステータス異常となったホスト数を検知する)
  • threshold:アラートがトリガーされる閾値。0とすることで、1でも異常を検知したらアラート状態にする
  • GreaterThanThreshold:メトリクス値が閾値を超えた場合にアラームを発火させる
  • evaluation_periods:アラームの評価期間 (1の場合、1回のデータポイントでアラームが評価される)
  • datapoints_to_alarm:アラームを発火させるために必要なデータポイントの数

SNS Topic

スクリーンショット 2024-04-30 18.47.25.png

スクリーンショット 2024-04-30 18.56.40.png

  • SNS Topicを構築
  • サブスクリプションのプロトコル・エンドポイントでLambdaを指定
  • IAM PolicyでCloud Wath AlarmにSNS TopicへのPublish(メッセージ公開)する権限を付与
  • conditionでaws:SourceOrgIDと一致する場合のみ(指定したAWSアカウントのみ)ポリシーを適用するよう設定

Lambda

スクリーンショット 2024-04-30 19.01.13.png

  • 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

スクリーンショット 2024-04-30 19.27.40.png

アラーム状態となりました

次に、Lambdaが実行されたか確認してみます

スクリーンショット 2024-04-30 19.29.22.png

メトリクスから実行が確認できました

続けて、Lambdaの実行ログを確認します
ログイベントは以下のようになっていました

スクリーンショット 2024-04-30 19.31.01.png

Helle World! を出力するだけの関数ですが、正常に動作していることが確認できました。


以上、Cloud Watch AlarmをトリガーにOUでアクセス制御してSNS Topic経由でLambda起動させる事例の紹介でした。

似たようなアーキテクチャを構築する際や、OUでアクセス制御してLambdaを起動したいケース等で、何かの参考になりましたら幸いです。

0
0
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
0
0