GitLab-CI+Ansibleで組み立てる、快適なRoute53マネージメント（α版）

ニジボックスは[Web制作の受託](http://nijibox.jp/service/web/)なんかもやっているのですが、その辺りも含めてドメインの管理数がそこそこ多かったりします。

現状Route53で管理しているのですが、

1. ドメインをレジストラから取得（手作業）
2. Route53でゾーンを作成（手作業）
3. NSレコードに用意されたネームサーバーをレジストラに登録（手作業）
4. ゾーンにレコードを追加（手作業）

という手作業祭りになっています。

いい加減面倒になってきたので、ちょっと仕組みを作ってみることにしました。


## 登場人物

### GitLab.com

「プライベートリポジトリ」「CIサービス」「Dockerレジストリ」と便利なものが揃って無料という素敵サービス。

今回はリポジトリでのドメイン情報やPlaybookの管理に、CIでの設定反映を一手に引き受ける今回の主人公

### Ansible

おなじみ構成管理ツール。「こんなのにもモジュールがあるのか」という驚きに定評がある。

### Slack

おなじみチャットツール。お知らせを受け取るのが仕事。

## その1: Route53管理用のPlaybookを用意する

（本当は社内情報を抜き出してリポジトリにした方がよかったのですが、無理だったので要点を抜粋します）


> + files/
>   + example.com.yml
> + group_vars/
>   + all.yml
> + tasks/
>   + zone.yml
>   + record.yml
> + templaes/
>   + records.j2
> + run.yml

よくあるAnsibleのベストプラクティスを魔改造気味にいじった感じになっています。
当然ながら、`run.yml`が実行の起点になっています。
`group_vars/all.yml`にゾーン情報、`files/example.com.yml`にはドメインごとのレコード情報を定義しています。

### run.yml

```yaml:run.yml
---
- hosts: runner
  connection: local
  vars:
    zone_list: '{{ zones|map(attribute="zone")|list }}'
  tasks:
    - name: Manage zones
      include: tasks/zone.yml
      with_items: '{{ zones }}'
    - name: Generate
      template:
        src: templates/records.j2
        dest: files/records.yml
    - name: load record list
      include_vars: files/records.yml
    - name: Manage records
      include: tasks/record.yml
      with_items: '{{ record_list }}'
```

`run.yml`内ではループしながら`tasks/`をインクルードして中のタスクを実行させるのが大半の役割です。
他にも、謎のGenerateが走るのですが、後述します。

```yaml:zone.yml
---
- name: Enable zones - {{ item.zone }}
  route53_zone:
    zone: '{{ item.zone }}'
    state: present
  register: zone_added
- name: Fetch zone information - {{ item.zone }}
  route53:
    command: get
    zone: '{{ item.zone }}'
    record: '{{ item.zone }}'
    type: NS
  register: zone_info
  when: zone_added.changed
- name: Report to Slack channel - {{ item.zone }}
  slack:
    token: '{{ slack.token }}'
    channel: '{{ item.slack_channel|default(slack.channel) }}'
    msg: |
      Route53に、新規ゾーン"{{ item.zone }}"を作成しました。
      レジストラに次のネームサーバーを登録してください。
      {% for ns in zone_info.nameservers %}
      - {{ ns }}
      {% endfor %}
  when: zone_added.changed
```

zone.ymlの中身は「Route53にドメインを登録してSlackにネームサーバーを通知する」のみです。
`route53_zone`モジュールは実行時にゾーンを追加した時に限り`changed`になるので、通知は最初の1回のみです。
（2回目以降はdigでも使いましょう）

### record.yml

```YAML:record.yml
---
- name: Enable record - {{ item.name }}
  route53:
    zone: '{{ item.zone }}'
    record: '{{ item.name }}'
    value: '{{ item.value }}'
    type: '{{ item.type }}'
    command: create
```

ひたすら、レコードを追加するのがrecord.ymlの仕事。
レコード削除？まだないです（むしろつくらない気がする）


基本的には、

1. all.ymlにドメインを追加して、ゾーンに登録
1. ドメイン用のレコード情報YAMLをfilesに置いて、レコードを登録する

というような運用になっています。


## その2: GitLab-CIで最低限のチェックとPlaybook実行する

```
---
stages:
  - lint
  - deploy


check_yaml_format:
  stage: lint
  image: python:2-alpine
  script:
    - pip install yamllint
    - yamllint .

run_ansible:
  stage: deploy
  only:
    - master
  image: ansible/ubuntu14.04-ansible
  script:
    - pip install boto
    - ansible --version
    - |
      if [ $CI_PROJECT_NAMESPACE = "attakei" ] ; then
        ansible-playbook -i hosts/gitlab-ci run.yml
      else
        ansible-playbook -i hosts/gitlab-ci run.yml -C
      fi
```

GitLab-CIが便利です。

GitLab-CIはタスク処理を段階的に分ける仕組みが備わっているのですが、
ブランチ条件の指定も可能なので、**特定条件の反映に限り実行する**という定義も可能です。
ここでは、`check_yaml_format`と`run_ansible`という二つの処理に分けて、

1. `check_yaml_format`は毎回実行してリポジトリ配下のYAMLが文法上正しいかをチェックする
1. `run_ansible`は
    1. 「**masterブランチであり**」 「**namespaceがattakeiである**」時に限りrun.ymlのPlaybookを実行する
    1. 「**masterブランチであり**」 「**namespaceがattakeiでない**」時はrun.ymlのPlaybookをdry-runする

と分けることにしています。

AnsibleはDockerHubに実行可能なコンテナイメージを用意してくれているので、今回依存している`boto`さえついかでインストールさえすれば、簡単にGitLab-CI上でAnsibleを実行できます。
GitLab-CI実行時には環境変数なども見ることができるため、`AWS_ACCESS_KEY`や`AWS_SECRET_KEY`を登録しておけばリポジトリにキーをおかずに済むので安心。

## その3: (その1) と (その2) が合わさり最強に見える

これらの実装を組み合わせると、

1. オリジナル(upstream)はmasterブランチをロックして、マージのみ受け付ける
1. 新しく管理ドメインを足したい時は、`group_vars/all.yml`を更新してマージリクエストを出すようにする
1. 既存のドメインに新しくレコードを追加したい時は、`files/example.com.yml`を更新してマージリクエストを出すようにする
1. マージリクエストはマージ元のCIが通ったことを確認してからマージする
1. マージされる先がmasterブランチとなるため、`run_ansible`が自動で実行されて必要に応じてSlackに報告が上がる
1. Ansibleが冪等性を保ってくれるため、何度やっても問題ない

という、嬉しい実装になります。

## こうなった

1. ドメインをレジストラから取得（手作業）
1. Route53でゾーンを作成（手作業）
1. NSレコードに用意されたネームサーバーをレジストラに登録（手作業）
1. ゾーンにレコードを追加（手作業）

↓これを

1. ドメインをレジストラから取得（手作業）
1. `group_vars/all.yml`を更新してRoute53でゾーンを作成（**ファイル更新だけ手作業**）
1. NSレコードに用意されたネームサーバーをレジストラに登録（手作業）
1. `files/example.com.yml`を追加/更新してRoute53でレコードを登録（**ファイル更新だけ手作業**）

こうしました。結局全部手作業があるのは変わっていないのですが、

- 2,4の手作業枠のうち、AWSコンソールのログインなどがバッサリ消えた
- コード化されたので、確認するためにAWSコンソールに行く必要もなくなった

という恩恵にあずかることができます。


## オチ

というのを実装したものの、まだ実運用に至っていないので、
既存ドメインを持ってきたり、啓蒙活動をちょこちょこしないといけないですね。