試行錯誤のおと

日々の試行錯誤した結果です。失敗することが多い記録、それだけでっす!

私的 CloudFormation ベストプラクティス

最近 CloudFormation を触っていて、よくある初期構築のベストプラクティスについて意見がほしいので自分の考える CloudFormation の設計や使い方についての考えを書いた。

CloudFormation のメリット

CloudFormation を利用するためメリットはリソースの参照を簡単に記述できることと、べき等性の保証だと思っている。

以前、 EC2 インスタンスのプロビジョニングを Ansible で書いたことがある。

べき等性を確保したく、 サブネットの作成、 EBS のマウント、アンマウント、 EIP の付け替えなどの変更操作を Ansible だけで操作したい + その後のプロビジョニングの操作も Ansible にお任せしたいということを考えたときに、 Ansible の YAML ではなく以下のような Python のコードを書いた。

# ansible_playbook は data に playbook のデータを取ってタスクを実行する関数
data = [
    {
        'connection': 'local',
        'tasks': [
            {'ec2_vpc_subnet': {
                'vpc_id': 'vpc-xxxxxxxx',
                'state': 'present',
                'az': 'ap-northeast-1a',
                'cidr': '10.0.0.0/24',
                'resource_tags': {
                    'Name': 'jessie'
                }
            }, 'name': 'create subnet'}],
        'hosts': 'localhost'
    }
]
ansible_playbook(data=data)

subnets = get_all_subnets(filters={"vpcId": vpc_id,
                                     "tag:Name": "jessie"})
subnet_id = subnets[0].id

data = [
    {
        'connection': 'local',
        'tasks': [
            {
                'ec2':
                {
                    'assign_public_ip': 'yes',
                    'count': 1,
                    'ebs_optimized': False,
                    'image': 'ami-dbc0bcbc',
                    'instance_type': "t2.micro",
                    'key_name': 'kizkoh',
                    'monitoring': 'no',
                    'placement_group': False,
                    'private_ip': '10.0.0.4',
                    'tenancy': 'default',
                    'vpc_subnet_id': subnet_id,
                    'wait': 'yes'
                },
                'name': 'run jessie'
            }
        ],
        'hosts': 'localhost'
    }
]

ansible_playbook(data=data)

こんなコードを書いたのは Ansible の Playbook では YAML で EC2 API の実行の応答結果を扱えないためだ。 たとえば、インスタンスを作成した時に得られるインスタンス ID を引数に他の API を実行することができない。

CloudFormation はテンプレートと呼ばれるファイルに記述することでそれぞれのマネージドサービスのプロパティを記述することができる。 マネージドサービスのプロパティでは他のマネージドサービスのプロパティを参照できる。 先の Python のコードを CloudFormation のテンプレートに書き換えると以下のようになる。

AWSTemplateFormatVersion: 2010-09-09
Description: jessie stack
Resources:
  Subnet:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: ap-northeast-1a
      CidrBlock: 10.0.0.0/24
      Tags:
        - Key: Name
          Value: jessie
      VpcId:
        Ref: vpc-xxxxxxxx
  Instance:
    Type: "AWS::EC2::Instance"
    Properties:
      EbsOptimized: False
      ImageId: ami-dbc0bcbc
      InstanceType: t2.micro
      KeyName: kizkoh
      Monitoring: False
      NetworkInterfaces: 
        - AssociatePublicIpAddress: "true"
          DeviceIndex: "0"
          SubnetId: 
            Ref: !Ref Subnet
      Tenancy: default

依存関係ごとにスタックを分けて、記述するのが個人的なベストプラクティスだと思っている。 特に依存関係がないものはマネージドサービスごとにスタックを分けていて、以下のようなフォルダ構成を取っている。

$ tree
.
├── cloudtrail
│   ├── parameters
│   │   ├── production.json
│   │   └── staging.json
│   └── template.yaml
├── iam
│   └── template.yaml
├── README.md
...

ステージング環境と本番環境で一部のパラメタを変えて API の引数を変えているのでオペレーションを環境間でそのまま適用できるのも CloudFormation の魅力だと思っている。 スタックの動作について環境間の差分としてパラメータ以外は変えることはできず If 文がサポートされていないのもそういった用途のマッチしていて、環境間で整合性をとるための方法としてユースケースを限定してくれているのがよいなという印象を持った。

