遅いAnsibleプレイブックのボトルネック特定と修正

パフォーマンスのボトルネックを特定し排除することで、Ansibleデプロイを大幅に高速化します。このガイドでは、遅いプレイブックのプロファイリング、ファクト収集の最適化、接続管理、タスク実行のチューニングに関する実践的な手順、設定例、ベストプラクティスを提供します。Ansibleの機能を活用して、効率的かつ迅速なインフラ自動化を実現しましょう。

遅いAnsibleプレイブックのボトルネック特定と修正

遅いAnsibleプレイブックはイライラさせられます。なぜなら、遅延が一箇所に集中していることはほとんどないからです。実行中、数秒はファクト収集に、さらに数秒はSSH接続の確立に費やされ、その後、ファイルを一度に1台のホストにコピーするのに数分かかることもあります。推測だけで対策を講じると、たいてい間違った箇所をチューニングしてしまいます。

まずは時間がどこで費やされているかを測定することから始めましょう。そして、最大の遅延原因から先に修正します。小規模な環境では、毎回パッケージマネージャーを実行する単一のシェルタスクが原因かもしれません。大規模な環境では、接続のセットアップ、ファクト収集、低いforks値、または意図以上に処理を直列化するプレイブックが原因であることが多いです。

Ansibleのパフォーマンス指標を理解する

具体的な最適化手法に入る前に、Ansibleのパフォーマンスを測定し解釈する方法を理解することが重要です。Ansibleには診断に役立つ組み込みのタイミング情報が用意されています。

詳細ログの前にタイミング出力を使用する

非常に詳細な出力は接続問題の解決に役立ちますが、パフォーマンス分析にはノイズが多すぎます。よりクリーンな最初のステップは、profile_tasksコールバックを使用することです。これにより、実行終了時にタスクの所要時間が表示されます。

ansible.cfgで:

[defaults]
callbacks_enabled = profile_tasks

その後、通常通りプレイブックを実行します:

ansible-playbook my_playbook.yml

最も遅いタスクから確認します。1つのタスクが実行時間の大半を占めている場合、forksについて朝から議論する必要はありません。

出力の詳細度を制御する

SSHの詳細、モジュール転送動作、リトライ、インタプリタの検出を確認する必要がある場合は、-vvvを使用します。通常のタイミング測定では、詳細ログが大量に出力され、重要な情報が埋もれてしまう可能性があります。

一般的なボトルネックと最適化戦略

Ansibleプレイブックの速度低下には複数の要因が考えられます。ここでは、一般的なボトルネックを探り、それらに対処するための実践的な戦略を提供します。

1. 過剰なファクト収集

デフォルトでは、Ansibleは各プレイの開始時に管理対象ホストからファクト(システム情報)を収集します。便利な反面、特に多数のホストや低速なネットワーク環境では時間がかかることがあります。プレイブックが収集したすべてのファクトを必要としない場合は、ファクト収集を無効化または制限できます。

ファクト収集の無効化

プレイのファクト収集を完全に無効にするには、gather_facts: noディレクティブを使用します:

- name: My Playbook
  hosts: webservers
  gather_facts: no
  tasks:
    - name: Ensure Apache is installed
      apt: name=apache2 state=present

ファクト収集の制限

一部のファクトだけが必要な場合は、gather_subsetを使用して収集するファクトを指定できます。

- name: My Playbook
  hosts: webservers
  gather_facts: yes
  gather_subset:
    - '!all'
    - '!any'
    - hardware
    - network
  tasks:
    - name: Use network facts
      debug: var=ansible_default_ipv4.address

ファクトのキャッシュ

ファクトが頻繁に変更されない環境では、ファクトをキャッシュすることで後続のプレイブック実行を大幅に高速化できます。Ansibleは複数のファクトキャッシュプラグイン(例:jsonfileredismemcached)をサポートしています。

ファクトキャッシュを有効にするには、ansible.cfgファイルで設定します:

[defaults]
fact_caching = jsonfile
fact_caching_connection = /path/to/ansible/facts_cache
fact_caching_timeout = 86400 # 24時間キャッシュ

これにより、プレイブックは利用可能な場合に自動的にキャッシュされたファクトを使用します。

2. 非効率なタスク実行

一部のタスクは本質的に遅かったり、非効率な方法で実行されたりする場合があります。

並列実行(フォーク)

Ansibleのデフォルト動作は、プレイ内でホストに対してタスクを順次実行することです。Ansibleが同時に管理するホストの並列プロセス数(フォーク)を増やすことができます。これはansible.cfgforks設定または-fコマンドラインオプションで制御します。

ansible.cfg:

[defaults]
forks = 10

コマンドライン:

ansible-playbook my_playbook.yml -f 10

ヒント: 適度なフォーク数から始め、コントロールノード、ネットワーク、ターゲットサービスを監視しながら徐々に増やしてください。フォークを増やすとデプロイは高速化できますが、パッケージリポジトリ、ロードバランサー、データベースマイグレーションステップを圧迫する可能性もあります。

