CloudFormation の設定用JSONでもう少し幸せになる方法



前回、[AWS の設定用JSONで少し幸せになる方法](http://qiita.com/kz_takatsu/items/d5343e15dacd670ab1df) ってことで書いたネタの続き。
いろいろいじってると、もう少し便利にしたくなってきました。


設定のコメントアウト
----------

１個目はシンプルなもの。
CloudFormation のデバッグをしていると、いろいろエラーが出てきて、コメントアウトしたい！ってなるんだけど、前回の正規表現だと、意識的にコメントをつけることは出来ても、JSONとして書いているところをコメントアウト出来ない。

ということで、下記のようにコメントアウトにも対応しました。先頭に 「//」がある時しか対応してないけど、個人的にはこれだけで十分です。

```
    def self.strip_comments(str)
        ret_str = str.gsub(/\/\/[^'"]*$/, "\n").gsub(/^\/\/*$/, "")
        return ret_str
    end
```


CloudFormation のファイル分割
--------

JSONファイルが長くなるとやっぱりわけたい。
もともと CloudFormation には分割のための TemplateUrl とかあるんですが、手元のファイルで分割したりしながらやりたいわけで…

でも、分割しちゃうと、いろいろな IDの参照とかが出来なくなってしまうので、いちいち IDを確認して　Parameters に書き入れるなんて作業が必要になり、作っては消し　するたびにむなしいコピペが発生します。

Terraform 導入も考えたけど、まだいろいろツラみもあるようだし…
ファイル分割できるだけで　だいぶ幸せになれるんだけどな〜　と思って、少し Ruby SDK を眺めたところ、それなりに便利な方法が出来ました :-)

### 前提

* VPC で切られた環境に構築。
* CloudFormation の設定ファイルは、例えば下記みたいに分割
  * ネットワーク設定
  * 踏み台サーバ
  * ELB
* CloudFormation の各設定は、自分で依存関係を意識した上で、それぞれ順番に別stackとして実行する。
* 分割された個々のファイルの中で、参照したい {"Ref": } については、ちゃんと "Parameters" の中で定義しておく（値は後で上書かれる）。

分割した設定ファイルをそれぞれ分割して別stack として実行するので、踏み台サーバの stack だけを削除して、デバッグして再実行する　なんてことも出来ます。

もちろん、設定ファイルは、もっと増やしてもOKです。


### Parameters の解決方法

AWS SDK for Ruby を入れている状態で、下記みたいにやってみると...

```
[ec2-user@ip-xx-xxx-xx-xx scripts]$ aws.rb --region ap-northeast-1
Aws> cf = Aws::CloudFormation::Client.new
=> #<Aws::CloudFormation::Client>
Aws> cf.describe_stack_resources({ :stack_name => "sample" })
```

いろいろなリソースが出てくるんですが、すべてのリソースについて下記のような記載が出てきます。

    logical_resource_id="AttacheGateway",
    physical_resource_id="VPC-f-Attac-xxxxxxxxxxxxx",
    resource_type="AWS::EC2::VPCGatewayAttachment",

どうやら logical_resource_id というのが CloudFormation で書いた定義名で、physical_resource_id というのが、参照時に入れるべき値のようです。
とすると、これらの値を取得して Parameters につっこんであげれば、参照の問題が解決しそう。



### ファイル分割の実行例

だいぶ省略して雑（＆せつな）に書いてますが、こんな感じの ruby スクリプトを用意しました。


```:aws-cf-script.rb
#! /usr/bin/ruby
require './aws-common.rb'

stacks = {
    "network" => {
        :template => "./json/network.json",
        :ref => []
    },
    "fumidai" => {
        :template => "./json/fumidai.json",
        :ref => ["network"]
    },
    "elb" => {
        :template => "./json/elb.json",
        :ref => ["network"]
    }
}

vpc_name = ARGV[0]
template = ARGV[1]
stack_name = vpc_name + "-" + template

params = AWSutil.load_params_from_stack(vpc_name + "-" + stacks[template][:ref][0])
params = AWSutil.delete_unused_params(stacks[template][:template], params)
AWSutil.cf_start(stack_name, stacks[template][:template], params)
```

```:aws-common.rb
require 'aws-sdk'
require 'aws_config'
require './jsoncfg.rb'

Aws.config.update(AWSConfig.default.config_hash)

module AWSutil
    def self.cf_start(stack_name, template_file, params)
        cf = Aws::CloudFormation::Client.new

        template = JSON_cfg.loadFromFile(template_file)
        template_body = JSON.generate(template)
        puts JSON.pretty_generate(template)

        stack = Aws::CloudFormation::Stack.new(stack_name)

        if stack.exists? then
            cf.update_stack({ stack_name: stack_name, template_body: template_body, on_failure: "DO_NOTHING", capabilities: ["CAPABILITY_IAM"], parameters: params })
        else
            cf.create_stack({ stack_name: stack_name, template_body: template_body, on_failure: "DO_NOTHING", capabilities: ["CAPABILITY_IAM"], parameters: params })
        end
    end

    def self.delete_unused_params(template_file, params)
        template = JSON_cfg.loadFromFile(template_file)
        t_params = template["Parameters"]
        new_params = []
        params.each do |param|
            if t_params[param["parameter_key"]]
                p param["parameter_key"] + ": " + param["parameter_value"]
                new_params.push param
            else
            end
        end
        return new_params
    end

    def self.load_params_from_stack(stack_name)
        cf = Aws::CloudFormation::Client.new
        resources = cf.describe_stack_resources({ :stack_name => stack_name })

        params = []
        resources[0].each do | res |
            param = {}
            param["parameter_key"] = res[:logical_resource_id]
            param["parameter_value"] = res[:physical_resource_id]
            param["use_previous_value"] = true
            params.push(param)
        end

        return params
    end
end
```

やっていることはシンプルで、下記のとおりです。

1. 実行するstackの中で、参照したいリソースに関するstack を定義する
2. 参照先の stack に関する describe_stack_resources を呼ぶ
3. describe_stack_resources の返り値をもとに params を作る
4. 実行したいテンプレートに存在しない params を削除する
5. CloudFormation 実行時の Parameters として取得した params を設定したうえでテンプレートを実行する

```bash:実行例
% ruby ./aws-cf-script.rb sample elb
```

こうすると、elb が参照している "sample-network" stack のリソースを取得し、VPCのIDなどを parameters として取り込んで、"sample-elb" stack を実行してくれます。

こんな感じで、ファイルも分割できて、コメントもつけられるようになり、CloudFormation の設定用JSON を作る作業がさらに少し幸せになりました :-)