オンプレとAWSのハイブリット構成で、数千台の仮想マシンを Ansible で構成管理している環境で、実際に経験した問題と、有効だったリファクタリングについて説明します
少し前にTweetした↓の内容を、より詳細に書いています
Ansibleのベストプラクティスでplaybook, inventory, vars, rolesを全部独立したディレクトリで管理してるけど、これスパゲッティにならずにメンテナンスできるのだろうか?
— 入水岬@インフラエンジニア (@Mjusui) September 28, 2019
弊社では無理でinventoryごとにディレクトリ分割してplaybook, role, varsはinventoryにひもづけてます
気がついたら、こんな事態に
社内でサービスの新機能開発プロジェクトが進行している中、インフラエンジニアとして、新機能用のサーバ構築をしていたときのことです
roles/ec2
roles/ec2/create
roles/ec2-create
EC2インスタンスを作成するための roles が乱立していました……
もしやと思いオンプレのコードを確認してみると
roles/create-vm
roles/virt-install
roles/v1.0/kvm/guest/install
やはり同じような roles が乱立していました
仮想マシンの作成だけでなく apache2 nginx mysql などミドルウェアのインストールをするroleも、同じように別名で乱立している状態でした
なぜこんなことに?
AnsibleにおけるRolesとは
まずひとつは、そもそも roles という概念をとらえ損ねていることが原因でしょう
roles というのは言葉どおり役割ですから**「仮想マシン作成」や「Apache2をインストール」のような処理の単位で分割するのではなく「Webサーバ」や「DBサーバ」**のように、もっと大きな単位で分割すべきものです
公式ドキュメントの例(ユーザガイド - Roles)でも、まさにそのように例示されています
roles を細かく分割すれば、それだけ汎用性、再利用性は高まりますが、中途半端な汎用性しか持ち合わせていないと ほとんど同じ処理だけど、一部分だけ特殊なことをしたい という場合に対応しきれなくなり、仕方なく新しく roles を作成する、ということになりがちです
コードの依存関係
もうひとつは、メンテナンス性の問題です
1つ目とつながる部分もあるのですが、roles の汎用性、再利用性が高すぎると、無数のplaybookからロードされるようになるため、あるroleを変更した際 どのplaybookに、あるいは、どのinventoryに影響するか分からない といった状態になります
また、Ansibleのディレクトリ構成も、依存関係の把握を難しくしている要因の1つだと思います
下記はAnsibleの公式ドキュメントで、ディレクトリ構成のベストプラクティス([ユーザガイド - ディレクトリ構成](https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html?highlight=best practice#directory-layout))として紹介されているものの1つを inventory vars modules playbooks roles という単位で分割して記載したものです(見やすさのため一部、省略)
production # inventory file for production servers
staging # inventory file for staging environment
group_vars/
group1.yml # here we assign variables to particular groups
group2.yml
host_vars/
hostname1.yml # here we assign variables to particular systems
hostname2.yml
library/ # if any custom modules, put them here (optional)
module_utils/ # if any custom module_utils to support modules, put them here (optional)
filter_plugins/ # if any custom filter plugins, put them here (optional)
site.yml # master playbook
webservers.yml # playbook for webserver tier
dbservers.yml # playbook for dbserver tier
roles/
common/ # this hierarchy represents a "role"
~ commonの中身は省略 ~
webtier/ # same kind of structure as "common" was above, done for the webtier role
monitoring/ # ""
fooapp/ # ""
この構成では inventory vars playbooks roles のディレクトリが全て同じ階層にあるので、どのplaybookから、どのroles,vars,inventoryが参照されているのか分かりづらくなっています
これらに加えてさらに include_role や dependencies でroleの中でroleを呼ぶような処理をしていると、コードに変更を加えた際の影響範囲は、いっそうは分かりづらくなっていきます
こうして、既存のroleのメンテナンスコストが増大し、結局あらたにroleを作成した方が速い、という流れになりがちなのです
ディレクトリ構成を変えてみた
上記で紹介した問題は roles を、汎用的になりすぎないように分割することで、ある程度は解消できると思います
ただそれでも roles - inventory - playbook の依存関係が不明瞭なことには変わりありません
そこで以下のようにディレクトリ構成を変えてみました
regionA/ # オンプレ、AWSなど
common/ # 一応commonも作る。ただし重複するコードでも、極力commonは利用しない
roles/
vars/
serverA/
inventory/
roles/
playbook1.yml (varsはディレクトリ化せず、playbookの中に直接書く)
playbook2.yml (varsはディレクトリ化せず、playbookの中に直接書く)
...
serverB/
inventory/
roles/
playbook1.yml (varsはディレクトリ化せず、playbookの中に直接書く)
playbook2.yml (varsはディレクトリ化せず、playbookの中に直接書く)
...
...
regionB/ # オンプレ、AWSなど
common/ # 一応commonも作る。ただし重複するコードでも、極力commonは利用しない
serverA/
inventory/
roles/
playbook1.yml (varsはディレクトリ化せず、playbookの中に直接書く)
playbook2.yml (varsはディレクトリ化せず、playbookの中に直接書く)
...
serverB/
inventory/
roles/
playbook1.yml (varsはディレクトリ化せず、playbookの中に直接書く)
playbook2.yml (varsはディレクトリ化せず、playbookの中に直接書く)
...
...
このディレクトリ構成の特徴
-
serverAserverBというのがWebサーバとかDBサーバに対応する概念です。またAWSやオンプレなど、サーバが動作する基盤やネットワークからして異なるものはregionとしてディレクトリを分割しています -
regionを分ける理由はrolesの中でregion判定して条件分岐する(AWSの場合だけ実行するなど)といった記述を避けるためです - 他にも、よくあるAnsibleの書き方として
CentOSUbuntuといったディストリビューションごとに分岐処理するようなコードがありますが、これも「serverAは同じディストリビューションであること」という制約を加えればserverAディレクトリの中では ディストリビューションを調べるコードを書く必要がなくなります - よくよく考えるてみると 「Webサーバ」のroleは「Webサーバ以外」のinventoryやplaybookとひもづける必要はない ということが分かってくるでしょう。なので、関連するroles, playbooks, inventoryは一つのディレクトリにまとめて「他のディレクトリとの依存関係は持たない」とした方が、コード改修時の影響範囲が明確でメンテナンス性が向上します
- この構成では、極力
commonは利用しない方針にします
例えばserverAとserverBの両方にApache2をインストールする、といった重複する処理があった場合にもcommonは利用せずserverAとserverBのそれぞれのディレクトリに同じコードを配置します
こうしておけば将来的にserverBだけApache2のバージョンを上げたいとか、httpd.confを変更したいなど、サーバ固有のカスタマイズが必要になったときにserverAとの 共通部分がないことによって、影響範囲が極小化され、コードの改修がスムーズに進むためです - それと
varsはディレクトリ化せずにplaybookに直接書いた方が、コードの可読性が高まります(ただしinventory_varsはinventoryに書いた方がよいです)
他にも嬉しい副作用
- 複数サーバ間で共有しているコードを極力排除することで、それぞれの構築担当者の書いたコードのconflictが減り、作業の並列性が向上します
-
serverAserverBなどのディレクトリ内では、構築担当者が比較的に自由なコーディングができるため、あらたに業務に参加する方の障壁が少なくてすみます - たとえばAWSからAzureへ意向するような場合でも
regionを新しく追加するだけで済み、既存のコードとの依存関係を考える必要がないため、スムーズに進みます
最後に
Ansibleはコードのシンプルさを保つために、高度なプログラミング言語に実装されているような安全機能が実装されておらず、プログラマが注意してコード設計をする必要があります
ですが、それが面白いところでもあります