Terraformで構築したリソースの情報をAWS SAMに引き渡す

# はじめに
本記事では、`Terraform`で構築したリソースの情報を`AWS SAM テンプレート`に引き渡す方法について解説しています。

VPCやサブネット、セキュリティグループなどのネットワーク関連のリソースは`Terraform`で管理し、Lambda関数は`AWS SAM`で管理するケースをサンプルコードで説明しています。

# 本ケースの全体構成
![terraform_resource_export.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/c2c5d7f2-a251-b776-14a1-1a0c801f09e0.png)

# 本ケースのサンプルコード

https://github.com/okubo-t/aws-tf-resource-export-sample.git

### ディレクトリ構成
```terminal
aws-tf-resource-export-sample
├── main.tf
├── provider.tf
├── src
│   └── lambda_function.py
├── template.yaml
└── tf-resource-export.yml
```

### Terraform側のコード
`aws_cloudformation_stack` を使って、リソースの情報（ここでは、`SunbetId`と`SecurityGroupId`）を、CloudFormation のテンプレートのOutputs セクションに組み込んだStackを構築するようにしています。
```terraform:main.tf
#
# リソースの情報をCloudformationのOutputs機能を使ってエクスポートする
# このStackでTerraformのバックエンド用のリソースを作成する
#
resource "aws_cloudformation_stack" "this" {
  name = "tf-resource-export-sample"

  template_body = templatefile("./tf-resource-export.yml", {

    # tfstateのリソース作成用
    tfstate_backend = {
      s3_bucket_name          = "tf-resource-export-sample"
      dynamodb_for_state_lock = "tf-resource-export-sample"
    },

    # サブネット情報のOutputs用
    network = {
      private_subnet_1a_name = "PrivateSubnet1a"
      private_subnet_1a_id   = module.network.private_subnets[0]
      private_subnet_1c_name = "PrivateSubnet1c"
      private_subnet_1c_id   = module.network.private_subnets[1]
    },

    # セキュリティグループ情報のOutputs用
    security_group = {
      name_for_lambda = "HelloWorldFunctionSG"
      id_for_lambda   = aws_security_group.this.id
    }
  })
}

#
# Terraformで構築するリソース
#
*** 以下、省略 ***
```

**CloudFormation のテンプレート**
```yaml:tf-resource-export.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: "Terraform Resource info Export Template"

Resources:
  TfStateBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain
    Properties:
      BucketName: ${tfstate_backend["s3_bucket_name"]}
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

  TfStateLockTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: ${tfstate_backend["dynamodb_for_state_lock"]}
      ProvisionedThroughput:
        ReadCapacityUnits: 1
        WriteCapacityUnits: 1
      AttributeDefinitions:
        - AttributeName: "LockID"
          AttributeType: "S"
      KeySchema:
        - AttributeName: "LockID"
          KeyType: "HASH"

#
# Terraform で構築したリソースの情報をこのOutputsセクションに組み込みます。
#
Outputs:
  PrivateSubnet1a:
    Description: "Id for PrivateSubnet 1a"
    Export:
      Name: ${network["private_subnet_1a_name"]}
    Value: ${network["private_subnet_1a_id"]}

  PrivateSubnet1c:
    Description: "Id for PrivateSubnet 1c"
    Export:
      Name: ${network["private_subnet_1c_name"]}
    Value: ${network["private_subnet_1c_id"]}

  HelloWorldFunctionSG:
    Description: "Security Group for HelloWorldFunction"
    Export:
      Name: ${security_group["name_for_lambda"]}
    Value: ${security_group["id_for_lambda"]}
```
※CloudFormationのテンプレートは、`Resources` が必須で、Outputs のみを記載することができないため、このサンプルコードでは、`Resources` にTerraformのtfstateを管理するためのバックエンド用リソースを構築するようにしています。

### AWS SAM側のコード
`Fn::ImportValue` 組み込み関数 を使って、Terraform側で構築したStackのOutputsの値を`クロススタック参照`するようにAWS SAM Template に組み込みます。

```yaml:template.yaml
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: "Lambda Function SAM Template"

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: HelloWorldFunction
      Handler: lambda_function.lambda_handler
      Runtime: python3.9
      CodeUri: ./src/
      Timeout: 10
      MemorySize: 128
#
# Fn::ImportValue 組み込み関数で値をインポートします。
#
      VpcConfig:
        SecurityGroupIds:
          - !ImportValue HelloWorldFunctionSG
        SubnetIds:
          - !ImportValue PrivateSubnet1a
          - !ImportValue PrivateSubnet1c
      Policies:
        - AWSLambdaVPCAccessExecutionRole
        - AWSLambdaBasicExecutionRole

Outputs:
  HelloWorldFunction:
    Description: "HelloWorldFunction ARN"
    Value: !GetAtt HelloWorldFunction.Arn

```
これで、Terraformで構築したリソースの情報をAWS SAMに引き渡すことができるようになります。

# 本ケースのデプロイ手順
実際に、`Terraform`と`AWS SAM`を使って、本ケースをデプロイする手順について、記載します。
## デプロイ手順確認環境
```terminal
$ terraform -version
Terraform v1.4.4
on darwin_amd64
+ provider registry.terraform.io/hashicorp/aws v4.62.0

$ sam --version
SAM CLI, version 1.79.0
```

