LoginSignup
1
0

More than 3 years have passed since last update.

TerraformとCloudFormation間でリソース情報を連携する

Posted at

はじめに

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
  }
}
cloudformation_template_terraform_export.yml
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すると、

キャプチャ1.png

ちゃんとエクスポートできてる!

エクスポートされたリソースを使ってみる

次に、以下のようなスタックを作ってインポートを試してみよう。
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
  }
}
cloudformation_template_cfn_import.yml
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を実行すると、

image.png

標準出力に 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すると、

image.png

無事、Terraform から CloudFormation で作成した Lambda 関数に対してイベントソースを設定できた!

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