Date: 2015-6-25
Tags: ansible, aws

ansibleのadd_hostを全てのhostで実行する

Ansible を使って、これまで手動で行っていた作業を自動化しました。 その際に、 Ansible公式のec2モジュールのドキュメント のサンプルをまねて行うとうまくいかなかったので、うまく行く方法を考えました。

要件

やりたいことは、以下の通りです。

  • 複数のEC2インスタンスのパッケージを更新する

  • パッケージ更新前と後にAMIバックアップを取る

  • いくつかは普段停止状態なので、作業前に起動させて作業後に停止させる

実行手順

  1. インスタンスのアップデート前バックアップを取る

  2. インスタンスを起動する

  3. apt-get update & dist-upgrade & autoremove を行う

  4. インスタンスのアップデート後バックアプを取る

  5. インスタンスを停止する

これまで - awscli

元々は手動でAWSのコンソールを操作していましたが、awscli を使って以下の手順に置き換えました。この手順でも、起動待ちなどを人間が見て次の手順に進めるなどが必要だったので、後述するAnsibleに乗り換えることにしました。

インストール:

$ pip install awscli
$ aws configure
AWS Access Key ID []: xxxxxxxxxxx
AWS Secret Access Key []: yyyyyyyyyyyyy
Default region name []: ap-northeast-1
Default output format [None]:

AMIバックアップ:

$ aws ec2 create-image --instance-id i-xxxxxxxx --name "srv-dev 20150625-before" --description "srv dev (ubuntu14.04)” --reboot | grep ImageId
"ImageId": "ami-01234567"
$ aws ec2 describe-images --image-ids ami-01234567 | grep State
"State": "available",
$ #availableになるまで何度か実行する

インスタンス起動:

$ aws ec2 start-instances --instance-id i-xxxxxxxx
$ aws ec2 describe-instances --instance-ids i-xxxxxxxx | grep PublicIpAddress
"PublicIpAddress": "54.168.251.239",
$ aws ec2 describe-instance-status --instance-ids i-xxxxxxxx | grep Status
"Status": "ok",
$ #okになるまで何度か実行する

パッケージ更新:

$ ssh foo@54.168.251.239 sudo apt-get update
$ ssh foo@54.168.251.239 sudo apt-get -y dist-upgrade
$ ssh foo@54.168.251.239 sudo apt-get -y autoremove

AMIバックアップ:

$ aws ec2 create-image --instance-id i-xxxxxxxx --name "srv-dev 20150625-after" --description "srv dev (ubuntu14.04)” --reboot | grep ImageId
$ aws ec2 describe-images --image-ids ami-98765432 | grep State
"State": "available",
$ #availableになるまで何度か実行する

インスタンス停止:

$ aws ec2 stop-instances --instance-ids i-xxxxxxxx

この手順ではすべてシェルコマンドで済みますが、待ち時間があったり、AMIのIDやIPアドレスを次のコマンドに渡したりと手間がかかっていました。

これから - Ansible

最終的には以下のようにplaybookを作成しました。

インストール:

$ pip install ansible boto

group_vars/all:

---
# file: group_vars/all

ansible_ssh_user: foo
region: ap-northeast-1
yyyymmdd: '{{ansible_date_time.year}}{{ansible_date_time.month}}{{ansible_date_time.day}}'
cold_standby: false

hosts:

[internal]
localhost ansible_python_interpreter=/usr/local/bin/python

[dev]
srv-dev       instance_id=i-xxxxxxxx cold_standby=true
srv-www-stage instance_id=i-wwwwwwww

update.yml:

---
- name: Backup and launch
  hosts: dev
  connection: local
  gather_facts: true
  tasks:
    - name: Create image
      local_action:
        module: ec2_ami
        instance_id: '{{ instance_id }}'
        region: '{{ region }}'
        wait: yes
        name: '{{inventory_hostname}} {{yyyymmdd}}-before'
        description: '{{inventory_hostname}} (ubuntu14.04)'

    - name: Start instances
      local_action:
        module: ec2
        instance_ids: '{{ instance_id }}'
        region: '{{ region }}'
        state: running
        wait: yes
      register: ec2

    # 実行中のhostをwithで回してdeployグループにIPを登録
    - name: Add new instances to host group
      local_action: add_host hostname={{hostvars[item].ec2.instances[0].public_ip}} groupname=deploy
      with_inventory_hostnames: play_hosts

    - name: Wait for the instances to boot by checking the ssh port
      local_action: wait_for host={{item.public_dns_name}} port=22 timeout=60 state=started
      with_items: ec2.instances


- name: udpate packages
  hosts: deploy  #must match groupname in "add_host" above
  gather_facts: true
  tasks:
    - name: apt-get update
      apt: upgrade=dist update_cache=yes
      sudo: yes

    - name: Autoremove unused packages
      command: apt-get -y autoremove
      sudo: yes


- name: Backup and shutdown
  hosts: dev
  connection: local
  gather_facts: true
  tasks:
    - name: Create image
      local_action:
        module: ec2_ami
        instance_id: '{{ instance_id }}'
        region: '{{ region }}'
        wait: yes
        name: '{{inventory_hostname}} {{yyyymmdd}}-after'
        description: '{{inventory_hostname}} (ubuntu14.04)'

    - name: Stop instances
      local_action:
        module: ec2
        instance_ids: '{{ instance_id }}'
        region: '{{ region }}'
        state: stopped
        wait: yes
      when: cold_standby

add_host の "bypass host loop" 問題

上記のplaybookのadd_hostを使っているところでは、 srv-devsrv-www-stage の2つのホストのIPアドレスを取得して deploy グループに登録することを期待しています。でも、実際には srv-dev のIPしか登録されません。

これは、 "bypass host loop" と呼ばれる挙動で、add_hostのような一部のモジュールはホストの数だけ実行するのでは無く、1回だけ実行するということのようです。 Ansible公式のec2モジュールのドキュメント に書いてあるadd_hostの使い方では、インスタンスを1つしか指定していません。でも、これを読んだら複数インスタンスで使いたいと思いますよね。

というあたりのIssueがいくつも見つかりました。

Ansibleのadd_hostモジュール のページには注意書きとして、「1回しか実行されないから、必要なら with_ 系のループを使ってくれ」と書かれているので、以下のようにして回避しました。

# 実行中のhostをwithで回してdeployグループにIPを登録
- name: Add new instances to host group
  local_action: add_host hostname={{hostvars[item].ec2.instances[0].public_ip}} groupname=deploy
  with_inventory_hostnames: play_hosts

hostvars[item].ec2.instances[0].public_ip のあたりが苦し紛れな感じです。

hostvarsはホスト別の変数を全部もっている変数です。 with_inventory_hostnames: play_hosts で現在の実行対象ホスト一覧を回して、直前のアクションで register: ec2 した変数を取り出しています。

この例では起動されるインスタンスはホスト毎に確実に1つなので、 instances[0] としてしまっています。今回調べて良く目にした with_items: ec2.instances という例は、AMIからインスタンスを起こしているため複数のインスタンスがありえますが、自分の使い方では0決め打ちでOKでしょう。本当はループしたかったのですが、 with_ loopは複数同時に使えないみたいです。

期待する動作になっているのでいいかな、と思いつつ、もっと良い書き方があればお知らせ下さい。