LoginSignup
0
1

既存AWS構成をTerraformでIaC化(CloudWatch, Lambda, IAM)

Posted at

はじめに

今回は前回の続きです。Lambda部分をTerraformでIaC化していきます。
前回ではVPC回りとAPI Gateway構成部分をTerraformでIaC化しました。以下がその記録です。

ちなみにIaC化する前提構成は前回と同じままです(以下図)。
image.png

今回の実施内容としては以下となります。

  • CloudWatchLogs管理をTerraform管理に変更
  • Lambdaで利用するIAMロールをTerraform管理に変更
  • LambdaをTerraform管理に変更

IaC化

CloudWatchLogs

Lambdaに関わるCloudWatchLogsのロググループだけでなく、本構成で必要なロググループをIaC化しました。

aws_pubsub_cw.tf
# #----------------------------------------
# # Authorizer Lambda Log Group
# #----------------------------------------
resource "aws_cloudwatch_log_group" "AuthForPubSub" {
  name              = "/aws/lambda/AuthForPubSub"
  retention_in_days = 90
  skip_destroy      = false
}

# #----------------------------------------
# # Fargate Task Run Lambda Log Group
# #----------------------------------------
resource "aws_cloudwatch_log_group" "deliverTopicToSNS_Test01" {
  name              = "/aws/lambda/deliverTopicToSNS_Test01"
  retention_in_days = 90
  skip_destroy      = false
}

# #----------------------------------------
# # Subscriber Lambda Log Group
# #----------------------------------------
resource "aws_cloudwatch_log_group" "subscribeTopicFromSNS_Test01" {
  name              = "/aws/lambda/subscribeTopicFromSNS_Test01"
  retention_in_days = 90
  skip_destroy      = false
}

# #----------------------------------------
# # DynamoDB Stream Triggered Lambda Log Group
# #----------------------------------------
resource "aws_cloudwatch_log_group" "getDataFromDynamodbStream" {
  name              = "/aws/lambda/getDataFromDynamodbStream"
  retention_in_days = 90
  skip_destroy      = false
}

# #----------------------------------------
# # Fargate Publisher Log Group
# #----------------------------------------
resource "aws_cloudwatch_log_group" "task-pubsub-pub" {
  name              = "/ecs/task-pubsub-pub"
  retention_in_days = 90
  skip_destroy      = false
}

IAM

続いてIAMのPolicyとRoleの設定部分をIaC管理できるようにしていきます。
もともとはインラインポリシーとして利用していたLambdaRunEcsTaskPolicy,PutItemToSnsSqsLambdaTableをカスタマーマネージドポリシーとして作成し、それらのポリシーも含めてCustom_Lambda_Basic_Execution_Roleにアタッチしています。

aws_pubsub_iam.tf
# #----------------------------------------
# # IAM Policy for Lambda Execution Role
# #----------------------------------------
resource "aws_iam_policy" "LambdaRunEcsTaskPolicy" {
  name        = "LambdaRunEcsTaskPolicy"
  path        = "/"
  description = "Allow Lambda to run ECS Task"

  policy = jsonencode({
    "Version" : "2012-10-17",
    "Statement" : [
      {
        "Sid" : "VisualEditor0",
        "Effect" : "Allow",
        "Action" : [
          "iam:PassRole",
          "ecs:RunTask"
        ],
        "Resource" : "*"
      }
    ]
  })
}

resource "aws_iam_policy" "PutItemToSnsSqsLambdaTable" {
  name        = "PutItemToSnsSqsLambdaTable"
  path        = "/"
  description = "Allow Lambda to putItem to DynamoDB sns_sqs_lambda_table"

  policy = jsonencode({
    "Version" : "2012-10-17",
    "Statement" : [
      {
        "Sid" : "VisualEditor0",
        "Effect" : "Allow",
        "Action" : "dynamodb:PutItem",
        "Resource" : "arn:aws:dynamodb:ap-northeast-1:<アカウントID>:table/sns_sqs_lambda_table*"
      }
    ]
  })
}

# #----------------------------------------
# # IAM Role: Lambda Execution Role
# #----------------------------------------
resource "aws_iam_role" "Custom_Lambda_Basic_Execution_Role" {
  name = "Custom_Lambda_Basic_Execution_Role"
  assume_role_policy = jsonencode(
    {
      Statement = [
        {
          Action = "sts:AssumeRole"
          Effect = "Allow"
          Principal = {
            Service = "lambda.amazonaws.com"
          }
        },
      ]
      Version = "2012-10-17"
    }
  )
  description = "Lambda Role for PubSub Application"
  managed_policy_arns = [
    "arn:aws:iam::aws:policy/AWSLambdaExecute",
    "arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess",
    "arn:aws:iam::aws:policy/AmazonSNSFullAccess",
    "arn:aws:iam::aws:policy/CloudWatchFullAccess",
    "arn:aws:iam::aws:policy/service-role/AWSLambdaSQSQueueExecutionRole",
    aws_iam_policy.LambdaRunEcsTaskPolicy.arn,
    aws_iam_policy.PutItemToSnsSqsLambdaTable.arn
  ]
}

