はじめに
CloudFormation も Terraform も素晴らしい IaC の実行環境だが、それぞれの間でリソース情報をやり取りしようとすると上手くいかないため、IaC やデプロイの設計が難しくなる。
ということで、設計をシンプルにするために Terraform から CloudFormation に対してリソース情報を渡す方法を検討した。
なお、サンプルは分かりやすくするために CloudFormation を使っているが、そもそも CloudFormation でやれることは Terraform でも大体やれるので、あまり意味はない。実際の利用シーンとしては、SAM で CI/CD をする場合にやむを得ず CloudFormation を使うということを想定してもらいたい。
やってみること
以下のことをやってみる。
- Terraform で S3 バケットと、この後出てくる CloudFormation で作成する Lambda 関数に付与する IAM ロールを作成する
- CloudFormation で、↑で作った S3 バケットから GetObject してきてファイルの中身をログ出力する Lambda 関数を作る
- ついでに、もう一度 Terraform に戻って、↑で作成した Lambda のイベントソースマッピングを作る
CloudFormation に情報を渡す準備
以下のようにリソースを準備する。
test_object.txt にはテキトーな中身を書いておこう。
IAM のポリシーで dynamodb:*
を付与しているのは、最後のフェーズで使うためだ。
最小権限の原則からするとよろしくないので、もう少し絞った方が良いが、今回の趣旨ではないので割愛する。
################################################################################
# S3 Bucket #
################################################################################
resource "aws_s3_bucket" "test" {
bucket = local.bucket_name
acl = "private"
}
resource "aws_s3_bucket_object" "object" {
bucket = aws_s3_bucket.test.id
key = "test_object"
source = "${path.module}/test_object.txt"
etag = filemd5("${path.module}/test_object.txt")
}
################################################################################
# IAM Role #
################################################################################
resource "aws_iam_role" "lambda" {
name = local.lambda_role_name
assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
}
data "aws_iam_policy_document" "lambda_assume" {
statement {
effect = "Allow"
actions = [
"sts:AssumeRole",
]
principals {
type = "Service"
identifiers = [
"lambda.amazonaws.com",
]
}
}
}
resource "aws_iam_role_policy_attachment" "lambda" {
role = aws_iam_role.lambda.name
policy_arn = aws_iam_policy.lambda_custom.arn
}
resource "aws_iam_policy" "lambda_custom" {
name = local.lambda_policy_name
policy = data.aws_iam_policy_document.lambda_custom.json
}
data "aws_iam_policy_document" "lambda_custom" {
statement {
effect = "Allow"
actions = [
"s3:GetObject",
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"dynamodb:*",
]
resources = [
"*",
]
}
}
CloudFormation にリソース情報を渡す
さて、どうやって実現するかと言うと、「CloudFormation の Outputs を利用する」だ。シンプル。
ただし、CloudFormation でリソースなしのスタックを作ることができないので、テキトーに S3 バケット等のダミーのリソースを作ってやればいい。
################################################################################
# CloudFormation(Terraform Resource Export) #
################################################################################
resource "aws_cloudformation_stack" "terraform_export" {
name = local.terraform_export_stack_name
template_body = data.template_file.terraform_export.rendered
}
data "template_file" "terraform_export" {
template = "${file("${path.module}/cloudformation_template_terraform_export.yml")}"
vars = {
iam_role_arn = aws_iam_role.lambda.arn
s3_bucket_arn = aws_s3_bucket.test.arn
dummy_bucket_name = local.dummy_bucket_name
}
}
AWSTemplateFormatVersion: "2010-09-09"
Description:
Export Terraform resource information
Resources:
# ------------------------------------------------------------#
# Dummy Resource
# ------------------------------------------------------------#
DummyBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: ${dummy_bucket_name}
Outputs:
LambdaRoleARN:
Description: IAM Role ARN created by Terraform
Value: ${iam_role_arn}
Export:
Name: LambdaRoleARN
S3BucketARN:
Description: S3 Bucket ARN created by Terraform
Value: ${s3_bucket_arn}
Export:
Name: S3BucketARN
これで、terraform apply
すると、
ちゃんとエクスポートできてる!
エクスポートされたリソースを使ってみる
次に、以下のようなスタックを作ってインポートを試してみよう。
depends_on
を設定しているのは、Terraform 内でのリソース連携、CloudFormation 内でのリソース連携であれば依存関係の待ち合わせをしてくれるが、CloudFormation のエクスポートは Terraform 側では意識できない。エクスポートが終わる前にインポートを使用とするとエラーになってしまうのを避けるために、依存関係をつくっておく。
################################################################################
# CloudFormation(Terraform Resource Import) #
################################################################################
resource "aws_cloudformation_stack" "cfn_import" {
depends_on = [aws_cloudformation_stack.terraform_export]
name = local.cfn_import_stack_name
template_body = data.template_file.cfn_import.rendered
}
data "template_file" "cfn_import" {
template = "${file("${path.module}/cloudformation_template_cfn_import.yml")}"
vars = {
lambda_function_name = local.lambda_function_name
s3_bucket_name = aws_s3_bucket.test.id
}
}
AWSTemplateFormatVersion: "2010-09-09"
Description:
Export Terraform resource information
Resources:
LambdaFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: ${lambda_function_name}
Role: !ImportValue LambdaRoleARN
MemorySize: 128
Runtime: python3.7
Handler: "index.handler"
Code:
ZipFile: |
import boto3
s3 = boto3.client('s3')
def handler(event, context):
body = s3.get_object(Bucket="${s3_bucket_name}",Key="test_object")['Body'].read()
print(body.decode('utf-8'))
return {
'statusCode': 200
}
Outputs:
LambdaFunctionArn:
Description: LambdaFunction name created by CloudFormation
Value: !GetAtt LambdaFunction.Arn
Export:
Name: LambdaFunctionARN
これでLambdaを実行すると、
標準出力に S3 バケットのオブジェクトの中身が表示された!
CloudFormation のリソースを Terraform から参照する
↑の CloudFormation テンプレートで作成した Lambda 関数に Terraform でイベントソースマッピングを設定する。Lambda 関数の ARN が必要になるので、CloudFormation の Outputs を活用する(実際には、Lambda 関数の ARN は事前に推測可能なので、無理に Outputs を読まなくても実現は可能である)。
CloudFormation の Outputs については、cloudformation_export のデータソースがあるので、これを使うのがシンプルで良い。
なお、ここも、CloudFormation のスタックが完成していないとエクスポートした情報を参照できないため、depends_on
でスタックが出来上がるのを待ち合わせする。
################################################################################
# Lambda Event Source Mapping #
################################################################################
resource "aws_lambda_event_source_mapping" "terraform_import" {
depends_on = [aws_cloudformation_stack.cfn_import]
event_source_arn = aws_dynamodb_table.dummy.stream_arn
function_name = data.aws_cloudformation_export.lambda_function_arn.value
starting_position = "LATEST"
}
resource "aws_dynamodb_table" "dummy" {
name = local.dynamodb_table_name
billing_mode = "PAY_PER_REQUEST"
stream_enabled = true
stream_view_type = "NEW_AND_OLD_IMAGES"
hash_key = "id"
attribute {
name = "id"
type = "S"
}
}
data "aws_cloudformation_export" "lambda_function_arn" {
depends_on = [aws_cloudformation_stack.cfn_import]
name = "LambdaFunctionARN"
}
これで、terraform apply
すると、
無事、Terraform から CloudFormation で作成した Lambda 関数に対してイベントソースを設定できた!