AXLGEAR開発を支えるAnsible [ループ処理編]

こんにちは。AXLBITでインフラまわりをみている竹西宏真です。
弊社でも技術ブログをはじめることとなりました。頑張って更新していきますのでよろしくおねがいします。

初めての記事を書くにあたり悩みましたが、私が業務でも良く使用するということで
AXLGEARを構築する際に欠かせないAnsibleの簡単な説明と
以前ハマったポイントをお伝えしようと思います。

Ansibleとは

本題に入る前にAnsibleとは構成管理ツールと呼ばれInfrastructure as a Codeを実現するツールの一つです。
構成管理ツールを導入することで煩わしい反復作業を自動化でき、ヒューマンエラーを排除できると
数多くの企業が採用しています。

有名な構成管理ツールには他にもPuppetやChefといったツールがよくあげられます。
数多くのサイトでも紹介されていますがAnsibleの特徴はなんといっても

  • エージェントレス
  • シンプルな記述

という点ではないでしょうか。
私自身、初期導入への学習コストが低いということが最大の強みと感じています。
ですが、Ansibleは多くのモジュールを提供しており複雑な構成を構築するためにはモジュールへの理解が必要不可欠という点は念頭に置く必要があります。

多重ループ

早速ハマったポイントがどこかという話に入っていきたいと思います。
ハマったポイントは多重ループの書き方でした。
プログラミングにおける初歩ですね。。。for文で良く書くこんなやつです。

for(i=0;i<10;++i){
  for(j=0;j<10;++j){
  }
}

…そんなとこかよ、と思うかもしれませんがAnsibleのモジュールや構文について
ロクに調べていなかった私にとってどうしたらいいのかわかりませんでした。
taskによるループ処理はloopwith_itemしか使っていなかったのですから。

先に結論を述べてしまいますとwith_subelementsを使用して多重ループは解決することが可能です。

- name: with_subelements
  debug:
    msg: "{{ item.0.name }} - {{ item.1 }}"
  with_subelements:
    - "{{ users }}"
    - mysql.hosts

他にも解決方法はありますが、その他の方法についてはこちらを参考にしてみてください。

Nginxで複数バーチャルホスト内にサブディレクトリを切る

では、具体的にどんな課題があったのか、先述のwith_subelementsによるtaskの多重ループと、
応用としてtemplateモジュールで設定ファイル内多重ループの使用方法についてお話したいと思います。

今回の課題はNginxを用いて複数のバーチャルホストを作成し、そのバーチャルホスト内でさらにサブディレクトリを複数切るという要件を解決したいというものでした。
イメージとしてはこんな感じでしょうか。

実装方法としてはバーチャルホスト毎にhttpsアクセスを受け付け、ssl終端をおこなってから
サブディレクトリへhttp転送するようリバースプロキシの設定をしていきます。

group_varsの定義

では上図の要件をAnsibleで実現していきましょう。
まずバーチャルホストやサブディレクトリの情報を変数に定義します。
単一のvhost内に複数のサブディレクトリ情報が必要なのでネストするように記述します。

vhosts:
  - vhost_name:            'example.com'
    subdir:
      - subdir_name:          'test_example'
        site_name:            'TEST-EXAMPLE'
        redirect_port:        '40001'
      - subdir_name:          'prod_example'
        site_name:            'PRODUCT-EXAMPLE'
        redirect_port:        '40002'
  - vhost_name:            'sample.com'
    subdir:
      - subdir_name:          'test_sample'
        site_name:            'TEST-SAMPLE'
        redirect_port:        '40003'
      - subdir_name:          'prod_sample'
        site_name:            'PRODUCT-SAMPLE'
        redirect_port:        '40004'

taskの準備

次に、作成した設定ファイルを対象ホストに送付するtaskを作成します。
ここで冒頭で説明したwith_subelementsを使用しています。

- name: nginxバーチャルホスト(リバースプロキシ)設定ファイルを転送      #item.0は変数の一階層目を指定
  template: src=etc/nginx/conf.d/vhost.conf dest=/etc/nginx/conf.d/{{ item.0.vhost_name }}.conf
  with_subelements:
    - "{{ vhosts }}"
    - subdir

- name: nginxサブディレクトリ設定ファイルを転送                       #item.1は変数の二階層目を指定
  template: src=etc/nginx/conf.d/subdir.conf  dest=/etc/nginx/conf.d/{{ item.1.subdir_name }}.conf
  with_subelements:
    - "{{ vhosts }}"
    - subdir