## Terraformの実行
### リソースの構築
Terraform でネットワーク関連のリソースとその情報をOutputs セクションに組み込んだStackを構築します。
```terminal
$ export AWS_PROFILE= [ YOUR AWS ACCOUNT PROFILE NAME ]
```

```terraform
$ terraform init
Initializing the backend...
Successfully configured the backend "local"! Terraform will automatically

$ terraform plan
Plan: 9 to add, 0 to change, 0 to destroy.

$ terraform apply
Apply complete! Resources: 9 added, 0 changed, 0 destroyed.
```

### tfstateを管理するバックエンドの変更
下記のように`provider.tf`内のtfstateのバックエンド先を`LocalからS3`に修正します。
```terraform:provider.tf
  #backend "local" {
  #  path = "terraform.tfstate"
  #}

  backend "s3" {
    bucket         = "tf-resource-export-sample"
    key            = "terraform.tfstate"
    dynamodb_table = "tf-resource-export-sample"
    region         = "ap-northeast-1"
  }
```

下記コマンドを実行して、tfstateを管理するバックエンドを`LocalからS3`に変更します。
```terraform
$ terraform init -migrate-state

Initializing the backend...
Terraform detected that the backend type changed from "local" to "s3".

Do you want to copy existing state to the new backend?
  Pre-existing state was found while migrating the previous "local" backend to the
  newly configured "s3" backend. No existing state was found in the newly
  configured "s3" backend. Do you want to copy this state to the new "s3"
  backend? Enter "yes" to copy and "no" to start with an empty state.

  Enter a value: yes 
```

下記コマンドを実行して、tfstateが、S3に保存されていることを確認します。
```terminal
$ aws s3 ls s3://tf-resource-export-sample
9999-99-99 99:99:99      13870 terraform.tfstate
```

# AWS SAMの実行
### Lambda関数の構築
下記コマンドを実行して、AWS SAM TemplateでLambda関数をデプロイします。

```
sam deploy --template-file template.yaml \
           --stack-name tf-resource-import-sample \
           --resolve-s3 \
           --capabilities CAPABILITY_IAM \
           --region ap-northeast-1

CloudFormation stack changeset
---------------------------------------------------------------------------------------------------------------------------------
Operation                        LogicalResourceId                ResourceType                     Replacement                    
---------------------------------------------------------------------------------------------------------------------------------
+ Add                            HelloWorldFunctionRole           AWS::IAM::Role                   N/A                            
+ Add                            HelloWorldFunction               AWS::Lambda::Function            N/A                            
---------------------------------------------------------------------------------------------------------------------------------
```

デプロイできました。
```
CloudFormation outputs from deployed stack
---------------------------------------------------------------------------------------------------------------------------------
Outputs                                                                                                                         
---------------------------------------------------------------------------------------------------------------------------------
Key                 HelloWorldFunction                                                                                          
Description         HelloWorldFunction ARN                                                                                      
Value               arn:aws:lambda:ap-northeast-1:999999999999:function:HelloWorldFunction                                      
---------------------------------------------------------------------------------------------------------------------------------

Successfully created/updated stack - tf-resource-import-sample in ap-northeast-1
```
# 動作確認
下記コマンドで、デプロイしたLambda関数を実行します。
```
$ aws lambda invoke --function-name HelloWorldFunction /dev/stdout
{"statusCode": 200, "body": "{\"message\": \"Hello World!\"}"}
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
```

# クリーンアップ
### Lambda関数の削除
下記コマンドで、AWS SAM Template でデプロイしたLambda関数を削除します。
```
$ sam delete --stack-name tf-resource-import-sample
        Are you sure you want to delete the stack tf-resource-import-sample in the region ap-northeast-1 ? [y/N]: y
        Do you want to delete the template file 71b91735b453182a1c33df8bceda4df3.template in S3? [y/N]: y

Deleted successfully
```

### tfstateを管理するバックエンドの変更
下記のように`provider.tf`内のtfstateのバックエンド先を`S3からLocal`に修正します。
```terraform:provider.tf
  backend "local" {
    path = "terraform.tfstate"
  }

  #backend "s3" {
  #  bucket         = "tf-resource-export-sample"
  #  key            = "terraform.tfstate"
  #  dynamodb_table = "tf-resource-export-sample"
  #  region         = "ap-northeast-1"
  #}
```

下記コマンドを実行して、tfstateを管理するバックエンドを`S3からLocal`に変更します。
```terraform
$ terraform init -migrate-state -lock=false

Initializing the backend...
Terraform detected that the backend type changed from "s3" to "local".

Do you want to copy existing state to the new backend?
  Pre-existing state was found while migrating the previous "s3" backend to the
  newly configured "local" backend. No existing state was found in the newly
  configured "local" backend. Do you want to copy this state to the new "local"
  backend? Enter "yes" to copy and "no" to start with an empty state.

  Enter a value: yes
```

### リソースの削除
下記コマンドで、各リソースを削除します。
```terraform
$ terraform destroy
Destroy complete! Resources: 9 destroyed.
```

下記コマンドで、tfstate用のS3バケットを削除します。
```terminal
$ aws s3 rb s3://tf-resource-export-sample --force        
delete: s3://tf-resource-export-sample/terraform.tfstate
remove_bucket: tf-resource-export-sample
```
以上です。

# さいごに
こちらは、あくまで1例になります。参考になれば幸いです。