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

# はじめに
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:*``` を付与しているのは、最後のフェーズで使うためだ。
最小権限の原則からするとよろしくないので、もう少し絞った方が良いが、今回の趣旨ではないので割愛する。

```HCL
################################################################################
# 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 バケット等のダミーのリソースを作ってやればいい。

```HCL
################################################################################
# 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
  }
}
```

```YAML: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/0f2e0404-086b-d58e-ee8e-d24283f3a390.png)

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

# エクスポートされたリソースを使ってみる
次に、以下のようなスタックを作ってインポートを試してみよう。
```depends_on``` を設定しているのは、Terraform 内でのリソース連携、CloudFormation 内でのリソース連携であれば依存関係の待ち合わせをしてくれるが、CloudFormation のエクスポートは Terraform 側では意識できない。エクスポートが終わる前にインポートを使用とするとエラーになってしまうのを避けるために、依存関係をつくっておく。

```HCL
################################################################################
# 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
  }
}
```

```YAML: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/e9a771a6-a15d-8faa-aaed-747ea98f4150.png)

標準出力に S3 バケットのオブジェクトの中身が表示された！

# CloudFormation のリソースを Terraform から参照する
↑の CloudFormation テンプレートで作成した Lambda 関数に Terraform でイベントソースマッピングを設定する。Lambda 関数の ARN が必要になるので、CloudFormation の Outputs を活用する(実際には、Lambda 関数の ARN は事前に推測可能なので、無理に Outputs を読まなくても実現は可能である)。

CloudFormation の Outputs については、[cloudformation_export のデータソース](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/cloudformation_export)があるので、これを使うのがシンプルで良い。

なお、ここも、CloudFormation のスタックが完成していないとエクスポートした情報を参照できないため、```depends_on``` でスタックが出来上がるのを待ち合わせする。

```HCL
################################################################################
# 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/7a88e15f-eb36-b49c-f652-2e569fb46b02.png)

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