【朗報】TerraformのCloudFromation用リソース/データソースでYAMLに対応しそう

はじめに

こんにちは、中山です。

TerraformのPRを眺めていたら、個人的に待ち望んでいた aws_cloudformation_stack データソースと aws_cloudformation_stack リソースのYAML対応用PRが上がっていたのでご紹介したいと思います。以前TerraformとCloudFromation(以下CFn)を連携させるエントリを書きましたが、その時点ではJSONで書かなければならずちょっと使いにくいという問題がありました。

執筆時点(2017/01/18)ではまだアップストリームにマージされていませんが、今後このPRが取り込まれたらJSON業とさよならできそうですね!

使ってみる

それでは早速使ってみましょう。

インストール

上述したようにこのPRはまだTerraformのアップストリームに取り込まれていません。PR作者の方がフォークしたリポジトリからTerraformのバイナリをコンパイルしましょう。

$ cd $GOPATH/src/github.com/hashicorp/terraform/
$ git remote -v
origin  https://github.com/hashicorp/terraform (fetch)
origin  https://github.com/hashicorp/terraform (push)
$ git remote add ordinaryexperts/terraform git@github.com:ordinaryexperts/terraform.git
$ git remote -v
ordinaryexperts/terraform       git@github.com:ordinaryexperts/terraform.git (fetch)
ordinaryexperts/terraform       git@github.com:ordinaryexperts/terraform.git (push)
origin  https://github.com/hashicorp/terraform (fetch)
origin  https://github.com/hashicorp/terraform (push)
$ git fetch ordinaryexperts/terraform
<snip>
$ git checkout feature/aws-cloudformation-yaml-support
Branch feature/aws-cloudformation-yaml-support set up to track remote branch feature/aws-cloudformation-yaml-support from fork.
Switched to a new branch 'feature/aws-cloudformation-yaml-support'
$ git log -1
commit 5977eaa23633437aa6f9d8e3c63b37ea55b849a0
Author: Dylan Vaughn <dylancvaughn@gmail.com>
Date:   Mon Jan 9 13:59:11 2017 -0800

    don't normalize YAML templates
$ make dev
<snip>
$ $GOPATH/bin/terraform version
Terraform v0.8.3-dev (5977eaa23633437aa6f9d8e3c63b37ea55b849a0)

Your version of Terraform is out of date! The latest version
is 0.8.4. You can update by downloading from www.terraform.io

コード

コンパイルが完了したら早速使ってみましょう。今回はみんな大好きLambda-Backed Custom Resourceを定義したテンプレートを aws_cloudformation_stack リソースで作成し、 aws_cloudformation_stack データソースで出力された値をTerraformで取得してみたいと思います。

  • main.tf
provider "aws" {
  region = "ap-northeast-1"
}

resource "aws_cloudformation_stack" "test" {
  name          = "test-stack"
  template_body = "${file("${path.module}/template.yml")}"
  capabilities  = ["CAPABILITY_IAM"]

  parameters {
    Name = "amzn-ami-hvm-*"
  }
}

data "aws_cloudformation_stack" "test" {
  name = "test-stack"
}

output "ami_id" {
  value = "${data.aws_cloudformation_stack.test.outputs["AMIId"]}"
}

CFnのテンプレートはTerraformと分けたいので file 関数で読み込む形にしています。後述しますが、テンプレートでIAM Roleを作成しているため、 CAPABILITY_IAM を設定しています。また、パラメータとして文字列を指定できるようにしました。 aws_cloudformation_stack データソースでは先程作成したスタックからアウトプットで出力された内容(AMI Id)を出力しています。もちろん aws_cloudformation_stack リソースの属性から参照することも可能です。今回はリソース/データソース両方共使いたかったのでこういった形にしています。

  • template.yml
AWSTemplateFormatVersion : 2010-09-09
Description: Test Template

Parameters:
  Name:
    Description: Input Name
    Type: String