冪等性と状態管理

タスクが冪等であることを確認してください。つまり、タスクを複数回実行しても、1回実行した場合と同じ結果になることを意味します。Ansibleモジュールは一般的に冪等になるように設計されていますが、カスタムスクリプトやコマンドはそうでない場合があります。タスク内の非効率なチェックもオーバーヘッドを増加させる可能性があります。

例えば、サービスが実行中かどうかを確認してから起動するコマンドを実行する代わりに、専用のserviceモジュールを使用します:

非効率:

- name: Start service (inefficient check)
  command: systemctl start my_service.service || true
  when: "'inactive' in service_status.stdout"
  register: service_status
  changed_when: false # このタスクは状態を変更しない

効率的(serviceモジュールを使用):

- name: Ensure my_service is running
  service:
    name: my_service
    state: started

長時間実行操作でのasyncpollの使用

完了までに時間がかかる可能性のあるタスク(例:パッケージアップグレード、データベースマイグレーション)では、Ansibleのasyncおよびpollディレクティブを使用することで、プレイブックがハングするのを防げます。

  • async: タスクがバックグラウンドで実行される最大時間を指定します。
  • poll: Ansibleが非同期タスクのステータスを確認する頻度を指定します。
- name: Perform a long-running operation
  command: /usr/local/bin/long_script.sh
  async: 3600 # 最大1時間実行
  poll: 60    # 60秒ごとにステータス確認

3. 接続の最適化

Ansibleが管理ノードに接続する方法は、パフォーマンスに大きな影響を与えます。

SSH接続多重化

SSH多重化(ControlMaster)により、複数のSSHセッションが単一のネットワーク接続を共有できます。これにより、同じホストへの後続の接続が大幅に高速化されます。

ansible.cfgで有効にします:

[ssh_connection]
control_master = auto
control_path = ~/.ansible/cp/ansible-%%r@%%h:%%p
control_persist = 600 # 制御接続を10分間維持

SSHリトライとタイムアウト

SSH接続パラメータを調整することで、ホストが一時的に利用できない場合の不要な遅延を防げます。

[ssh_connection]
sf_retries = 3
sf_delay = 1
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o ConnectionAttempts=5 -o ConnectTimeout=10

pipeliningの使用

パイプライン処理により、Ansibleは各コマンドに対して新しいSSHセッションを作成せずに、リモートホストで直接コマンドを実行できます。これにより、多くのタスクのオーバーヘッドが大幅に削減されます。

ansible.cfgで有効にします:

[ssh_connection]
pipelining = True

警告: パイプライン処理は、特に古いディストリビューションでsudoにrequirettyが有効になっている場合など、一部の権限昇格設定と競合する可能性があります。本番プレイブックで使用するのと同じbecomeパスでテストしてください。

4. プレイブックの構造とロジックの最適化

プレイブックの書き方自体が速度低下の原因になることがあります。

delegate_torun_onceの使用

タスクを1台のホストでのみ実行すればよいが、他の複数ホストに影響を与える場合(例:ロードバランサーの再起動)、delegate_torun_onceを使用して効率的に実行します。

- name: Restart load balancer
  service: name=haproxy state=restarted
  delegate_to: lb_server_1
  run_once: true

ロールとインクルードの戦略的使用

ロールとインクルードは整理に役立ちますが、深くネストされたり非効率な構造のインクルードはわずかなオーバーヘッドを追加する可能性があります。ロールの依存関係とインクルードロジックがクリーンであることを確認してください。

serialキーワード

serialキーワードは、プレイ内で同時に処理できるホスト数を制限します。これは制御されたロールアウトによく使用されますが、希望するパフォーマンスに対して低すぎる値に設定するとボトルネックになる可能性もあります。

- name: Deploy application to a subset of servers
  hosts: appservers
  serial: 2 # 一度に2台のホストでのみ実行
  tasks:
    - name: Update application code
      copy: src=app/ dest=/opt/app/

意図的に並列性を制限していない場合は、serialが設定されていないか、十分に高い数値に設定されていることを確認してください。

遅いタスクを修正する、単なる遅い転送だけではない

接続チューニングは、プレイブックに多数の短いタスクがある場合に役立ちます。毎回過剰な作業を行うタスクを修正するわけではありません。

よくある例は、shellを使用してパッケージコマンドを実行することです:

- name: Install nginx with shell
  shell: apt-get update && apt-get install -y nginx

このタスクはAnsibleにとって状態を把握するのが難しく、毎回changedと報告されたり、毎回パッケージメタデータを更新したり、構造化された障害情報が得られにくくなります。状態を理解するモジュールを優先しましょう:

- name: Refresh apt cache when needed
  apt:
    update_cache: true
    cache_valid_time: 3600

- name: Install nginx
  apt:
    name: nginx
    state: present

