Ansibleのメジャーバージョンアップをやってみた
こんにちは。
株式会社ココナラのインフラ・SREチーム所属の かず と申します。
今回は、Ansibleのメジャーバージョンアップを行ったため、その過程にて苦労したこと・工夫したことを紹介します。
進め方
まずはどのように進めるのかを検討します。
極端ですが、いきなり対象バージョンへあげて実行し、出てきたエラーを潰すという方法でも対応は可能です。実際にこの方法が最適な場合もあります。
しかし今回は、メジャーバーションアップであること、あとで述べる共通箇所に修正が多く必要となること、の2つがある程度わかっていたため、次のステップで進めることとしました。
- 現状の確認
- 変更点の確認
- 実装の修正
- 検証
上記のステップにて進めることにより手戻りを抑え、対応を計画的に進められると考えました。
現状の確認
「彼を知り己を知れば百戦殆からず」という故事成語がありますが、ここでいう「己」が現状の確認にあたります。
Ansibleのコードは、私が入社するよりも前に書かれているものでした。
そのため、まずはあらためて開始時点の実装を、どのような制約が出てきそうであるか、独自の実装をしているところはないのかを中心にながめ直しました。
そして、対応において課題となりそうな点がわかりました。
- 共通モジュールの多用
上記の内容について、具体的にどういうことかを説明します。
共通モジュールの多用
おそらく思想は、「共通のmoduleを定義し、呼び出し側で必要なものを利用する」という実装だと思います。
そのため、以下のようなディレクトリ構造をとっています。
# Ansibleを管理するディレクトリの構成
$(ansible_root_dir)
├── common
│ ├── playbooks
│ └── roles
│ ├── role1
│ ├── role2
│ ...
├── Service1
│ ├── ansible.cfg
│ ├── dump_vars
│ ├── files
│ ├── group_vars
│ ├── host_vars
│ ├── inventory_${設定環境毎の名前}
│ ├── playbooks
│ ├── site.yml
│ └── ssh_config
├── Service2
├── Service3
...
上記での common/roles
以下が共通するパーツにあたる箇所であり、 Service
側でそのパーツを組み立てて実行する形でした。
つまり、バージョンアップによる影響には「パーツそのもの」と「パーツの組み立て方」の2パターンがあり、さらにこのパーツは共用しているものであるため、サービスごとに順序立てて対応するのが難しいという状態でした。
具体的なコードは、下記の例をご覧ください。
---
# 個別処理として記載されているが最終的には共通処理で実装されている例
- name: install td-agent plugin
hosts:
- batch
roles:
- ../../common/roles/td-agent_plugin
tags:
- td-agent
- td-agent_plugin
変更点の確認
「彼を知り己を知れば百戦殆からず」でいうところの「彼」にあたるのがAnsible側の変更点です。
愚直な作業ではありますが、採用する可能性があるバージョンまでのリリースノート[1]を読み、記載内容を把握することから始めました。
リリースノートに記載される情報には、処理に関係ない変更が多く混じるため、対応の早い段階で関係や対応の有無を大まかにフィルタリングしておくことが重要です。
今回は新規機能の利用目的でのメジャーバージョンアップ対応ではないため、以下のように情報を整理しました。
- バージョン毎に確認が必要な変更をコピー&ペーストしてリストアップする
- 影響の有無と処理への影響度に注目して確認を行う
- 影響度が高い変更に対して、精査を行い対応方針を決める
ここでいう「影響度が高い変更」は、次で述べる破壊的変更になります。
破壊的な変更
Pythonの最低利用バージョンの変更などの、対応するにも相応の工数がかかり、かつ優先的に行う必要がある変更点です。
リリースノートの見出しでいうと、次の3つが主な記載箇所となります。
- Major Changes
- Breaking Changes / Porting Guide
- Removed Features
また、Known Issues
のチェックもしておくとよいでしょう。この項目には、バージョンアップに伴う一時的な制約や条件が記載されていることがあるためです。
実装の修正
実装の修正への対応内容は、「破壊的な影響を伴う変更への対応」と「今後のアップデートを見据えた対応」の2つになります。
それぞれの対応は、事前に対応方針を決めた上で、実装の修正を行いました。
破壊的な影響を伴う変更への対応
今回対応した中で破壊的な影響を伴う変更は、 ansible.cfg
における hash_behaviour = merge
の廃止でした。
上記の機能が廃止された主な影響は、設定ファイルを取り込む独自の取り込み処理の挙動が変化し、処理結果が変化することです。
影響は簡単な検証により、期待通りの結果にならないことも事前に確認がとれたため、下記の2つの案を比べて実現性の高い案を採用しました。
- 独自処理を廃止し、仕様に沿った方法に統一をする
- 変数を取り込む独自処理は残してメジャーバージョンアップ対応をする
検討した結果、下の方針を採用しました。採用理由は、影響による変更を局所化することで、レビュー観点も絞れるため、全体としての対応が軽くなることが期待できるためです。
今後のアップデートを見据えて
先の破壊的な変更に対し、今回は「変数を取り込む独自処理は残してメジャーバージョンアップ対応をする」方針を選択しました。
しかし、どんなバージョンにもEOLは必ず訪れるため、単純にバージョンアップを完了できればそれでよいというわけではありません。
弊社のAnsibleでは変数ファイルの取り込みに独自のルールを組み込んでおり、それが処理結果を大きく左右しておりました。
そのため、今後定期的に行われるバージョンアップを見据えて以下の対応をしました。
- 独自処理を整備し、継続的にメンテナンスし易くした
- 廃止されたモジュールを利用している箇所の再構築は、汎用モジュールと標準機能を用いて実装を行った
- 廃止が予定されているモジュールや設定においても、汎用モジュールと標準機能を用いて実装を行った
検証
リリースノートにて変更内容を把握し、修正対応を行いましたが、実際に処理が期待通りに動くのかは別になるため、各環境へ処理を実行し検証をしました。
検証を通過できる条件は、「メジャーバージョンアップ対応前後の処理結果において差分がないこと」になります。
検証フェーズにて検知した変化の一例を以下に示します。
リリースノートには記載が見られなかった挙動の変更
リリースノートの記載が見られなかった変更が存在しました。
具体的な影響の一例として、dry-run実行時におけるhandler処理が、メジャーバージョンアップ後は実行されるようになりました。
リリースノートやドキュメントには記載が見つからず、不具合か仕様か判明しなかったことから、再び挙動が変更される可能性も考慮しつつ対応しました。
---
# dry-runにおいても実行されてしまう処理への対応例
- name: restart chrony
service:
name: "{{ my_vars.middlewares.chrony.service_name | default('chrony') }}"
state: restarted
ignore_errors: "{{ ansible_check_mode }}"
まとめ
バージョンアップ対応の一環として、Ansibleにおけるメジャーバージョンアップ対応の事例を紹介しました。
公式ドキュメントの案内から外れた設定や変更点に記載がない事項への対応も含め、バージョンアップ対応における参考例になれば幸いです。
ココナラへ興味をお持ちの方は、下記にフォームを用意しました。
ブログの内容への感想、カジュアルにココナラの技術組織の話をしてみたい方は、こちらからご覧いただけます!
※ブログ閲覧者の方限定のカジュアル面談の応募フォームとなります!
エンジニアの募集職種一覧はこちらになります!
Discussion