Resources:
  LambdaAMIInfoRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Sid: LambdaBasicExecRole
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: DescribeImagePolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - ec2:DescribeImages
                Resource: "*"
  LambdaAMIInfo:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          def handler(event, context):
              if event['RequestType'] == 'Delete':
                  cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
              response_data = {}
              filters = [
                  {'Name': 'architecture', 'Values': ['x86_64']},
                  {'Name': 'root-device-type', 'Values': ['ebs']},
                  {'Name': 'name', 'Values': [event['ResourceProperties']['Name']]},
                  {'Name': 'virtualization-type', 'Values': ['hvm']},
                  {'Name': 'block-device-mapping.volume-type', 'Values': ['gp2']}]
              try:
                  images = boto3.client('ec2').describe_images(Owners=['amazon'], Filters=filters)
              except Exception as e:
                  response_data['Error'] = e
                  cfnresponse.send(event, context, cfnresponse.FAILED, response_data)
              for i in sorted([image for image in images['Images']], key=lambda x: x['Name']):
                  if i['Name'].lower().count('beta') > 0 or i['Name'].lower().count('.rc') > 0:
                      continue
                  response_data['Id'] = i['ImageId']
              cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)
      Handler: index.handler
      Role: !GetAtt LambdaAMIInfoRole.Arn
      Runtime: python2.7
      Timeout: 20
  CustomAMIInfo:
    Type: Custom::AMIInfo
    Version: 1.0
    Properties:
      ServiceToken: !GetAtt LambdaAMIInfo.Arn
      Name: !Ref Name

Outputs:
  AMIId:
    Value: !GetAtt CustomAMIInfo.Id

今回のCFnテンプレートはLambda-Backed Custom Resourceでパラメータで渡された文字列を元にAMI Idを検索するという内容にしました。まぁ、aws_ami データソース使えば同等のことはできるのですが。。。アウトプットで取得したAMI Idを出力して、Terraformからその値を参照可能にしています。

実行

Terraform実行時点ではテスト用スタックが作成されていないので -target オプションで特定のリソースのみ作成します。

$ $GOPATH/bin/terraform plan -target=aws_cloudformation_stack.test
$ $GOPATH/bin/terraform apply -target=aws_cloudformation_stack.test
aws_cloudformation_stack.test: Creating...
  capabilities.#:          "" => "1"
  capabilities.1328347040: "" => "CAPABILITY_IAM"
  name:                    "" => "test-stack"
  outputs.%:               "" => "<computed>"
  parameters.%:            "" => "1"
  parameters.Name:         "" => "amzn-ami-hvm-*"
  policy_body:             "" => "<computed>"
<snip>
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

The state of your infrastructure has been saved to the path
below. This state is required to modify and destroy your
infrastructure, so keep it safe. To inspect the complete state
use the `terraform show` command.

State path: terraform.tfstate

上記のようにTerraformの作成が完了したら、 -target オプション抜きで全てのコードを実行します。

$ $GOPATH/bin/terraform plan
$ $GOPATH/bin/terraform apply
aws_cloudformation_stack.test: Refreshing state... (ID: arn:aws:cloudformation:ap-northeast-1:/754159e0-dd53-11e6-8f47-503a369c8836)
data.aws_cloudformation_stack.test: Refreshing state...

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

Outputs:

ami_id = ami-9f0c67f8

AWS CLIでもスタックの状態を確認してみます。

$ aws cloudformation describe-stacks \
  --stack-name test-stack
{
    "Stacks": [
        {
            "StackId": "arn:aws:cloudformation:ap-northeast-1:************:stack/test-stack/754159e0-dd53-11e6-8f47-503a369c8836",
            "Description": "Test Template",
            "Parameters": [
                {
                    "ParameterValue": "amzn-ami-hvm-*",
                    "ParameterKey": "Name"
                }
            ],
            "Tags": [],
            "Outputs": [
                {
                    "OutputKey": "AMIId",
                    "OutputValue": "ami-9f0c67f8"
                }
            ],
            "CreationTime": "2017-01-18T07:55:20.076Z",
            "Capabilities": [
                "CAPABILITY_IAM"
            ],
            "StackName": "test-stack",
            "NotificationARNs": [],
            "StackStatus": "CREATE_COMPLETE",
            "DisableRollback": false
        }
    ]
}

正常にスタックが作成されているようです。やりましたね。

まとめ

いかがだったでしょうか。

TerraformからCFnを利用する際にYAMLで記述できるPRをご紹介しました。早くマージしてくれ!!!!1111

本エントリがみなさんの参考になれば幸いに思います。