templateファイルの準備

最後にnginxの設定ファイルを作成します。
vhostレベルの設定ファイル、サブディレクトリ単位の設定ファイルが必要です。
また、今回は想定としてcakephpアプリケーションの設定を定義します。

わかりづらいので記事の最後にネストした変数を展開する解説をしたいと思います。

server {
    listen          443;
    server_name     {{ item.0.vhost_name }};
    server_tokens   off;

    ssl on;
    ssl_certificate     /etc/pki/tls/certs/{{ item.0.vhost_name }}/server.crt;
    ssl_certificate_key /etc/pki/tls/private/{{ item.0.vhost_name }}/server.key;

    proxy_set_header    Host                $http_host;
    proxy_set_header    X-Real-IP           $remote_addr;
    proxy_set_header    X-Forwarded-Host    $host;
    proxy_set_header    X-Forwarded-Server  $host;
    proxy_set_header    X-Forwarded-Proto   $scheme;
    proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;

    access_log  /var/log/nginx/access-{{ item.0.vhost_name }}.log  main;

# ネストした変数を展開
{% set if_vhost_name=item.0.vhost_name %}
{% for vhost in vhosts %}
{% for item in vhost.subdir %}
{% if vhost.vhost_name==if_vhost_name %}
    location /{{ item.subdir_name }}/ {
        proxy_pass    http://127.0.0.1:{{ item.redirect_port }}/{{ item.subdir_name }}/;
        proxy_redirect http:// https://;
    }
{% endif  %}
{% endfor %}
{% endfor %}

}

server {
    listen          {{ item.1.redirect_port }};
    server_name     {{ item.0.vhost_name }};

    index  index.php;

    location ^~ /{{ item.1.subdir_name }}/ {
            root   /var/www/html/{{ item.1.subdir_name }}/webroot;
            index  index.php;
            try_files $uri $uri/ @cakephp;
    }
    location ^~ /{{ item.1.subdir_name }}/\.php$ {
            root   /var/www/html/{{ item.1.subdir_name }}/webroot;
            try_files $uri $uri/ @cakephp;
            fastcgi_pass   unix:/var/run/php-fpm/{{ item.1.subdir_name }}.sock;
            fastcgi_index  index.php;
            fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
            include        fastcgi_params;
    }

    location @cakephp {
            root   /var/www/html/{{ item.1.subdir_name }}/webroot;
            fastcgi_pass   unix:/var/run/php-fpm/{{ item.1.subdir_name }}.sock;
            fastcgi_param  SCRIPT_FILENAME $document_root/index.php;
            include        fastcgi_params;
            rewrite ^(.*)$ /index.php break;
    }


}

実行結果

これで準備が整いましたのでplaybookを実行してNginxの設定が反映されているか確認したいと思います。
playbookを実行すれば以下のような出力結果が得られると思います。

Playbook実行結果

TASK [send : nginxバーチャルホスト(リバースプロキシ)設定ファイルを転送] **********************************************************************************************************************************************************************************************
changed: [nginx] => (item=[{u'vhost_name': u'example.com'}, {u'subdir_name': u'test_example', u'site_name': u'TEST-EXAMPLE', u'redirect_port': u'40001'}])
ok: [nginx] => (item=[{u'vhost_name': u'example.com'}, {u'subdir_name': u'prod_example', u'site_name': u'PRODUCT-EXAMPLE', u'redirect_port': u'40002'}])
changed: [nginx] => (item=[{u'vhost_name': u'sample.com'}, {u'subdir_name': u'test_sample', u'site_name': u'TEST-SAMPLE', u'redirect_port': u'40003'}])
ok: [nginx] => (item=[{u'vhost_name': u'sample.com'}, {u'subdir_name': u'prod_sample', u'site_name': u'PRODUCT-SAMPLE', u'redirect_port': u'40004'}])

TASK [send : nginxサブディレクトリ設定ファイルを転送] ********************************************************************************************************************************************************************************************************
changed: [nginx] => (item=[{u'vhost_name': u'example.com'}, {u'subdir_name': u'test_example', u'site_name': u'TEST-EXAMPLE', u'redirect_port': u'40001'}])
changed: [nginx] => (item=[{u'vhost_name': u'example.com'}, {u'subdir_name': u'prod_example', u'site_name': u'PRODUCT-EXAMPLE', u'redirect_port': u'40002'}])
changed: [nginx] => (item=[{u'vhost_name': u'sample.com'}, {u'subdir_name': u'test_sample', u'site_name': u'TEST-SAMPLE', u'redirect_port': u'40003'}])
changed: [nginx] => (item=[{u'vhost_name': u'sample.com'}, {u'subdir_name': u'prod_sample', u'site_name': u'PRODUCT-SAMPLE', u'redirect_port': u'40004'}])