Lambda

続いてLambdaです。これまではコンソールにコードを直接書いてデプロイしてましたが、今回のIaC化に合わせて、zipファイルを作成してzipファイルをデプロイする方式に変更しました。
LambdaをTerraformでIaC化する際の大まかな手順イメージは以下のようになるかと思います。

  1. data "archive_file"でzipファイルを作成
  2. Lambdaコードをデプロイ元の環境に転記(今回の場合はコンソール⇒Cloud9へ転記)
  3. resource "aws_lambda_function"でzipファイルをデプロイするリソースを作成
  4. 必要に応じて不要な部分を削除(既存のdataリソース部分など)

一つのファイルにまとめて記載しています。

aws_pubsub_lambda.tf
# #----------------------------------------
# # Authorizer Lambda Function
# #----------------------------------------
# data "aws_lambda_function" "AuthForPubSub" {
#   function_name = "AuthForPubSub"
# }

# #----------------------------------------
# # Publisher Lambda Function
# #----------------------------------------
# data "aws_lambda_function" "deliverTopicToSNS_Test01" {
#   function_name = "deliverTopicToSNS_Test01"
# }

# #----------------------------------------
# # Create zip file
# #----------------------------------------
data "archive_file" "AuthForPubSub" {
  type        = "zip"
  source_file = "lambda_functions/src/auth_for_pubsub.py"
  output_path = "lambda_functions/output/auth_for_pubsub.zip"
}

data "archive_file" "deliverTopicToSNS_Test01" {
  type        = "zip"
  source_file = "lambda_functions/src/deliver_topic_sns.py"
  output_path = "lambda_functions/output/deliver_topic_sns.zip"
}

data "archive_file" "subscribeTopicFromSNS_Test01" {
  type        = "zip"
  source_file = "lambda_functions/src/subscribe_topic_sns.py"
  output_path = "lambda_functions/output/subscribe_topic_sns.zip"
}

data "archive_file" "getDataFromDynamodbStream" {
  type        = "zip"
  source_file = "lambda_functions/src/get_data_from_dynamodb_stream.py"
  output_path = "lambda_functions/output/get_data_from_dynamodb_stream.zip"
}

# #----------------------------------------
# # Deply Lambda Configuration
# #----------------------------------------
resource "aws_lambda_function" "AuthForPubSub" {
  filename         = data.archive_file.AuthForPubSub.output_path
  function_name    = "AuthForPubSub"
  handler          = "auth_for_pubsub.lambda_handler"
  description      = "Authorizer Lambda to check header token."
  role             = aws_iam_role.Custom_Lambda_Basic_Execution_Role.arn
  runtime          = "python3.8"
  memory_size      = 128
  timeout          = 5
  source_code_hash = data.archive_file.AuthForPubSub.output_base64sha256
  tags = {
    "lambda-console:blueprint" = "api-gateway-authorizer-python"
  }
}

resource "aws_lambda_function" "deliverTopicToSNS_Test01" {
  filename         = data.archive_file.deliverTopicToSNS_Test01.output_path
  function_name    = "deliverTopicToSNS_Test01"
  handler          = "deliver_topic_sns.lambda_handler"
  description      = "run ECS fargate task to deliver sns topic."
  role             = aws_iam_role.Custom_Lambda_Basic_Execution_Role.arn
  runtime          = "python3.8"
  memory_size      = 128
  timeout          = 5
  source_code_hash = data.archive_file.deliverTopicToSNS_Test01.output_base64sha256
  environment {
    variables = {
      "ECS_CLUSTER"     = "pubsub-cluster-pub"
      "SUBNET_ID_1"     = "subnet-0a8e33a542ddb7443"
      "SUBNET_ID_2"     = "subnet-02ba1bf978801ab20"
      "TASK_DEFINITION" = "task-pubsub-pub:2"
    }
  }
}

resource "aws_lambda_function" "subscribeTopicFromSNS_Test01" {
  filename         = data.archive_file.subscribeTopicFromSNS_Test01.output_path
  function_name    = "subscribeTopicFromSNS_Test01"
  handler          = "subscribe_topic_sns.lambda_handler"
  description      = "subscribe message from sqs and put new item into DynamoDB."
  role             = aws_iam_role.Custom_Lambda_Basic_Execution_Role.arn
  runtime          = "python3.8"
  memory_size      = 128
  timeout          = 5
  source_code_hash = data.archive_file.subscribeTopicFromSNS_Test01.output_base64sha256
}

