AnsibleとShellでALBのTargetGroupのWebサーバのデーモンを再起動する

こんにちは。インフラの奥村です。

本日は、AnsibleとShellでALBのTargetGroupに登録されているインスタンスのWebサーバのデーモンを再起動させる記事です。

前提

今回の記事の概要としては、
ALBのターゲットグループに登録されたインスタンスで動作しているWebサーバーを

  • グレイスフルにターゲットグループから外し
  • 特定のコマンドを実行し、
  • 起動が確認できたら再度ターゲットグループに登録する

というのをAnsible + Shellで行います。

Shellで行う部分

  • ターゲットグループの操作
  • 特定のコマンドの実行
  • 起動確認

Ansibleで行う部分

  • 対象のサーバーへ逐次実行

使うもの

  • Ansible
  • Bash
    • awscli
    • jq

対象

  • ALB
    • ターゲットグループのarnが必要
  • Webサーバー(Appサーバー)

事前準備

記事の例では再起動の対象をunicornにしています。

本題

Shell

まず単体で動作するシェルスクリプトを作成します。 それがこちらです。
動作がわかりやすいように、実行時にメッセージを出力するようにしていますが、動作には必要ありません。

tg_restart_unicorn.sh

#!/usr/bin/bash
set -euC
target_group_arn=$1
rails_env=$2
target_group_name=$(echo "$target_group_arn" | awk -F":" '{print $6}' | awk -F"/" '{print $2}')