Ansible実行対象サーバのNginxファイルは以下のように配備されています。

[root@nginx]# ls /etc/nginx/conf.d/
example.com.conf  prod_example.conf  prod_sample.conf  sample.com.conf  test_example.conf  test_sample.conf

内容をみるとgroup_varsで定義した値がそれぞれのファイルに設定されているのが確認できました。

server {
    listen          443;
    server_name     example.com;    #vhostドメイン
    server_tokens   off;

    ssl on;
    ssl_certificate     /etc/pki/tls/certs/example.com/server.crt;
    ssl_certificate_key /etc/pki/tls/private/example.com/server.key;

    proxy_set_header    Host                $http_host;
    proxy_set_header    X-Real-IP           $remote_addr;
    proxy_set_header    X-Forwarded-Host    $host;
    proxy_set_header    X-Forwarded-Server  $host;
    proxy_set_header    X-Forwarded-Proto   $scheme;
    proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;

    access_log  /var/log/nginx/access-example.com.log  main;

    location /test_example/ {
        proxy_pass    http://127.0.0.1:40001/test_example/;    #転送先サブディレクトリの指定
        proxy_redirect http:// https://;
    }
    location /prod_example/ {
        proxy_pass    http://127.0.0.1:40002/prod_example/;    #転送先サブディレクトリの指定
        proxy_redirect http:// https://;
    }

}
解説:ネストした変数の展開

最後に分かりにくいjinja2templateのfor文でネストした変数の展開を
taskの挙動をふくめて解説したいと思います。

# ネストした変数を展開
{% set if_vhost_name=item.0.vhost_name %}
{% for vhost in vhosts %}
{% for item in vhost.subdir %}
{% if vhost.vhost_name==if_vhost_name %}
   location /{{ item.subdir_name }}/ {
       proxy_pass    http://127.0.0.1:{{ item.redirect_port }}/{{ item.subdir_name }}/;
       proxy_redirect http:// https://;
   }
{% endif  %}
{% endfor %}
{% endfor %}

nginx.ymlでwith_subelementsを用いることでgroup_varsでネストされた変数を展開し、取得することができます。

nginx.ymlのwith_subelements一回目のループでitem.0.vhost_name=example.comを設定後にvhost.confを呼び出しているので
vhost.conf内のif_vhost_nameはif_vhost_name=item.0.vhost_name=example.comとなります。

次にvhost.confのvhost.vhost_nameはjinja2templateのfor文で
ネストされた変数を展開して取得しています。取得結果は以下のようになります。

{% for vhost in vhosts %}(外側)ループ1回目
vhost.vhost_name=example.com

{% for item in vhost.subdir %}(内側)ループ1回目
item.subdir_name=test_example
item.redirect_port=40001

{% for item in vhost.subdir %}(内側)ループ2回目
item.subdir_name=prod_example
item.redirect_port=40002

{% for vhost in vhosts %}(外側)ループ2回目
vhost.vhost_name=sample.com

{% for item in vhost.subdir %}(内側)ループ3回目
item.subdir_name=test_sample
item.redirect_port=40003

{% for item in vhost.subdir %}(内側)ループ4回目
item.subdir_name=prod_sample
item.redirect_port=40004

この時{% if vhost.vhost_name==if_vhost_name %}のif文で
nginx.ymlのtask側で取得した値を格納したif_vhost_nameとjinja2templete側で取得したvhost.vhost_nameを比較し、
値が一致した場合のみ処理を実行するように記述しています。

まとめ

いかがだったでしょうか?
jinja2templateでネストした変数を展開する部分に関してはわかりづらいですし、うまく説明できたかわかりませんが参考になれば幸いです。
また、間違いや指摘事項などございましたら是非教えてください。もっといい書き方があればなおしたいので。。

最後に、初めての投稿でここまで長い記事にする予定ではなかったのですがついつい長くなってしまいました。
次回以降どの程度のボリュームの記事を投稿するかわかりませんが
これから試行錯誤しながらも頑張って続けていきたいと思いますのでよろしくお願いします。