TerraformとAWS CDKの連携について考えてみる
2019-08-07

tl; dr

  • Terraformのlocal-execで cdk deploy
  • Terraform, AWS CDK間でのパラメーターの受け渡しできることが大切
  • DataソースでCDK Stackのoutputsを参照できる
  • Terraformからは環境変数を使ってCDK Stackに渡す
  • https://github.com/youyo/terraform-aws-cdk-template

モチベーション

以前TerraformからAWS SAMを扱う記事を書きました。

TerraformからAWS SAMをデプロイしてみた

そのときのモチベーションをベースに, さらにYAMLも書きたくないのでAWS SAMではなくAWS CDKを組み合わせられないか考えてみました。

  • 基本的にはTerraformを使いたい
  • でもTerraformでLambda扱うのとかしんどい
  • そこはAWS SAM使いたい
  • AWS SAMもいいけどYAML書きたくない
  • AWS CDK使いたい
  • TerraformからAWS CDK扱いたい

複数のツールを組み合わせる場合には, そのツール間で相互にデータのやりとりが出来るかどうかもポイントになるときもあります。(一方通行でもいいときもあるしケースバイケース)
そこも重視して検証してみました。

環境

コード全体はリポジトリに公開しています。

検証ログ

コード


.
├── Makefile
├── cfn
│   ├── app.py
│   ├── cdk.json
│   ├── function
│   │   └── src
│   │       ├── app.py
│   │       └── requirements.txt
│   └── requirements.txt
├── cfn.tf
└── provider.tf

cfn/app.py


from aws_cdk import (
    core,
    aws_lambda as lambda_
)
import os


class Stack(core.Stack):
    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        foo = os.getenv('FOO')

        f = lambda_.Function(
            self,
            'func',
            code=lambda_.Code.asset('./function/.artifact/'),
            handler='app.handler',
            runtime=lambda_.Runtime.PYTHON_3_7,
            # timeout=core.Duration.seconds(30)
        )
        f.add_environment('FOO', foo)

        core.CfnOutput(self, 'functionname', value=f.function_name)


def main():
    app = core.App()
    Stack(app, 'terraform-aws-cdk-template')
    app.synth()


if __name__ == '__main__':
    main()
  • os.getenv('FOO') で環境変数を参照し, Terraformからパラメーターを受け取るようにします。
  • f.add_environment('FOO', foo) でLambdaにも渡しています。
  • core.CfnOutput(self, 'functionname', value=f.function_name) でfunction nameをTerraformから参照できるようにしています。
  • code=lambda_.Code.asset('./function/.artifact/'), はデプロイ時に作成し, 外部ライブラリを含んだLambdaファンクションへ対応できるようにします。
    ref. AWS CDK Pythonで外部ライブラリを含むLambda Function(runtime: python3.7)をデプロイする

cfn.tf


resource "null_resource" "cdk_deploy" {
  provisioner "local-exec" {
    command     = "npx cdk deploy '*' --require-approval never"
    working_dir = "cfn/"
    environment = {
      FOO = "bar"
    }
  }
}


data "aws_cloudformation_stack" "cdk" {
  name = "terraform-aws-cdk-template"

  depends_on = [null_resource.cdk_deploy]
}

output "function_name" {
  value = data.aws_cloudformation_stack.cdk.outputs.functionname
}
  • environment = {FOO = "bar"} で環境変数で必要なパラメーターをTerraformからAWS CDKへと渡します。
  • data "aws_cloudformation_stack" "cdk" でCDK Stackを参照し,
    data.aws_cloudformation_stack.cdk.outputs.functionname でfunction nameを取得しています。
  • このときdataリソースは depends_on = [null_resource.cdk_deploy] で先に cdk deploy が実行されるようにしておきます。

やってみる

makeコマンドで実行しています。
実行内容の詳細は Makefile を見てください。
pip install のログは邪魔だったので削除

  • plan and diff