resource "aws_lambda_function" "getDataFromDynamodbStream" {
  filename         = data.archive_file.getDataFromDynamodbStream.output_path
  function_name    = "getDataFromDynamodbStream"
  handler          = "get_data_from_dynamodb_stream.lambda_handler"
  description      = "get new item info from dynamodb stream and publish a new sns topic for email."
  role             = aws_iam_role.Custom_Lambda_Basic_Execution_Role.arn
  runtime          = "python3.8"
  memory_size      = 128
  timeout          = 5
  source_code_hash = data.archive_file.getDataFromDynamodbStream.output_base64sha256
}

まずは# Create zip fileのセクションでデプロイするzipファイルを作成する内容を記載しています。zip化される実際の関数コードについては別ファイルに記載(転記)してます(ソースコードは以下に載せてます)。
次にデプロイするLambda関数の設定などを記載する# Deploy Lambda ConfigurationセクションでLambdaの実行時の設定などを記載しています。
また、前回までのIaC化ではLambda関数は参照部分のみTerraformで記載してたので、data "aws_lambda_function"を使っていましたが、今回でLambda自身もTerraformで管理するように変更したので、該当部分(コード一番上部分)はコメントアウトしています。

以下がLambda関数のコードを記載しているファイルになります。lambda_functions/src配下としました。
image.png

例えばauth_for_pubsub.pyの中身は以下のようになっています。

auth_for_pubsub.py
import base64
import json
from logging import getLogger, INFO

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

def lambda_handler(event, context):
    print("============ output event =================")
    logger.info(json.dumps(event))
    
    token = event['headers']['Authorization']
    effect = 'Deny'
    if token == 'abc':
        effect = 'Allow'
        
    context = {
        "overwrite_message": "this is override message from authorizer."
    }
    
    json_context = json.dumps(context)
    base64_context = base64.b64encode(json_context.encode('utf8'))
        
    return_policy = {
        'principalId': '*',
        'policyDocument': {
            'Version': '2012-10-17',
            'Statement': [
                {
                    'Action': '*',
                    'Effect': effect,
                    'Resource':  event['methodArn']
                }    
            ]
        },
        'context': {
            'overwrite_message': base64_context.decode('utf-8')
        }
        
    }
    
    logger.info(json.dumps(return_policy))
    return return_policy

他のpythonファイルについてもgithubにアップ予定ですので、興味あるかたはそちらを参照してください。アップが完了したら記事の最下部にリンクを足しておきます。

そして、前回まではAPI GatewayのTerraform記載部分でdata.aws_lambda_functionとしてLambda関数を紐づけていましたが、今回Lambda関数もTerraform管理としたので、該当部分をaws_lambda_functionとしTerraformリソースを参照するように修正しています。

aws_pubsub_apigw.tf
# (一部抜粋)
resource "aws_api_gateway_authorizer" "pubsubApi" {
  name        = "PubSubLambdaAuthorizer"
  rest_api_id = aws_api_gateway_rest_api.pubsubApi.id
  # authorizer_uri                   = data.aws_lambda_function.AuthForPubSub.invoke_arn
  authorizer_uri                   = aws_lambda_function.AuthForPubSub.invoke_arn
  authorizer_result_ttl_in_seconds = 0
  type                             = "REQUEST"
}

...

resource "aws_api_gateway_integration" "pubsubApi" {
  rest_api_id = aws_api_gateway_rest_api.pubsubApi.id
  resource_id = aws_api_gateway_resource.pubsubApi.id
  http_method = "POST"
  type        = "AWS_PROXY"
  # uri                     = data.aws_lambda_function.deliverTopicToSNS_Test01.invoke_arn
  uri                     = aws_lambda_function.deliverTopicToSNS_Test01.invoke_arn
  content_handling        = "CONVERT_TO_TEXT"
  integration_http_method = "POST"
}

以上で今回やろうとしていたIaC化部分は完了しました。

おわりに

Lambda関数をTerraformで管理する場合は環境内にzipを作成する手法(今回はこちらを採用)とS3へzipをアップロードしてそこからデプロイする手法が主に考えられると思います。実際に複数人でより多くのLambda関数を開発・管理していく場合はS3を経由する方法が現実的かもしれません。
また、Lambda関数Terraform化する際に、Lambdaで間接的に利用するAWSリソース(CWログとかIAMとか)を先にTerraformで記載しておいた方が、Lambda自身をTerraformで記載する際にスムーズになるとも思いました。
次回以降、まだIaC化できていない他の部分を実施していきたいと思います。

参考サイト

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