リソースの参照以外にも最新の API への対応がすぐにされるあたりは公式ならではだと思う。 Terraform などのサードパーティ製ツールを使うとどうしても API への対応が遅れることがあるので、公式がサポートしている恩恵も大きい。 CloudFormer を使えば YAML を書かなくてもテンプレート化できるので、深くドキュメントを読み込む必要がないところもよいと感じた。

テンプレートフォーマットの選択

テンプレートフォーマットに YAML がサポートされるまではテンプレートは JSON で記述するか、サードパーティ製モジュールの SparkleFormation, kumogata, CoffeeFormation 等を用いて独自の DSL で記述するといった選択肢があった。 これらはナンセンスだと思っていて、 JSON を使って記述するとフォーマットとしての可読性はそこそこ高いのだけどコメントが書けない致命的な問題、サードパーティ製モジュールの DSL を利用するとサードパーティモジュールへの依存や、公式ドキュメントのサンプルを参照しにくい問題につまずいた。 YAML がサポートされるようになってからはこれらの問題が解決されて、 YAML が選択肢として良いと思っている。

しかし、 YAML を採用するとなると新たに考慮すべき点がある。例えば、ポリシードキュメントの記法を考慮しないといけない。 IAM グループを作成するときのテンプレートを、すべて YAML で記述すると以下のようになる。

...
  Groupadmin:
    Type: "AWS::IAM::Group"
    Properties:
      GroupName: admin
      Policies:
        - PolicyName: AllAllow
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action: "*"
                Resource: "*"
...

しかし、ポリシードキュメント部分は以下のようにして JSON (文字列扱い) で書くことができる。

...
  Groupadmin:
    Type: "AWS::IAM::Group"
    Properties:
      GroupName: admin
      Policies:
        - PolicyName: AllAllow
          PolicyDocument: |
            {
              "Statement": [
                {
                  "Effect": "Allow",
                  "Action": "*",
                  "Resource": "*"
                }
              ]
            }
...

どちらが見やすいかという議論ではなくて、ポリシードキュメントは AWS のシステム上では JSON で記述される。 そのため、ポリシードキュメントのドキュメントを見ても、 JSON のサンプルはあるものの YAML のサンプルはない。 また、変更したい時にマネジメントコンソール上で validate して確認した後にテンプレートに貼り付けることで Stack 更新時のエラーにハマることなく更新できる。 なので、自分はポリシードキュメント部分に関しては JSON で記述するようにしている。

どこまでを CloudFormation で扱うか

今回、 CloudFormation のテンプレートを書いていて困ったのは一部の API が扱えないこと。 例えば、 EC2 の KeyPair の登録や CloudFormation 以外で作成したリソースのプロパティの取得ができない。

EC2 の KeyPair の登録は AWS アカウントごとあるいはリージョンごとに繰り返す作業なので、 CloudFormation にまとめて定型化したい。 特に CloudFormation が API をサポートしてくれない理由はないと思うだけど、 CloudFormation が対応しない API に対しては無理に CloudFormation で対応するのではなく別のツールを使うのが良いと思う。 外部のリソースに関してはパラメータに埋め込むのがよいと思う。 テンプレートを動的に生成するような対応を取ると、リソースの依存解決が CloudFormation 外に依存することや環境ごとにテンプレートが異なる可能性 (この環境ではリソースを作成するが別の環境では作成しない等の If 文のような場合分け) が発生する。 テンプレートの生成次第でなにもかもできるようになってしまうと、結局のところ冒頭で書いた Ansible の Python コードと何ら変わりはないので、CloudFormation は CloudFormation がサポートする範囲内でオペレーションを収めたい。 AWS に CloudFormation の API のサポートを要望を出していって、それまでのつなぎとして使うには動的テンプレート生成はありかなと思うけれども、避けられるようなら避けたい課題かなと思う。

まとめ

CloudFormation の個人的に思っているベストプラクティス知見について書いた。もっとこうしたほうがいいとか、知見がほしい。