$ make plan
Success! The configuration is valid.

Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create
 <= read (data resources)

Terraform will perform the following actions:

  # data.aws_cloudformation_stack.cdk will be read during apply
  # (config refers to values not yet known)
 <= data "aws_cloudformation_stack" "cdk"  {
      + capabilities       = (known after apply)
      + description        = (known after apply)
      + disable_rollback   = (known after apply)
      + iam_role_arn       = (known after apply)
      + id                 = (known after apply)
      + name               = "terraform-aws-cdk-template"
      + notification_arns  = (known after apply)
      + outputs            = (known after apply)
      + parameters         = (known after apply)
      + tags               = (known after apply)
      + template_body      = (known after apply)
      + timeout_in_minutes = (known after apply)
    }

  # null_resource.cdk_deploy will be created
  + resource "null_resource" "cdk_deploy" {
      + id = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

Stack terraform-aws-cdk-template
The terraform-aws-cdk-template stack uses assets, which are currently not accounted for in the diff output! See https://github.com/aws/aws-cdk/issues/395
IAM Statement Changes
┌───┬─────────────────────────┬────────┬────────────────┬──────────────────────────────────┬───────────┐
│   │ Resource                │ Effect │ Action         │ Principal                        │ Condition │
├───┼─────────────────────────┼────────┼────────────────┼──────────────────────────────────┼───────────┤
│ + │ ${func/ServiceRole.Arn} │ Allow  │ sts:AssumeRole │ Service:lambda.${AWS::URLSuffix} │           │
└───┴─────────────────────────┴────────┴────────────────┴──────────────────────────────────┴───────────┘
IAM Policy Changes
┌───┬─────────────────────┬────────────────────────────────────────────────────────────────────────────────┐
│   │ ResourceManaged Policy ARN                                                             │
├───┼─────────────────────┼────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${func/ServiceRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole │
└───┴─────────────────────┴────────────────────────────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See http://bit.ly/cdk-2EhF7Np)

Parameters
[+] Parameter func/Code/S3Bucket funcCodeS3BucketD02BF0B4: {"Type":"String","Description":"S3 bucket for asset \"terraform-aws-cdk-template/func/Code\""}
[+] Parameter func/Code/S3VersionKey funcCodeS3VersionKeyCE453219: {"Type":"String","Description":"S3 key for asset version \"terraform-aws-cdk-template/func/Code\""}
[+] Parameter func/Code/ArtifactHash funcCodeArtifactHashF01F3509: {"Type":"String","Description":"Artifact hash for asset \"terraform-aws-cdk-template/func/Code\""}

Resources
[+] AWS::IAM::Role func/ServiceRole funcServiceRoleA96CCB44
[+] AWS::Lambda::Function func funcC3A0C2E2

Outputs
[+] Output functionname functionname: {"Value":{"Ref":"funcC3A0C2E2"}}

TerraformのplanとAWS CDKのdiffが確認できます。

  • apply

$ make apply
Success! The configuration is valid.

null_resource.cdk_deploy: Creating...
null_resource.cdk_deploy: Provisioning with 'local-exec'...
null_resource.cdk_deploy (local-exec): Executing: ["/bin/sh" "-c" "npx cdk deploy '*' --require-approval never"]
null_resource.cdk_deploy (local-exec): terraform-aws-cdk-template: deploying...
null_resource.cdk_deploy: Still creating... [10s elapsed]
null_resource.cdk_deploy (local-exec): Updated: asset.f1df5da6cc6b2b38af93d4491d22b57fdea5e71eb4f2e440bd7bbce0d4ebfffc (zip)
null_resource.cdk_deploy (local-exec): terraform-aws-cdk-template: creating CloudFormation changeset...
null_resource.cdk_deploy: Still creating... [20s elapsed]
null_resource.cdk_deploy (local-exec):  0/4 | 20:39:06 | CREATE_IN_PROGRESS   | AWS::CloudFormation::Stack | terraform-aws-cdk-template User Initiated
null_resource.cdk_deploy: Still creating... [30s elapsed]
null_resource.cdk_deploy: Still creating... [40s elapsed]
null_resource.cdk_deploy: Still creating... [50s elapsed]
null_resource.cdk_deploy (local-exec):  0/4 | 20:39:39 | CREATE_IN_PROGRESS   | AWS::IAM::Role        | func/ServiceRole (funcServiceRoleA96CCB44)
null_resource.cdk_deploy (local-exec):  0/4 | 20:39:39 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata    | CDKMetadata
null_resource.cdk_deploy (local-exec):  0/4 | 20:39:40 | CREATE_IN_PROGRESS   | AWS::IAM::Role        | func/ServiceRole (funcServiceRoleA96CCB44) Resource creation Initiated
null_resource.cdk_deploy (local-exec):  0/4 | 20:39:41 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata    | CDKMetadata Resource creation Initiated
null_resource.cdk_deploy (local-exec):  1/4 | 20:39:41 | CREATE_COMPLETE      | AWS::CDK::Metadata    | CDKMetadata
null_resource.cdk_deploy: Still creating... [1m0s elapsed]
null_resource.cdk_deploy (local-exec):  2/4 | 20:39:50 | CREATE_COMPLETE      | AWS::IAM::Role        | func/ServiceRole (funcServiceRoleA96CCB44)
null_resource.cdk_deploy: Still creating... [1m10s elapsed]
null_resource.cdk_deploy (local-exec):  2/4 | 20:39:53 | CREATE_IN_PROGRESS   | AWS::Lambda::Function | func (funcC3A0C2E2)
null_resource.cdk_deploy (local-exec):  2/4 | 20:39:54 | CREATE_IN_PROGRESS   | AWS::Lambda::Function | func (funcC3A0C2E2) Resource creation Initiated
null_resource.cdk_deploy (local-exec):  3/4 | 20:39:54 | CREATE_COMPLETE      | AWS::Lambda::Function | func (funcC3A0C2E2)
null_resource.cdk_deploy (local-exec):  4/4 | 20:39:56 | CREATE_COMPLETE      | AWS::CloudFormation::Stack | terraform-aws-cdk-template

null_resource.cdk_deploy (local-exec):  ✅  terraform-aws-cdk-template

null_resource.cdk_deploy (local-exec): Outputs:
null_resource.cdk_deploy (local-exec): terraform-aws-cdk-template.functionname = terraform-aws-cdk-template-funcC3A0C2E2-179GI6KHL6DFF

null_resource.cdk_deploy (local-exec): Stack ARN:
null_resource.cdk_deploy (local-exec): arn:aws:cloudformation:ap-northeast-1:000000000000:stack/terraform-aws-cdk-template/f32c7f00-b907-11e9-a245-0a534bae3094
null_resource.cdk_deploy: Creation complete after 1m12s [id=5911625897989636650]
data.aws_cloudformation_stack.cdk: Refreshing state...

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

function_name = terraform-aws-cdk-template-funcC3A0C2E2-179GI6KHL6DFF

AWS CDKが実行され, function nameをTerraform側で取得できています。

Lambdaファンクションを実行すると bar が出力されており, Terraform側からパラメーターが正しく渡っていることが確認できます。


$ aws lambda invoke --function-name terraform-aws-cdk-template-funcC3A0C2E2-179GI6KHL6DFF --payload {} --log-type Tail outfile.txt --query 'LogResult' | tr -d '"' | base64 -D
START RequestId: bad0be6d-fe25-4ffb-adb9-ab3b51530463 Version: $LATEST
bar
['ap-east-1', 'ap-northeast-1', 'ap-northeast-2', 'ap-south-1', 'ap-southeast-1', 'ap-southeast-2', 'ca-central-1', 'eu-central-1', 'eu-north-1', 'eu-west-1', 'eu-west-2', 'eu-west-3', 'me-south-1', 'sa-east-1', 'us-east-1', 'us-east-2', 'us-west-1', 'us-west-2']
END RequestId: bad0be6d-fe25-4ffb-adb9-ab3b51530463
REPORT RequestId: bad0be6d-fe25-4ffb-adb9-ab3b51530463	Duration: 495.74 ms	Billed Duration: 500 ms 	Memory Size: 128 MB	Max Memory Used: 77 MB
  • plan and diff

timeout=core.Duration.seconds(30) の設定をLambdaファンクションに加えて差分を見てみます。


$ make plan
Success! The configuration is valid.

Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

null_resource.cdk_deploy: Refreshing state... [id=5911625897989636650]

------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
 <= read (data resources)

Terraform will perform the following actions:

  # data.aws_cloudformation_stack.cdk will be read during apply
  # (config refers to values not yet known)
 <= data "aws_cloudformation_stack" "cdk"  {
      + capabilities       = (known after apply)
      + description        = (known after apply)
      + disable_rollback   = (known after apply)
      + iam_role_arn       = (known after apply)
      + id                 = (known after apply)
      + name               = "terraform-aws-cdk-template"
      + notification_arns  = (known after apply)
      + outputs            = (known after apply)
      + parameters         = (known after apply)
      + tags               = (known after apply)
      + template_body      = (known after apply)
      + timeout_in_minutes = (known after apply)
    }

Plan: 0 to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

Stack terraform-aws-cdk-template
The terraform-aws-cdk-template stack uses assets, which are currently not accounted for in the diff output! See https://github.com/aws/aws-cdk/issues/395
Resources
[~] AWS::Lambda::Function func funcC3A0C2E2
 ├─ [~] Environment
 │   └─ [~] .Variables:
 │       └─ [-] Removed: .FOO
 ├─ [+] Timeout
 │   └─ 30
 └─ [~] Metadata
     └─ [~] .aws:asset:path:
         ├─ [-] asset.f1df5da6cc6b2b38af93d4491d22b57fdea5e71eb4f2e440bd7bbce0d4ebfffc
         └─ [+] asset.1ae4cffe327fb5db611326ec7ae12343d7ea5b25828c1c5e5953c9d2bbf2bede

Timeout 30 が差分として表示されているのでOK.
( Environment.Variables.Removed: FOO が表示されるのがよくわからない...? 実際には削除されてません)

  • deploy

$ make deploy
Success! The configuration is valid.

null_resource.cdk_deploy: Refreshing state... [id=5911625897989636650]
null_resource.cdk_deploy: Destroying... [id=5911625897989636650]
null_resource.cdk_deploy: Destruction complete after 0s
null_resource.cdk_deploy: Creating...
null_resource.cdk_deploy: Provisioning with 'local-exec'...
null_resource.cdk_deploy (local-exec): Executing: ["/bin/sh" "-c" "npx cdk deploy '*' --require-approval never"]
null_resource.cdk_deploy (local-exec): terraform-aws-cdk-template: deploying...
null_resource.cdk_deploy: Still creating... [10s elapsed]
null_resource.cdk_deploy (local-exec): Updated: asset.a10850966a2f9dca8e968f4b12dbd2825fe975c0a9a23de09a829202bed6965e (zip)
null_resource.cdk_deploy (local-exec): terraform-aws-cdk-template: creating CloudFormation changeset...
null_resource.cdk_deploy: Still creating... [20s elapsed]
null_resource.cdk_deploy (local-exec):  0/2 | 20:44:08 | UPDATE_IN_PROGRESS   | AWS::CloudFormation::Stack | terraform-aws-cdk-template User Initiated
null_resource.cdk_deploy: Still creating... [30s elapsed]
null_resource.cdk_deploy: Still creating... [40s elapsed]
null_resource.cdk_deploy: Still creating... [50s elapsed]
null_resource.cdk_deploy (local-exec):  0/2 | 20:44:41 | UPDATE_IN_PROGRESS   | AWS::Lambda::Function | func (funcC3A0C2E2)
null_resource.cdk_deploy (local-exec):  1/2 | 20:44:42 | UPDATE_COMPLETE      | AWS::Lambda::Function | func (funcC3A0C2E2)
null_resource.cdk_deploy: Still creating... [1m0s elapsed]
null_resource.cdk_deploy (local-exec):  1/2 | 20:44:45 | UPDATE_COMPLETE_CLEA | AWS::CloudFormation::Stack | terraform-aws-cdk-template
null_resource.cdk_deploy (local-exec):  2/2 | 20:44:46 | UPDATE_COMPLETE      | AWS::CloudFormation::Stack | terraform-aws-cdk-template

null_resource.cdk_deploy (local-exec):  ✅  terraform-aws-cdk-template

null_resource.cdk_deploy (local-exec): Outputs:
null_resource.cdk_deploy (local-exec): terraform-aws-cdk-template.functionname = terraform-aws-cdk-template-funcC3A0C2E2-179GI6KHL6DFF

null_resource.cdk_deploy (local-exec): Stack ARN:
null_resource.cdk_deploy (local-exec): arn:aws:cloudformation:ap-northeast-1:000000000000:stack/terraform-aws-cdk-template/f32c7f00-b907-11e9-a245-0a534bae3094
null_resource.cdk_deploy: Creation complete after 1m3s [id=8444902881669111358]
data.aws_cloudformation_stack.cdk: Refreshing state...

Apply complete! Resources: 1 added, 0 changed, 1 destroyed.

Outputs:

function_name = terraform-aws-cdk-template-funcC3A0C2E2-179GI6KHL6DFF

Timeoutが30secになっていることも確認できました。


$ aws lambda get-function-configuration --function-name terraform-aws-cdk-template-funcC3A0C2E2-179GI6KHL6DFF --query 'Timeout'
30
  • destroy

$ make destroy
terraform-aws-cdk-template: destroying...
   0 | 20:46:31 | DELETE_IN_PROGRESS   | AWS::CloudFormation::Stack | terraform-aws-cdk-template User Initiated
   0 | 20:46:32 | DELETE_IN_PROGRESS   | AWS::CDK::Metadata    | CDKMetadata
   0 | 20:46:32 | DELETE_IN_PROGRESS   | AWS::Lambda::Function | func (funcC3A0C2E2)
   1 | 20:46:33 | DELETE_COMPLETE      | AWS::Lambda::Function | func (funcC3A0C2E2)
   2 | 20:46:33 | DELETE_COMPLETE      | AWS::CDK::Metadata    | CDKMetadata
   2 | 20:46:34 | DELETE_IN_PROGRESS   | AWS::IAM::Role        | func/ServiceRole (funcServiceRoleA96CCB44)
   3 | 20:46:35 | DELETE_COMPLETE      | AWS::IAM::Role        | func/ServiceRole (funcServiceRoleA96CCB44)

 ✅  terraform-aws-cdk-template: destroyed
null_resource.cdk_deploy: Refreshing state... [id=8444902881669111358]
null_resource.cdk_deploy: Destroying... [id=8444902881669111358]
null_resource.cdk_deploy: Destruction complete after 0s

Destroy complete! Resources: 1 destroyed.

まとめ

  • Terraformのlocal-execで cdk deploy しても, dataリソースでoutputsを参照できる
  • AWS CDKには環境変数でパラメーター渡せる
  • ParameterStoreやSecretsManager使ってもいい
  • Terraform, AWS CDK間でのパラメーターの受け渡しできることが大切
  • Makefileにまとめなくても実際にはビルドパイプラインに乗せるしコマンド多くても気にしない