【Terraform】 ECR 拡張スキャン通知用の Lambda をデプロイしてみる

こんにちは。
先日に投稿した 「Inspector V2 の ECR 拡張スキャン結果を Slack に通知する」 ですが、Lambda のみ手動で作成していたので、こちらも Terraform でデプロイできるように実装します。

【Terraform】Inspector V2 の ECR 拡張スキャン結果を Slack に通知する

Terraform

Terraform で Lambda をデプロイするには、予めソースコードを zip 化しておく必要があるようです。
ついては archive_file という Data Source で対応します。
ソースコードの更新も、zip ファイルの hash 値で判断してくれるようで便利ですね。

/** 
# NOTE: Lambda
*/

// Python スクリプト ZIP 化
data "archive_file" "ecr_enhanced_scanning_finding" {
  type        = "zip"
  source_file = "${path.module}/lambda/source/ecr_enhanced_scanning_finding_notice.py"
  output_path = "${path.module}/lambda/bin/ecr_enhanced_scanning_finding_notice.zip"
}

// Lambda 
resource "aws_lambda_function" "ecr_enhanced_scanning_finding" {
  filename                       = data.archive_file.ecr_enhanced_scanning_finding.output_path
  function_name                  = "${var.common.project}-${var.common.environment}-ecr-enhanced-scan-finding-notice-function"
  description                    = "${var.common.project}-${var.common.environment}-ecr-enhanced-scan-finding-notice-function"
  role                           = aws_iam_role.lambda_role.arn
  handler                        = "ecr_enhanced_scanning_finding_notice.lambda_handler"
  source_code_hash               = data.archive_file.ecr_enhanced_scanning_finding.output_base64sha256
  reserved_concurrent_executions = -1
  runtime                        = "python3.7"
  environment {
    variables = {
      channelName         = var.channel_name
      kmsEncryptedHookUrl = var.kms_encrypted_hookurl
    }
  }

  lifecycle {
    ignore_changes = [
      environment
    ]
  }

}

// SNS
resource "aws_lambda_permission" "ecr_enhanced_scanning_finding" {
  statement_id  = "AllowExecutionFromSNS"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.ecr_enhanced_scanning_finding.function_name
  principal     = "sns.amazonaws.com"
  source_arn    = aws_sns_topic.ecr_enhanced_scanning_finding.arn
}

// SNS Subscription
resource "aws_sns_topic_subscription" "ecr_enhanced_scanning_finding" {
  topic_arn = aws_sns_topic.ecr_enhanced_scanning_finding.arn
  protocol  = "lambda"
  endpoint  = aws_lambda_function.ecr_enhanced_scanning_finding.arn
}

Lambda

Lambda のソースコード ( Python ) は、以下のようなディレクトリ構成で配置します。

.
├── eventbridge.tf
├── lambda
│   ├── bin
│   │   └── ecr_enhanced_scanning_finding_notice.zip
│   └── source
│       └── ecr_enhanced_scanning_finding_notice.py
├── lambda.tf
└── variables.tf

現状、Terraform には環境変数を KMS キーで暗号化する機能が見当たりませんでした。
そのため、手動で暗号化するか、暗号化しないかのどちらでも対応できるように実装しておきます。
import boto3
import json
import logging
import os

from base64 import b64decode
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
from urllib.parse import quote

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):

    CHANNEL_NAME = os.environ['channelName']

    if  "hooks.slack.com" in os.environ['kmsEncryptedHookUrl']:
        logger.info("kmsEncryptedHookUrl: " + str(os.environ['kmsEncryptedHookUrl']))
        logger.info("kmsEncryptedHookUrl is not Encrypted")
        UNENCRYPTED_HOOK_URL = os.environ['kmsEncryptedHookUrl']
        HOOK_URL = "https://" + quote(UNENCRYPTED_HOOK_URL)
    else:
        logger.info("kmsEncryptedHookUrl is Encrypted")
        logger.info("kmsEncryptedHookUrl: " + str(os.environ['kmsEncryptedHookUrl']))
        ENCRYPTED_HOOK_URL = os.environ['kmsEncryptedHookUrl']
        HOOK_URL = "https://" + boto3.client('kms').decrypt(
            CiphertextBlob=b64decode(ENCRYPTED_HOOK_URL),
            EncryptionContext={'LambdaFunctionName': os.environ['AWS_LAMBDA_FUNCTION_NAME']}
        )['Plaintext'].decode('utf-8')

    logger.info("Event: " + str(event))
    message = json.loads(event['Records'][0]['Sns']['Message'])
    logger.info("Message: " + str(message))

    resources = message['resources'][0]
    title = message['title']
    severity = message['severity']
    inspector_score = message['inspectorScore']
    source_url = message['sourceUrl']

    # MEMO: <!channel>, <@user_id>
    slack_message = {
        "channel": CHANNEL_NAME,
        "icon_emoji": ":rotating_light:",
        "attachments": [
            {
                "color": "#FF0000",
                "title": "ECR イメージスキャン結果に脆弱性が存在します",
                "title_link": "https://ap-northeast-1.console.aws.amazon.com/ecr/repositories?region=ap-northeast-1",
                "text": "<!here> \n *Resources* : %s \n" % (resources),
                "fields": [
                    {
                        "title": "Title",
                        "value": title,
                        "short": True
                    },
                    {
                        "title": "Severity",
                        "value": severity,
                        "short": True
                    },
                    {
                        "title": "Inspector_score",
                        "value": inspector_score,
                        "short": True
                    },
                    {
                        "title": "Source_url",
                        "value": source_url,
                        "short": True
                    }
                ]

            }
        ]
    }

    logger.info("HOOK_URL: " + str(HOOK_URL))
    logger.info("CHANNEL_NAME: " + str(CHANNEL_NAME))

    req = Request(HOOK_URL, json.dumps(slack_message).encode('utf-8'))

    try:
        response = urlopen(req)
        response.read()
        logger.info("Message posted to %s", slack_message['channel'])
    except HTTPError as e:
        logger.error("Request failed: %d %s", e.code, e.reason)
    except URLError as e:
        logger.error("Server connection failed: %s", e.reason)

Terraform ですが思っていたよりも Lambda 周りのデプロイツールとして優秀なんじゃないかと思いました。
わあい。