同じ考え方はファイルのデプロイにも当てはまります。copyモジュールを使用して数百の小さなファイルを含む大きなディレクトリをコピーすると、Ansibleがファイルごとにチェックと転送を行うため遅くなる可能性があります。アプリケーションリリースでは、アーティファクトを一度ビルドし、アーカイブをアップロードしてターゲットで展開する方が高速な場合があります:

- name: Upload release artifact
  copy:
    src: dist/app.tar.gz
    dest: /tmp/app.tar.gz

- name: Unpack release
  unarchive:
    src: /tmp/app.tar.gz
    dest: /opt/app
    remote_src: true

これは常に正しい設計とは限りませんが、正しい問いかけです: Ansibleに何千もの小さな判断を同期させる代わりに、1つのアーティファクトで明確にできないでしょうか?

インベントリと変数の処理を確認する

動的インベントリも、もう一つの隠れた遅延要因です。プレイブックを実行するたびにクラウドAPIを呼び出し、ページネーションを待ち、ホストリスト全体を再構築していると、最初のタスクが始まる前からプレイブックが遅く感じられるかもしれません。プラグインがサポートしている場合はインベントリデータをキャッシュし、ホストパターンを狭く保ちましょう。allに対してWebデプロイを実行し、when条件でほとんどのホストをスキップするのは時間の無駄です。

変数の読み込みも複雑になりがちです。大きなgroup_vars/all.ymlファイル、高コストなルックアップ、繰り返されるテンプレートレンダリングは積み重なります。ルックアップがシークレットマネージャーやHTTPエンドポイントにアクセスする場合、多くのタスクで呼び出す代わりに、結果をプレイごとに1回変数に格納します。

プロファイリングツールとテクニック

Ansible自身の詳細出力に加えて、専用のプロファイリングがより深い洞察を提供できます。

ansible-playbook --syntax-check

このコマンドはプレイブックの構文エラーをチェックしますが、実行はしません。フル実行の前にプレイブックの構造を検証する簡単な方法です。

Ansibleイベントのログ記録

Ansibleは実行イベントをファイルにログ記録でき、後で分析できます。これは長時間実行されるプレイブックや監査に特に便利です。

ansible.cfgでイベントログを設定します:

[defaults]
log_path = /var/log/ansible.log

カスタムコールバックプラグイン

高度なプロファイリングのために、カスタムコールバックプラグインを作成して特定のメトリクスをキャプチャしたり、プレイブック実行に関するカスタムレポートを作成したりできます。

待機にはAsyncを使うが、すべてに使うわけではない

プレイブックの時間の一部は実際の待機です: サービスの再起動、パッケージのビルド、クラウドインスタンスの準備完了、データベースマイグレーションなど、正当に数分かかるものです。これらのタスクがすべてのホストを同期的にブロックする必要がない場合、Ansibleのasyncpollが役立ちます。

- name: Start long-running report generation
  command: /opt/tools/build-report
  async: 1800
  poll: 0
  register: report_job

- name: Check report job
  async_status:
    jid: "{{ report_job.ansible_job_id }}"
  register: report_status
  until: report_status.finished
  retries: 60
  delay: 10

注意して使用してください。Asyncは安全でないタスクを並列化するための近道ではありません。10台のホストがすべて同時にデータベースマイグレーションを開始すると、プレイブックは早く終了しても環境が壊れる可能性があります。Asyncは、Ansibleが後で確認する間、ターゲットが安全に処理を続行できる独立した作業に最適です。

ユーザーの視点から測定する

プレイブックは技術的に高速でも、オペレーターが有用なフィードバックを得るまでに長く待たされると、遅く感じられます。大規模なデプロイを明確なタスク名を持つフェーズに分割しましょう: 事前チェック、アーティファクトアップロード、サービス更新、ヘルスチェック、クリーンアップ。フェーズが遅い場合、プロファイル出力とターミナルを見ている人間の両方が時間の費やされた場所を理解できます。

これはロールバックの判断にも役立ちます。プレイブックが最初のヘルスチェックまでに12分かかる場合、障害の発見が遅すぎるかもしれません。ディスク容量、パッケージリポジトリへのアクセス、サービス認証情報をチェックする小さな事前タスクは、SSHセットアップから1秒削るよりもはるかに多くの時間を節約できます。

最高のAnsibleパフォーマンス作業は、良い意味で退屈です: タスクタイミングを有効にし、最も遅いステップを見つけ、1つ変更し、再度測定する。ファクトが必要ない場合のみ無効化する。ターゲットと依存関係が並列性を処理できる場合のみforksを増やす。ノイズの多いシェルコマンドを状態認識モジュールに置き換える。接続オーバーヘッドが実際に問題の一部であることを確認した後に、SSH多重化とパイプライン処理を使用する。

その規律により、プレイブックの可読性を保ちながら高速化できます。早く終わるが誰も理解できないデプロイは、明日の障害がプログレスバーを短くしただけのものです。