Ansible で CloudFormation のスタック作成・削除


Ansible を使って AWS の CloudFormation のスタックを作成および削除する方法のメモです。シンプルなスタックの管理は `aws` コマンドをシェルスクリプトでラップすれば良いのですが、テンプレートへの入力パラメータの管理と出力内容の再利用性を高めるために Ansible を使います。

Ansible の *cloudformation* モジュールのドキュメントはこちら。

- [cloudformation - Create or delete an AWS CloudFormation stack — Ansible Documentation](http://docs.ansible.com/ansible/latest/cloudformation_module.html)

作成するスタックは AWS の　Quick Start にある VPC と Bastion を使います。

- [VPC Architecture - AWS Quick Start](https://aws.amazon.com/jp/quickstart/architecture/vpc/)
- [Linux bastion hosts on AWS - Quick Start](https://aws.amazon.com/jp/quickstart/architecture/linux-bastion/)

Bastion スタックには DependsOn を利用して新規に VPC スタックを構成するマスターテンプレートもありますが、ここではスタックの出力を Ansible の変数として受け渡すことを示すため、個別にテンプレートを呼び出します。

## 準備

まずは Ansible をインストールし、AWS の API を実行できるように Boto を追加します。

Ansible のインストールは Ubuntu だと以下のような作業になりますが、他のディストリビューションおよび macOS でもだいたい一緒です。利用環境に合わせて調整してください。

```bash:Ansibleのインストール
$ sudo apt-get install -y python-pip
$ sudo pip install -U pip
$ sudo pip install ansible==2.4.0
```

続いて Boto をインストールし、アクセスキーの情報を設定ファイル (`~/.boto`) に記載します。

```bash:boto3のインストール
$ sudo pip install boto3
```

アクセスキーの情報は利用するアカウントに合わせてください。CloudFormation の作成や削除の権限以外に、スタックの中で管理するリソースに関する権限が必要になります。Bastion スタックでは IAM の Role 関係の権限が必要です。

```ini:~/.boto
[Credentials]
aws_access_key_id = xxxxxxxxxxxxxxx
aws_secret_access_key = xxxxxxxxxxxxxxx
```

Ansible の変数ファイルを `group_vars/all.yml` として用意します。踏み台ホスト (bastion) へのアクセスを許可する IP アドレスを *remote_global_ip_addr* と定義していますが、実行環境のネットワークに応じて値を変更してください。また、リージョンやAZ、キーペア、スタック名なども調整してください。

```yaml:group_vars/all.yml
---

remote_global_ip_addr: "aaa.bbb.ccc.ddd/32"

aws_region: ap-northeast-1
aws_ec2_keypair_bastion:
  name: bastion
  private_key_file: ~/.ssh/keys/aws_bastion.pem

aws_cfn_stack_name: "Example"
aws_cfn_vpc_stack_name: "{{ aws_cfn_stack_name }}VPC"
aws_cfn_vpc_availability_zones: "ap-northeast-1b,ap-northeast-1c"
aws_cfn_vpc_qs_template_url: "https://s3.amazonaws.com/quickstart-reference/aws/vpc/latest/templates/aws-vpc.template"
aws_cfn_vpc_qs_keypair: "{{ aws_ec2_keypair_bastion.name }}"
aws_cfn_bastion_stack_name: "{{ aws_cfn_stack_name }}Bastion"
aws_cfn_bastion_qs_template_url: "https://s3.amazonaws.com/quickstart-reference/linux/bastion/latest/templates/linux-bastion.template"
```

例示のために `all.yml` としていますが、インベントリごとに読み込みを変えたい場合はファイル配置とプレイブックを適宜調整してください。インベントリファイルの配置については Ansible のドキュメントを参照してください。

- [Best Practices — Ansible Documentation](http://docs.ansible.com/ansible/latest/playbooks_best_practices.html)


## スタックの作成

`site.yml` としてプレイブックを用意します。

```yaml:site.yml
---

- name: Create VPC and Bastion stack
  hosts: localhost
  connection: local
  gather_facts: no
  tasks:
    - name: Launch VPC network with NAT gateway
      cloudformation:
        region: "{{ aws_region }}"
        stack_name: "{{ aws_cfn_vpc_stack_name }}"
        state: "present"
        template_url: "{{ aws_cfn_vpc_qs_template_url }}"
        template_parameters:
          AvailabilityZones: "{{ aws_cfn_vpc_availability_zones }}"
          KeyPairName: "{{ aws_cfn_vpc_qs_keypair }}"
      register: _vpc
    - name: Launch bastion stack
      cloudformation:
        region: "{{ aws_region }}"
        stack_name: "{{ aws_cfn_bastion_stack_name }}"
        state: "present"
        template_url: "{{ aws_cfn_bastion_qs_template_url }}"
        template_parameters:
          VPCID: "{{ _vpc.stack_outputs.VPCID }}"
          PublicSubnet1ID: "{{ _vpc.stack_outputs.PublicSubnet1ID }}"
          PublicSubnet2ID: "{{ _vpc.stack_outputs.PublicSubnet2ID }}"
          RemoteAccessCIDR: "{{ remote_global_ip_addr }}"
          EnableTCPForwarding: "true"
          KeyPairName: "{{ aws_ec2_keypair_bastion.name }}"
      register: _bastion
    - name: Show bastion EIP
      local_action: shell echo {{ _bastion.stack_outputs.EIP1 }} > bastion
```

プレイブックを実行します。正常に処理が終了すると *bastion* ファイルに踏み台ホストの IP アドレスが出力されます。

```bash:プレイブックの実行
$ ansible-playbook site.yml
```

## スタックの削除

`halt.yml` としてプレイブックを用意します。

```yaml:halt.yml
---

- name: Delete all stacks
  hosts: localhost
  connection: local
  gather_facts: no
  tasks:
    - name: Delete bastion stack
      cloudformation:
        region: "{{ aws_region }}"
        stack_name: "{{ aws_cfn_bastion_stack_name }}"
        state: "absent"
    - name: Delete VPC stack
      cloudformation:
        region: "{{ aws_region }}"
        stack_name: "{{ aws_cfn_vpc_stack_name }}"
        state: "absent"
```

プレイブックを実行します。

```bash:プレイブックの実行
$ ansible-playbook halt.yml
```

## まとめ

Ansible を使って AWS の CloudFormation のスタックを作成および削除しました。入力パラメータを Ansible のインベントリに沿って管理でき、スタックの出力結果をプレイブックの中で変数として利用できますので、その他にもいくつかの処理が必要な場合には便利です。例えば、踏み台ホストの EIP を `~/.ssh/config` に追記することが考えられます。

シェルスクリプトだと書き方が人によってばらつきがあったり、条件による変数の使い分けが難しいことがありますので、Ansible のような構成管理ツールにまとめておくと便利な場面はあることでしょう。

## 環境補足

Vagrant を使って Ansible および Boto をインストールする場合は以下のようになります。

```ruby:Vagrantfile
# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.box = "bento/ubuntu-16.04"

  config.vm.provision "shell", inline: <<-SHELL
    apt-get update
    apt-get install -y python-pip
    pip install -U pip
    pip install ansible==2.4.0
    pip install boto3
  SHELL
end
```

プレイブックの実行には Ansible Local を使っても良いでしょう。

- [Ansible Local - Provisioning - Vagrant by HashiCorp](https://www.vagrantup.com/docs/provisioning/ansible_local.html)