【まいうー】TerraformからCloudFormationをおいしくいただく

2016.10.11

はじめに

こんにちは、中山です。

最近の大幅アップデートでCloudFormationがYAMLをサポートしましたね。一部では「徳を積む」と揶揄されることもある程CloudFormationの欠点に数えられていましたが、YAML形式で簡潔にテンプレートを記述できるようになったことはとても素晴らしいことだと思います。また、YAMLサポートの影に隠れている感がありますが、クロススタック参照という便利な機能も追加されました。詳細については以下のエントリを参照ください。

これでまた1つ苦行から開放されたので、私もCloudFormation職人を開業しました。「JSONが許されるのは2016年9月までだよねー」と煽っていこうと思います。

とはいえ、TerraformにはCloudFormationには無い魅力があります。AWS以外のクラウドサービスのサポート、OSSでの開発などなど、日々改良が続けられ今後も追いかけていきたいツールの1つだと考えています。

そこで、今回はパワーアップしたCloudFormationの力をTerraformから「おいしくいただく」方法をご紹介します。やはりAWSが公式にサポートしているサービスであるという点は魅力的です。特にCloudFormationの方が新サービスを早くサポートする傾向あります。そういった「おいしい部分」をいただいて「まいうー」しましょう。

CloudFormationを利用する方法

現在(2016/10/11)2つの方法があります。リソースとデータソースを利用した方法です。

リソース

aws_cloudformation_stackを利用することでTerraformからCloudFormationのテンプレートを直接実行可能です。 template_body 引数にテンプレートを直接記述するか file 関数でテンプレートへのパスを記述することができます。または、CloudFormationのテンプレートをTerraformのテンプレート機能を利用してレンダリングし、生成したファイルを利用することも可能です。

それぞれpros/consがあります。直接記述する方式ではTerraformのコードとして解釈されるので、マップなどの変数を利用できます。その代わり、コード中にテンプレートを記述する必要があるのでコードの見通しは悪くなると思います。 file 関数及びテンプレート機能はその逆です。状況に応じて使い分けましょう。

また、 template_url 引数でS3などにテンプレートを設置し、それを参照することも可能です。

データソース

データソースとは簡単に解説するとTerraform管理外の情報を参照する仕組みです。詳しくはこちらのエントリを参照ください。

v0.7.3で導入されたaws_cloudformation_stackというデータソースを利用することで、スタック自身とスタックで作成されたリソースの情報を参照可能です。名前自体はリソースと同じになっています。

データソースなので、TerraformとCloudFormationは別々に管理することが可能です。そのため、すでにCloudFormationで構築しているスタックに対して、Terraformからその情報を利用したいといったユースケースで便利かと思います。

基本的にTerraformを利用するが、一部だけ(例えばまだTerraformがサポートしていないサービスなど)はCloudFormationで構築するといった使い方が可能です。もちろん、その逆でCloudFormationを主体に利用し、Terraformを部分的に使うといったこともありです。

ただし、、、

大変残念ですが両方共現時点ではYAMLをサポートしていません。JSONで記述する必要があります。話の流れ的にYAMLでも書けますよ、と言いたかったのですがだめでした。早速ブーメランが直撃しています。Go力が向上したらPR出したいと思います。この辺この辺とか修正する必要がある気がする。

使ってみる

解説はこのぐらいにして早速使ってみたいと思います。

今回は aws_cloudformation_stack リソースを利用して現時点ではTerraformで未対応なAWS WAFを作成し、特定のIPからのみアクセス可能なS3静的サイトを構築してみます。また、 aws_cloudformation_stack データソースを利用して、作成したスタックからACLのIDを参照します。 aws_cloudformation_stack リソース自体はモジュールでGitHub上に置き、Terraformのコード本体とは分離させます。こうすることで、使い回しの効く汎用性の高いコードの記述が可能です。それぞれのコードは以下に置いておきました。ご自由にお使いください。

解説

tf-waf-ip-filter/main.tf