my_instance_id=$(curl -s http://169.254.169.254/latest/meta-data/instance-id/)
unicorn_pid=$(cat /var/run/amc/unicorn.pid)
unicorn_port=8080
app_dir=/var/www/sample

# ターゲットグループから登録を解除
aws elbv2 deregister-targets --target-group-arn "$target_group_arn" --targets Id="$my_instance_id"
echo "[message] deregistered $my_instance_id from $target_group_name"

# unicronを再起動
echo "[message] restart unicorn"
kill -QUIT "$unicorn_pid" \
  && cd "$app_dir" \
  && bash -lc "RAILS_ENV=$rails_env bundle exec unicorn -c config/unicorn.rb -p $unicorn_port -D" \
  && echo "[message] complete restart unicorn"

# 起動を確認できるまで無限ループ
while true
do
  status_code=$(curl -s 127.0.0.1:8080 -w "%{http_code}\n" -o /dev/null)
  echo "[message] unicorn satetu code: $status_code"
  if [[ "$status_code" == 302 || "$status_code" == 200 ]];then
    # 起動が確認できたら再度ターゲットグループに登録
    aws elbv2 register-targets --target-group-arn "$target_group_arn" --targets Id="$my_instance_id"
    echo "[message] register start $my_instance_id to $target_group_name "
    break
  fi
  sleep 1
done

# ターゲットグループのヘルスチェックが通るまで無限ループ
while true
do
  target_group_info=$(aws elbv2 describe-target-health --target-group-arn "$target_group_arn")
  target_instance_info=$(echo "$target_group_info" | jq ".TargetHealthDescriptions[] | select(.Target.Id == \"$my_instance_id\")")
  
  # ターゲットのインスタンスのヘルスチェックの状態を取得
  target_instance_state=$(echo "$target_instance_info" | jq -r .TargetHealth.State)
  echo "[message] instance state: $target_instance_state"
  if [[ "$target_instance_state"  == "healthy" ]];then
    echo "[message] complete register $my_instance_id to $target_group_name"
    exit 0
  fi
  sleep 1
done

呼び出し

./roling_restart_unicorn.sh <ターゲットグループのarn> development 
#今回はunicornを例にしているのでRAILS_ENV(development)を含めています

実行結果

[message] deregistered i-xxxxxxxxxxx(インスタンスID) from sample-tg(ターゲットグループネーム)
[message] restart unicorn
[message] complete restart unicorn
[message] unicorn satetu code: 302
[message] register start i-xxxxxxxxxxx to sample-tg
[message] instance state: healthy
[message] complete register i-xxxxxxxxxxx to sample-tg
  • ターゲットグループの操作
  • 特定のコマンドの実行
  • 起動確認

を実行できるシェルスクリプトができました。

Ansible

作成したシェルスクリプトを逐次実行させるAnsibleを作成します。

グループに所属するホストに順番に行う方法として、serial という方法がありますが、task単位では実行できません。
いろんな方がワークアラウンドを実現していますが、私はこちらの記事を参考にさせてもらいました。

unicornというroleを作成し、その中に2つのplaybookを配置します。
varsファイルにも二つの変数を定義します。 対象のホストは「10.1.1.1」「10.1.1.2」の2台とします。

※ ディレクトリ構成, インベントリファイルについては省略させていただきます。

roles/unicorn/tasks/main.yml

---
- name: debug ansible_play_hosts
  debug:
    var: ansible_play_hosts

- name: defined unicorn_pid_file variable
  stat:
    path: "{{ unicorn.pid_file }}"
  register: unicorn_pid_file

- name: restart unicorn process
  include_tasks: unicorn_restart.yml
  delegate_to: "{{ item }}"
  run_once: yes
  with_items: "{{ ansible_play_hosts }}"

- name: start unicorn
  become: no
  shell: bash -lc "RAILS_ENV={{ rails.env }} bundle exec unicorn -c config/unicorn.rb -p {{ unicorn.port }} -D"
  args:
    chdir: "{{ rails.app_dir }}"
  when: not unicorn_pid_file.stat.exists

roles/unicorn/tasks/unicorn_restart.yml

---
- name: copy tg_restart_unicorn.sh
  become: no
  copy:
    src: tg_restart_unicorn.sh
    dest: /tmp
    mode: 0755

- name: execute tg_restart_unicorn
  become: no
  shell: bash -lc "/tmp/tg_restart_unicorn.sh {{ alb.target_group.arn }} {{ rails.env }}"

group_vars/development.yml

---
alb:
  target_group:
    arn: "ターゲットグループのarn。シェルの引数として与える。"
rails:
  dir: /var/www/sample
  env: development

こうした場合、「delegate_to: "{{ item }}"」で対象のホストを変更できてはいるのですが、playbookの実行結果が以下のようになります。

TASK [rails : copy roling_restart_unicorn.sh **********************************************************************************************************
ok: [10.1.1.1]

TASK [rails : execute roling_restart_unicorn  **********************************************************************************************************
changed: [10.1.1.1]

TASK [rails : copy roling_restart_unicorn.sh  **********************************************************************************************************
ok: [10.1.1.1]

TASK [rails : execute roling_restart_unicorn  **********************************************************************************************************
changed: [10.1.1.1]

すべてのtaskが単一ホスト上で実行されているように見えますね。
run_onceを無しにすると、ホスト数分ループが行われます。
ansible_play_host の変数がホスト数分存在するためですね。

delegate_toの対象ホストがplaybookの実行結果から確認できるように、loop_controle を利用します。

loop_contrl を追加したバージョンがこちらです。

roles/unicorn/tasks/main.yml

---
- name: debug ansible_play_hosts
  debug:
    var: ansible_play_hosts

- name: defined unicorn_pid_file variable
  stat:
    path: "{{ unicorn.pid_file }}"
  register: unicorn_pid_file

- name: restart unicorn process
  include_tasks: unicorn_restart.yml
# loop_controlで登録した変数をターゲットにする
  delegate_to: "{{ target_host }}"
  run_once: yes
  with_items: "{{ ansible_play_hosts }}"
# loop_controlを追加
  loop_control:
    loop_var: target_host 

- name: start unicorn
  become: no
  shell: bash -lc "RAILS_ENV={{ rails.env }} bundle exec unicorn -c config/unicorn.rb -p {{ unicorn.port }} -D"
  args:
    chdir: "{{ rails.app_dir }}"
  when: not unicorn_pid_file.stat.exists

roles/unicorn/tasks/unicorn_restart.yml

---
- name: copy tg_restart_unicorn.sh {{ target_host }}
  become: no
  copy:
    src: tg_restart_unicorn.sh
    dest: /tmp
    mode: 0755

- name: execute tg_restart_unicorn {{ target_host }}
  become: no
  shell: bash -lc "/tmp/tg_restart_unicorn.sh {{ alb.target_group.arn }} {{ rails.env }}"

実行結果はこうなります。

TASK [rails : copy roling_restart_unicorn.sh 10.1.1.1] *******************************************************************************************************************
ok: [10.1.1.1]

TASK [rails : execute roling_restart_unicorn 10.1.1.1] *******************************************************************************************************************
changed: [10.1.1.1]

TASK [rails : copy roling_restart_unicorn.sh 10.1.1.2] ******************************************************************************************************************
ok: [10.1.1.1]

TASK [rails : execute roling_restart_unicorn 10.1.1.2] ******************************************************************************************************************
changed: [10.1.1.1]

delegate_toの対象が表示されるようになりました。

まとめ

Ansible + Shell でALBを含めたローリングデーモンリスタートを実現できました!
Ansibleで実現するには複雑 + 比較的シンプルな処理 は 積極的にシェルスクリプトを用いることにしています!
悩む時間が少なく、そこまでメンテンナンスの負荷が上がるわけでもないので個人的にはおすすめです。

最後までご覧いただきありがとうございまいた。