resource "aws_cloudformation_stack" "waf_ip_filter" {
  name = "${var.stack_name}"

  template_body = <<EOT
{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Resources": {
    "MyIPWhilteList": {
      "Type": "AWS::WAF::IPSet",
      "Properties": {
        "Name": "${var.ip_set["name"]}",
        "IPSetDescriptors": [
          {
            "Type" : "${var.ip_set["type"]}",
            "Value" : "${var.ip_set["value"]}"
          }
        ]
      }
    },
    "MyIPSetRule" : {
      "Type": "AWS::WAF::Rule",
      "Properties": {
        "Name": "MyIPSetRule",
        "MetricName" : "MyIPSetRule",
        "Predicates": [
          {
            "DataId" : { "Ref" : "MyIPWhilteList" },
            "Negated" : false,
            "Type" : "IPMatch"
          }
        ]
      }
    },
    "MyWebACL": {
      "Type": "AWS::WAF::WebACL",
      "Properties": {
        "Name": "My web acl",
        "DefaultAction": {
          "Type": "BLOCK"
        },
        "MetricName" : "MyWebACL",
        "Rules": [
          {
            "Action" : {
              "Type" : "ALLOW"
            },
            "Priority" : 1,
            "RuleId" : { "Ref" : "MyIPSetRule" }
          }
        ]
      }
    }
  },
  "Outputs": {
    "WebAclId": {
      "Value": { "Ref": "MyWebACL" }
    }
  }
}
EOT
}

今回は template_body 引数にテンプレートを直接記述する方式にしました。マップを利用したかったからです。テンプレートではそれぞれ以下のCloudFormationリソースを利用しています。

リソース名 用途
AWS::WAF::IPSet IPアドレスに基づくコンディションの作成
AWS::WAF::Rule ルールの作成
AWS::WAF::WebACL ACLの作成

また、CloudFormationのOutputsセクションでACLのIDを参照できるようにしています。

tf-waf-ip-filter-demo/waf.tf

module "waf_ip_filter" {
  source = "github.com/knakayama/tf-waf-ip-filter"

  stack_name = "${var.name}"
  ip_set     = "${var.ip_set}"
}

GitHub上のコードをソースに指定し、それを実行しています。Terraformは一度ローカルに持ってきてから実行するという動作をするので、 plan / apply 実行前に get で以下のようにコードを取得する必要があります。

$ terraform get
Get: git::https://github.com/knakayama/tf-waf-ip-filter.git

tf-waf-ip-filter-demo/cloudfront.tf

data "aws_cloudformation_stack" "waf_ip_filter" {
  name = "${var.name}"
}

resource "aws_cloudfront_distribution" "cf" {
  comment             = "${var.name}-cf"
  price_class         = "${var.cf_config["price_class"]}"
  web_acl_id          = "${data.aws_cloudformation_stack.waf_ip_filter.outputs["WebAclId"]}"
  default_root_object = "index.html"
  retain_on_delete    = true
  enabled             = true
<snip>

1 - 3行目のデータソースで作成したスタックを参照し、8行目でOutputsセクションの出力結果を利用しています。

実行結果

Terraformを実行したら結果を確認してみましょう。ターミナルからCloudFrontのドメインにアクセスしてみます。まずは、許可されたIPアドレスからアクセスしてみます。

$ curl https://**************.cloudfront.net/index.html
YAMLサポート早く来てくれ!!!!1111

心の叫びが聞こえてきましたね。それでは続いて許可してない別のIPアドレスからアクセスしてみます。

$ curl https://**************.cloudfront.net/index.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<HTML><HEAD><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">
<TITLE>ERROR: The request could not be satisfied</TITLE>
</HEAD><BODY>
<H1>ERROR</H1>
<H2>The request could not be satisfied.</H2>
<HR noshade size="1px">
Request blocked.
<BR clear="all">
<HR noshade size="1px">
<PRE>
Generated by cloudfront (CloudFront)
Request ID: ********************************************************
</PRE>
<ADDRESS>
</ADDRESS>
</BODY></HTML>%

アクセスが拒否されました。正常に動作しているようです。最後にCloudFrontとWAFが紐付いているのかも確認してみましょう。

$ aws cloudfront get-distribution-config \
  --id <CloudFront-ID> --query 'DistributionConfig.WebACLId'
"09370909-****-****-****-************"

問題なく紐付いているようです。やりましたね。

まとめ

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

CloudFormationでAWS公式サポートの利点を享受しつつ、Terraformの強みも利用する。これはまいうーですね。とはいえYAMLをサポートしてないので、星1つです。

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