てくなべ (tekunabe)

ansible / network automation / 学習メモ

[Ansible/Terraform] Terraform 入りの Execution Environment (EE) をビルドする定義ファイル例

はじめに

Ansible を Execution Environment (EE: コンテナベースの Ansible 実行環境)を利用していて、Ansible から EE の中の Terraform を呼び出す場合、EE の中に Terraform をインストールしておく必要があります。

AWX の標準の EE である quay.io/ansible/awx-ee には Terraform はライセンスの都合上入りません。ansible-automation-platform-24/ee-supported-rhel9 のよな AAP の EE にも入らないはずです

そのため、Terraform が必要な場合は、別途ビルド必要があります。

Terraform 入り EE を ビルドする際に参考になる execution-environment.yml を見つけましたのでご紹介します。

execution-environment.yml

以下のファイルです。

github.com

なお、このリポジトリの URL は、AAP 2.5 のドキュメントにも掲載される予定のようです(予想ですが、関連 PR)。

おためし

少しだけアレンジしたファイルでビルドしてみます。

環境は ansible-builder 3.0.1 です。

---
version: 3

images:
  base_image:
    name: quay.io/centos/centos:stream9

dependencies:
  ansible_core:
    package_pip: ansible-core
  ansible_runner:
    package_pip: ansible-runner
  galaxy:
    collections:
      - name: cloud.terraform     # 例では ファイル名指定ですが、ここでは直接指定。このコレクションが Terraform を利用する

additional_build_steps:
  append_base: |
    RUN yum install -y git
    RUN curl https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo | tee /etc/yum.repos.d/terraform.repo
    RUN yum install -y terraform
$ ansible-builder build -t localhost/terraform-ee -v 3
...(略)...
#11 [base 7/8] RUN curl https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo | tee /etc/yum.repos.d/terraform.repo
#11 1.667   % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
#11 1.667                                  Dload  Upload   Total   Spent    Left  Speed
100   381  100   381    0     0    385      0 --:--:-- --:--:-- --:--:--   384
#11 2.656 [hashicorp]
#11 2.656 name=Hashicorp Stable - $basearch
#11 2.656 baseurl=https://rpm.releases.hashicorp.com/RHEL/$releasever/$basearch/stable
#11 2.656 enabled=1
#11 2.656 gpgcheck=1
#11 2.656 gpgkey=https://rpm.releases.hashicorp.com/gpg
#11 2.656 
#11 2.656 [hashicorp-test]
#11 2.656 name=Hashicorp Test - $basearch
#11 2.656 baseurl=https://rpm.releases.hashicorp.com/RHEL/$releasever/$basearch/test
#11 2.656 enabled=0
#11 2.656 gpgcheck=1
#11 2.656 gpgkey=https://rpm.releases.hashicorp.com/gpg
#11 DONE 2.7s

#12 [base 8/8] RUN yum install -y terraform
#12 2.212 Hashicorp Stable - x86_64                       927 kB/s | 1.4 MB     00:01    
#12 2.431 Last metadata expiration check: 0:00:01 ago on Mon May 13 05:05:58 2024.
#12 2.809 Dependencies resolved.
#12 2.809 ================================================================================
#12 2.809  Package            Architecture    Version            Repository          Size
#12 2.809 ================================================================================
#12 2.809 Installing:
#12 2.809  terraform          x86_64          1.8.3-1            hashicorp           26 M
#12 2.809 
#12 2.809 Transaction Summary
#12 2.809 ================================================================================
#12 2.809 Install  1 Package
#12 2.809 
#12 2.810 Total download size: 26 M
#12 2.810 Installed size: 84 M
#12 2.810 Downloading Packages:
#12 5.056 terraform-1.8.3-1.x86_64.rpm                     12 MB/s |  26 MB     00:02    
#12 5.058 --------------------------------------------------------------------------------
#12 5.059 Total                                            12 MB/s |  26 MB     00:02     
#12 5.130 Hashicorp Stable - x86_64                        96 kB/s | 3.9 kB     00:00    
#12 5.170 Importing GPG key 0xA621E701:
#12 5.170  Userid     : "HashiCorp Security (HashiCorp Package Signing) <security+packaging@hashicorp.com>"
#12 5.170  Fingerprint: 798A EC65 4E5C 1542 8C8E 42EE AA16 FCBC A621 E701
#12 5.170  From       : https://rpm.releases.hashicorp.com/gpg
#12 5.302 Key imported successfully
#12 5.332 Running transaction check
#12 5.333 Transaction check succeeded.
#12 5.333 Running transaction test
#12 5.361 Transaction test succeeded.
#12 5.361 Running transaction
#12 5.411   Preparing        :                                                        1/1 
#12 5.922   Installing       : terraform-1.8.3-1.x86_64                               1/1 
#12 8.086   Verifying        : terraform-1.8.3-1.x86_64                               1/1 
#12 8.119 
#12 8.119 Installed:
#12 8.119   terraform-1.8.3-1.x86_64                                                      
#12 8.119 
#12 8.119 Complete!
#12 DONE 8.2s
...(略)...
#32 naming to localhost/terraform-ee 0.0s done
#32 DONE 1.7s

Complete! The build context can be found at: /home/sakana/ee3/context

ビルドできたので、試しのバージョンを表示させてみます。

$ docker run -it localhost/terraform-ee terraform --version
Terraform v1.8.3
on linux_amd64

無事にインストールできたようです。

これでこの EE で cloud.terraformコレクションが利用できるようになったはずです。

[Asnible] Playbook の品質改善プルリクを出してくれる Ansible code bot を試してみた

はじめに

AnsibleFet 2024 のキーノートで、Ansible code bot というものを知りました。

動画の 17:45 頃から。

GitHub 上の Playbook の品質上の修正提案のプルリクエストを出してくれる bot です。

おもしろそうだったの試してみました。

手順

スクショを取り損ねてしまったのですが、以下のページで手順がまとまっています。

access.redhat.com

有効な AAP サブスクリプションが必要で、Red Hat のサイトにログインする手順があります。

Your organization must have an active subscription to Red Hat Ansible Automation Platform to use the Ansible code bot.

今回はパブリックリポジトリで試しましたが、上記ドキュメントによるとプライベートでも対応しているようです。

管理画面と手動スキャン

Ansible code bot には以下のような管理画面があります。ここから手動スキャンを開始できます。

Ansible code bot 管理画面

スキャン履歴もこの画面から確認できます。

作成されたプルリク

今回作成されたプルリクがこちら。

作成されたプルリク

いくつか修正されましたが、今回の範囲では ansible-lint --fix 相当のように見えました。コミットメッセージにもその旨の記載がありました。

修正内容

定期スキャン

定期スキャンするには、対象リポジトリ.github/ansible-code-bot.yml にスケジュールルールを定義しておく必要があります。

schedule:
  interval: daily     # weekly や  monthly も可

時刻は AM 9:00 UTC で固定のようです。

参考: Chapter 7. Installing and configuring the Ansible code bot Red Hat Ansible Lightspeed with IBM watsonx Code Assistant 2.x_latest | Red Hat Customer Portal

他のリポジトリでのプルリク例

以下のように検索すると、他のリポジトリで Ansible Code bot が出したプルリクが見れます。

github.com

おわりに

簡単ですが、Ansible code bot を試しました。今回の修正内容だけであれば、作りこめば他の方法でも実現できそうではありますが、おもったよりさくっと導入できたのは便利でした。

Red Hat Summit 2024 のキーノートで気になったキーワード

はじめに

2024/05/6-9 (現地時間)、デンバーRed Hat Summit 2024 と AnsibleFest 2024 が開催されています。

このうち、AnsiblefFest のキーノートについては以下の記事でふれました。

tekunabe.hatenablog.jp

本記事では、Red Hat Summit 側のキーノート内で出てきた気になった言葉をまとめます。

理解が浅く、箇条書き+リンク集レベルです・・

基本は Red Hat Summit 関連のプレスリリースまとめ を追うのがよさそうです。

キーノート動画

www.youtube.com

www.youtube.com

気になったキーワード

[Ansible] AnsibleFest 2024 のキーノートで気になったポイント(事例、Event-Driven Ansible、Policy as Code など)

はじめに

2024/05/6-9 (現地時間)、デンバーRed Hat Summit 2024 と AnsibleFest 2024 が開催されています。去年からこの 2つのイベントが合同開催になりました。

私は現地には行っていませんが、両イベント合計4つのキーノートを YouTube Live で見れました。

このうち、AnsibleFest のキーノートの気になったパートの個人的なまとめと所感を記載します。パートによって掘り下げの濃淡がありますがご了承ください。

動画の文字起こしや翻訳機能を参考にしていますが、まとめは誤訳や誤解があるかもしれませんので、正確なところは動画を参照してください。

なお、キーノート以外は後日アーカイブが公開されるセッションもあるかもしれませんので、イベントページ や、セッションカタログAnsible の X アカウント などをチェックしておくと良さそうです。

■ Day1 Ansible Fest Keynote: Adopting a mission-critical automation mindset

www.youtube.com

ADP's Automation Journey

動画は 6:14 頃から。

  • ADP (Automatic Data Processing) の Automation Journey
  • Ansible はオープンソースであり、コミュニティが活発であることも気に入った
  • CLI 以上のものが必要になり AWX を導入した
  • 社内で注目され始めた
  • 社内でトレーニングを実施、好評
  • 社内に自動化関連の月次ミーティングの場が誕生
  • Dev と Ops の垣根がなくなって団結感が強まった
  • 企業としても成長するなか、システムダウン時に誰を頼ればよいのか
  • サポートが欲しくなってきたため AAP を導入
  • 当初(AWX)サポートがなかったことに不安だったチームも参加してきた

やはり AAP にはサポートがあるのが強みですね。

Ansible Automation Platform Features

17:03からしばらく表示される画面(左上に Ansible Automation Platform のロゴ)では、左のメニューに以下のような項目が見えます。

  • Projects
  • Automation Execution
  • Automation Decisions (EDA)
  • Automation Infrastructure
  • Automation Content
  • Automation Analytics

見たことがないのですが、どうやらこのようにいろいろ統合した UI が AAP 2.5 に導入されるということでしょうか・・?気になります。

他、17:45 からの Ansible code bot という、自動でコード修正のプルリクを作成する bot の話がありました。

画面ではリポジトリTopic のところに ansible-code-bot-scan を設定する様子が映っていました。これで bot が有効化されるということでしょうか。

[2024/05/10 追記] あとで調べたらこちらの手順がありました。Chapter 7. Installing and configuring the Ansible code bot Red Hat Ansible Lightspeed with IBM watsonx Code Assistant 2.x_latest | Red Hat Customer Portal

なので試してみました。

tekunabe.hatenablog.jp

Southwest Airlines: Mission-Critical Network Automation

動画は 19:41 頃から。

  • ミッションクリティカルな環境
    • Southwest Airlines、アメリカの航空会社
    • 120の空港、1日4,000便以上
    • 5,000台以上のネットワーク機器を管理
  • Cisco IOS のアップグレード
    • Cisco IOS のネットワーク機器のバージョン統一(an average process on your Cisco IOS networking devices を意訳)
    • いままではアップグレードはスイッチ1台あたり30-45分かかっていた
    • 飛行機が飛んでいるときにアップグレードするわけでもなく、メンテナンスウィンドウが短いという特性がある
    • AAP によって短いメンテナンスウィンドウで多くの台数のアップグレードできるようになった
  • 新しい空港のインフラの構築
    • Ansible でゴールデンコンフィグを投入することで、素早くオンボードできる
  • コンフィグの標準化
    • 空港、データーセンター、キャンパス全体でコンフィグを標準化したいと考えた
    • IaC の考え方のもと、モジュール化
    • 現在は新拠点で必要なものの 90% のベースラインがあり、細かい差分はわずかな時間で設定できる
  • 社内で開発したポータル
    • NES(Network Engineering Suite)ポータル、バックグラウンドに Ansible を利用
    • ネットワークの可視化
    • ゴールデンコンフィグの生成、フェイルオーバー
    • ステータスチェックによる異常検出と対処
      • 手動の時は 100以上のコマンドを実行してチェックする必要があった
    • イベント・ドリブン・アーキテクチャを導入することを検討している
      • リアルタイムに状況を判断して自己修復する世界を目指す

コンフィグの標準化は自動化以前にやっておくとよさそうと思う一方で、どこまで標準化して、例外や個別コンフィグをどこまで許容して、どう組み込むかを考えるのはなかなか難しかったのではないかと思いました。

自己修復の世界観も興味深いです。機能的な仕組みはなんとなく想像がつくものの、異常と対処のパターン化、フォルスポジティブやフォルスネガティブ発生時の対応のように、基準や運用手順を作っていくのが難しそうだと思いました。

Preparing for the Future: AI, Automation, and the Next IT Revolution

動画は 31:40 頃から

Mission-Critical automation midset として以下の3つにまとめられていました。

  • Unifying teams to unlock truly strategic automation
  • Using automation as a force multiplier
  • Building an automated foundation for AI adoption

うまく咀嚼できませんが、気になった方は動画をご覧ください。

■ Day2 AnsibleFest keynote: Automation, AI, and the next enterprise IT revolution

www.youtube.com

動画は 6:43 頃から。

新しいツールと文化のお話。

  • Ansible を採用して、先駆者はいたが、組織全体には定着しなかった
  • オープン性、透明性、知識共有、の文化を育成することにした
  • 順応性(Adaptability)は重要
    • 新しいツールやプロセスをすぐ試すことを恐れてはいけない
  • CLI を理想としていたが、UI(おそらくGUIのこと)を使うチームがあった
    • UI をなくすと、使っていたチームを排除することになる
    • そこでUIにアクセスできるサンドボックス環境を用意し、JSON に変換することで、快適に開発できる王になった
    • (おそらく、Playbook や CLI のみで実行や確認するのではなく、慣れたGUIと行き来しながら開発できるようにしたということ)
  • 好奇心は重要
  • PowerShellBash など異なる言語を使っていたチームたちは、Ansibleを通じて共通のフォーマットを持ち、プレイブックの共有をするようになった

Ansible を通じて共通言語が生まれて知識を共有するようになったというのは、印象的でした。Ansible に限りませんが、技術選定でツールが統一されていくと色々捗るのだろうなとおもいました。

MAPFRE 社

動画は 15:00 頃から

CTO の睡眠時間を KPI に導入・・!

We introduced this KPI you can copy. The hours of sleep on the CTO.

Event-Driven Ansible と Red Hat Ansible Lightspeed

動画は 26:52 頃から

去年も目玉になっていた Event-Driven Ansible (EDA) と、Red Hat Ansible Lightspeed について。

Palasol での事例とデモ。

  • EDA と AI による自動復旧
    • 例えばデプロイされたノードに問題がある場合、EDA 経由で予測AIが異常を検知し、パフォーマンスプロファイルを復元するように Controller の上部をトリガーされる仕組み
    • 情報収集した結果をもとに Service Now のチケットも作成される
    • 修正用 Playbook を実行
  • Windows Server のトラブルシューティングのデモ
    • データベースでエラーが発生しているが詳細は不明
    • EDA で Watsonに詳細な情報を聞くように仕込めるば、その情報を ServiceNow に反映できる
  • 恒久対処 Playbook の作成のデモ
    • Red Hat Developer Hub 内のリンクをクリックすると VS Code が開く。ADT(Ansible Development Tools)
    • Red Hat Ansible Lightspeed 導入済み
    • やりたいことを自然言語で入力すると、作業ステップが表示される。よければPlaybook生成ボタンで生成
    • ドキュメントもセットで生成される
    • git push してジョブを実行
  • AI を活用した自動化への懸念
    • AI に責任を持たせるために policy as code が有用
    • 例えば、特定の日や時間帯での処理を禁止するポリシーが設定できる(画面上では Playbook か Rulebook の policies キーワードとして表現)
    • policy as code では他にも以下のようなことができる
      • 使用するコレクションの制限
      • AWS インスタンスタイプの指定の制限
      • Playbook へのベストプラクティスの強制

障害対応は例外の連続です。その中でも、共通化、抽象化できるところを見いだせると仕組化しやすいのだと思います。 たとえば、仮に情報収集、判断、対処というステップに分けられる場合、情報収集は共通化しやすそうです。

今回でも情報収集は自動化され、エラーの詳細を AI に問い合わせるところまで示されていました。判断のところは、人の入る余地がまだ大きい気がします。今回は Python のバージョンが異なっていることを目視で確認する流れでした。最後の対処のところは、対処の方針までは人が決めて、それを実行するPlaybook 生成は AI、という役割分担のように見えました。徐々に 人の役割を剝がせきているような印象があります。

モノとしては 36:17から登場する画面が気になります。Ansible Lightspeed へのプロンプトを入力する欄があるのですが、これも ADT の機能なのでしょうか。ADT はコレクションの開発者向けのモノだと思っていたのですが、今回認識をあらためました。

プロンプト入力画面。動画 36:56 頃 のキャプチャ

Policy as Code も気になります。Policy as Code は Red Hat Summit 側のキーノート Optimizing IT for the AI era の 39:10 頃からも話がありました。

参考:

おわりに

キーノートでネットワーク自動化の話があったのは嬉しかったです。

去年(2023)は、Event-Driven Ansible と Ansible Lightspeed with IBM Watson Code Assistant の話が多かったですが、今年はそこまで目立っていませんでした。ただ、eda-server のリポジトリを眺めていると、開発は着々と続いているようです。UI 統合?もありそうで、正常進化していくのかなと思います。また、Lightspeed という言葉は Ansible にとどまらず Red Hat Ligthspeed という言葉も出てきました(あまり追えていません)。

とりあえず AAP 2.5 がどのようになるのか気になっています。現状の AAP 2.4 のサポート状況をを見ると、2024年10月くらいにリリースされるのかなと予想しています。

最後に、引き続きウォッチしていきたいキーワード的なことをまとめておきます。

参考情報

[Ansible] meta/argument_specs.yml による Role argument validation の注意点

はじめに

Ansible にはロールの実行に必要な変数のバリデーションができる「Role argument validation」という機能があります。ansible-core 2.11 から利用できます。

どのような変数が必須か、どういう値を取りうるか(選択肢)を、各ロールの meta/argument_specs.yml に定義しておくことで、ロールの呼び出す際、実際の処理をする前に仕様に応じたバリデーションができます。

tekunabe.hatenablog.jp

いくつか注意が必要かなと思う点があるので、この記事でまとめます。

環境は以下の通りです。今後のバージョンアップで変更されるかもしれません。

  • 前提環境
    • ansible-core 2.16.6
    • ansible.builtin.import_role モジュールによるロール呼び出し

注意が必要なポイント

型の判定がゆるめ

typestrintboollistdict、などの型を指定できます。デフォルトは str です。

---
argument_specs:
  main:
    options:
      myvar:
        type: int       # 例

この型の指定で、バシッと型のバリデーションまでできそうですが、実際は割とゆるめです。

傾向としてはキャストができればバリデーションもOKとみなされる雰囲気です。例えば type: int に対しては 100 でも "100" でも OK です。さすがに hoge は NG です。

ほか、定義済みだけど値がない場合はバリデーションが OK になる傾向です。required: true とセットの場合は NG になります。

以下検証結果です。

1. type: str の場合(デフォルト)

定義

---
argument_specs:
  main:
    options:
      mystr:
        type: str

結果

変数の値 バリデーション結果 エラーメッセージ抜粋
100 OK
"100" OK
100.0 OK
hoge OK
true OK
0 OK
値なし OK
[100] OK
{num: 100} OK

「値なし」が OK となっていますが、required: true とセットにした場合は、'None' is not a string and conversion is not allowed というエラーで NG となりました。

2. type: int の場合

定義

---
argument_specs:
  main:
    options:
      myint:
        type: int

結果

変数の値 バリデーション結果 エラーメッセージ抜粋
100 OK
"100" OK
100.0 NG <class 'float'> cannot be converted to an int
hoge NG <class 'ansible.parsing.yaml.objects.AnsibleUnicode'> cannot be converted to an int
true OK
0 OK
値なし OK
[100] NG <class 'list'> cannot be converted to an int
{num: 100} NG <class 'dict'> cannot be converted to an int

「値なし」が OK となっていますが、required: true とセットにした場合は、<class 'NoneType'> cannot be converted to an int というエラーで NG となりました。

3. type: bool の場合

定義

---
argument_specs:
  main:
    options:
      mybool:
        type: bool

結果

変数の値 バリデーション結果 エラーメッセージ抜粋
100 NG 'mybool' is of type <class 'int'> and we were unable to convert to bool: The value '100' is not a valid boolean. Valid booleans include: 0, 1, '0', 'yes', 'on', 'n', 'no', 'f', 't', 'off', 'false', 'y', 'true', '1'
"100" NG 'mybool' is of type <class 'ansible.parsing.yaml.objects.AnsibleUnicode'> and we were unable to convert to bool: The value '100' is not a valid boolean. Valid booleans include: 0, 1, '0', 'yes', 'on', 'n', 'no', 'f', 't', 'off', 'false', 'y', 'true', '1'
100.0 NG 'mybool' is of type <class 'float'> and we were unable to convert to bool: The value '100.0' is not a valid boolean. Valid booleans include: 0, 1, '0', 'yes', 'on', 'n', 'no', 'f', 't', 'off', 'false', 'y', 'true', '1'
hoge NG 'mybool' is of type <class 'ansible.parsing.yaml.objects.AnsibleUnicode'> and we were unable to convert to bool: The value 'hoge' is not a valid boolean. Valid booleans include: 0, 1, '0', 'yes', 'on', 'n', 'no', 'f', 't', 'off', 'false', 'y', 'true', '1'
true OK
0 OK
値なし OK
[100] NG <class 'list'> cannot be converted to a bool
{num: 100} NG <class 'dict'> cannot be converted to a bool

「値なし」が OK となっていますが、required: true とセットにした場合は、<class 'NoneType'> cannot be converted to a bool というエラーで NG となりました。

4. type: listelements: int の場合

リストの変数の場合で、ここでは int の値を持つ仕様を想定します。

定義

---
argument_specs:
  main:
    options:
      mylist:
        type: list
        elements: int

結果

変数の値 バリデーション結果 エラーメッセージ抜粋
100 OK
"100" OK
100.0 NG <class 'str'> cannot be converted to an int
hoge NG <class 'str'> cannot be converted to an int
true NG <class 'str'> cannot be converted to an int
0 OK
値なし OK
[100] OK
{num: 100} NG <class 'dict'> cannot be converted to a list

「値なし」が OK となっていますが、required: true とセットにした場合は、<class 'NoneType'> cannot be converted to a list というエラーで NG となりました。

5. type: dcit の場合

ディクショナリの変数の場合で、ここではさらに中に num というキーで int の値を持つ仕様を想定します。

定義

---
argument_specs:
  main:
    options:
      mydict:
        type: dict
        options:
          num:
            type: int

結果

変数の値 バリデーション結果 エラーメッセージ抜粋
100 NG argument of type 'int' is not iterable
"100" NG dictionary requested, could not parse JSON or key=value
100.0 NG argument of type 'float' is not iterable
hoge NG dictionary requested, could not parse JSON or key=value
true NG argument of type 'bool' is not iterable
0 NG argument of type 'int' is not iterable
値なし OK
[100] NG argument of type 'int' is not iterable
{num: 100} OK

「値なし」が OK となっていますが、required: true とセットにした場合は、<class 'NoneType'> cannot be converted to a dict というエラーで NG となりました。

複雑なチェックはできない

たとえば正規表現によるパターンマッチングや、数値の範囲、他のタスクの実行結果を利用したバリデーションなど、複雑なバリデーションはできません。

モジュールの argument_specs ほどの機能はない

モジュールのコードを読んだり書いたりしたこがある方であれば、argument_spec という言葉を見かけたことがあるかもしれません。(ロールではなく)モジュールのパラメーターの仕様を定義するディクショナリで、内部的にバリデーション的な処理がされます。

argument_spec という名前や、含まれるキーの名前が「Role argument validation」のものと似ていますが、一部異なります。比較すると「Role argument validation」によるバリデーションのほうができることが少ないです。

たとえば「Role argument validation」では、以下のような排他や、条件付きの必須の指定はできないようです。

  • mutually_exclusive
  • required_if
  • required_together
  • required_one_of
  • required_by

VS Code の Ansible の拡張を使って meta/argument_specs.yml を書いていると補完されてしまいますが。

あくまで、対応しているのは meta/argument_specs.yml 内の argument_specs のフォーマットにあるもの(requiredchoices)のみと捉えておくのが分かりやすいと思います。

関連 Issue:

default の指定は動作には関係ない

ドキュメントにも記載がありますが、default で指定した値は机上の値であり、実際に適用されるデフォルト値は defaults/main.yml での指定によります。

バリデーションタスクには always タグが暗黙的につく

バリデーションは1つのタスクのような扱われ方をします。

ドキュメントに記載ありますが、バリデーションのタスクには always タグ 扱いです。

たとえば、meta/argument_specs.yml を定義しているロールを呼びだす以下のPlaybookがあった場合。

---
- name: Test Play int
  hosts: localhost
  connection: local
  gather_facts: false

  tasks:
    - name: Import role1
      ansible.builtin.import_role:    # static な import
        name: role1
      vars:
        myint: 100

ansible-playbook コマンドを --list-tasks オプション付きで実行すると、バリデーション用のタスクに always タグが付いていることが分かります。

$ ansible-playbook -i localhost, playbook.yml --list-tasks

playbook: playbook.yml

  play #1 (localhost): Test Play int    TAGS: []
    tasks:
      role1 : Validating arguments against arg spec 'main'      TAGS: [always]

--list-tags オプション付きの実行でも always タグの存在を確認できます。

$ ansible-playbook -i localhost, playbook.yml --list-tags

playbook: playbook.yml

  play #1 (localhost): Test Play int    TAGS: []
      TASK TAGS: [always]

そのため、タグを指定した Playbook の実行時には少し注意が必要です。

たとえば、以下の Playbook を -t role2 で実行すると、role1 のバリデーション実行、role2 のバリデーション実行、role2 の処理実行、となります。role1 のバリデーション実行がされるところがポイントです。

---
- name: Test Play
  hosts: localhost
  connection: local
  gather_facts: false

  tasks:
    - name: Import role1
      ansible.builtin.import_role:
        name: role1
      tags:
        - role1

    - name: Import role2
      ansible.builtin.import_role:
        name: role2
      tags:
        - role2
$ ansible-playbook -i localhost, playbook2.yml -t role2

PLAY [Test Play] ******************************************************************************************************

TASK [role1 : Validating arguments against arg spec 'main'] ***********************************************************
ok: [localhost]

TASK [role2 : Validating arguments against arg spec 'main'] ***********************************************************
ok: [localhost]

TASK [role2 : Debug] **************************************************************************************************
ok: [localhost] => {
    "msg": "In role2"
}

--skip-tags=always を追加すれば、(role1role2も)バリデーションが行われなくなります。ただ、この辺りは凝りだすと複雑になり、思わぬ副作用を誘発しかねないので、ほどほどにした方が良いかなといます。

バリデーションを無効にしたい場合は、ansible.builtin.import_role モジュールであれば rolespec_validatefalse に指定するというのも手です。

Role argument validation 以外の選択肢

複雑なチェックには、ansible.utils.validate モジュールJSON Schema の組み合わせや、ansible.builtin.assert モジュール によるチェックが向いていると思います。

おわりに

meta/argument_specs.yml による Role argument validation の注意点をまとめました。

特性を理解したうえで、複雑でないバリデーションであれば有用かなとおもいました。

[Terraform/AAP] Terraform で Automation Controller を設定する ansible/aap プロバイダーを試す

はじめに

Terraform から AAP (厳密には Automation Controller)の設定を操作する ansible/aap プロバイダー 1.0.0 が 2024/05/01 にリリースされました。

registry.terraform.io

これまでも、Terraform から Playbook を呼び出す ansible/ansible プロバイダーや、逆に Ansible から Terraform を呼び出す cloud.terraform コレクションがありました。

今回リリースされた Terraform の ansible/aap プロバイダーは既存のものとは別用途で、 Automation Controller の設定を Terraform から行うものです。Ansiblle でいうと ansible.controller / awx.awx コレクションにあたるものかなと思います。

簡単ですが、ためしてみます。

  • 環境
    • ansible/aap プロバイダー 1.0.0
    • Automation Controller 4.1.0
    • Terraform v1.8.2

ansible/aap プロバイダーの機能

バージョン 1.0.0 での機能は以下のとおりです。現在のところは限定的です。

おためし

構成

インベントリーを1つ作り、その中にグループ、その中に1つのホストを作ります。

インベントリー network_devices
  グループ ios
    ホスト ios01

tf ファイル

一通り tf ファイルを作成します。

terraform.tf

terraform {
  required_providers {
    aap = {
      source = "ansible/aap"
      version = "1.0.0"
    }
  }
}

providers.tf

provider "aap" {
  host                             = "https://192.168.1.99"  # Automation Controller の アドレス
  insecure_skip_verify = true
}

ほか、本プロバイダーの provider ブロックでは、usernamepassword指定できます

ベタ書きを避けるため、ここではそれぞれ環境変数 AAP_USERNAMEAAP_PASSWORD を利用します。この方法は 1.0.0 現在、ドキュメント上には README.md の Acceptance tests でのみ記載があります。

もっと一般的な方法としては、別途 variable を定義して export TF_VAR_変数名="値" で設定しておき username = 変数名 のようにしておくのも良いかなと思います。

main.tf

各種リソースの定義をします。

resource "aap_inventory" "network_devices" {
  name        = "network_devices"
  description = "Managed by Terraform"
}

resource "aap_group" "ios" {
  name         = "ios"
  description  = "Managed by Terraform"
  inventory_id = aap_inventory.network_devices.id
  variables = jsonencode(
    {
      "ansible_network_os" : "cisco.ios.ios"
    }
  )
}

resource "aap_host" "ios01" {
  name         = "ios01"
  description  = "Managed by Terraform"
  inventory_id = aap_inventory.network_devices.id
  groups       = [aap_group.ios.id]
  variables = jsonencode(
    {
      "ansible_host" : "192.168.1.11"
    }
  )
}

data "aap_inventory" "network_devices" {
  id = aap_inventory.network_devices.id
}

aap_inventory リソースで、organization を指定していませんが、その場合はデフォルトの組織になるそうです。

最後の aap_inventory データソースはあまり意味はないですが、どんな情報が得られるか確認するために定義します。id は インベントリー ID を指定します。今回は Terraform 管理化のインベントリーの情報を参照しているため、〜.id という形で指定しています。Terraform 管理外のものの場合はあらかじめ ID を調べておく必要があります。

outputs.tf

おまけ的に定義した aap_inventory データーソースで得られる情報の中身を確認するため、output ブロックも定義します。

output "inventory_details" {
  value = data.aap_inventory.network_devices
}

認証情報の設定

provider ブロックで直接 usernamepassword を指定しなかった代わりに、環境変数を指定しています。

 export AAP_USERNAME="admin"
 export AAP_PASSWORD="dummy_password_xxx"

init

% terraform init

Initializing the backend...

Initializing provider plugins...
- Finding ansible/aap versions matching "1.0.0"...
- Installing ansible/aap v1.0.0...
- Installed ansible/aap v1.0.0 (self-signed, key ID 41F01D0480007165)

...()...

Terraform has been successfully initialized!
...()...

plan

% terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create
 <= read (data resources)

Terraform will perform the following actions:

  # data.aap_inventory.network_devices will be read during apply
  # (config refers to values not yet known)
 <= data "aap_inventory" "network_devices" {
      + description  = (known after apply)
      + id           = (known after apply)
      + name         = (known after apply)
      + organization = (known after apply)
      + url          = (known after apply)
      + variables    = (known after apply)
    }

  # aap_group.ios will be created
  + resource "aap_group" "ios" {
      + description  = "Managed by Terraform"
      + id           = (known after apply)
      + inventory_id = (known after apply)
      + name         = "ios"
      + url          = (known after apply)
      + variables    = jsonencode(
            {
              + ansible_network_os = "cisco.ios.ios"
            }
        )
    }

  # aap_host.ios01 will be created
  + resource "aap_host" "ios01" {
      + description  = "Managed by Terraform"
      + enabled      = true
      + groups       = [
          + (known after apply),
        ]
      + id           = (known after apply)
      + inventory_id = (known after apply)
      + name         = "ios01"
      + url          = (known after apply)
      + variables    = jsonencode(
            {
              + ansible_host = "192.168.1.11"
            }
        )
    }

  # aap_inventory.network_devices will be created
  + resource "aap_inventory" "network_devices" {
      + description  = "Managed by Terraform"
      + id           = (known after apply)
      + name         = "network_devices"
      + organization = (known after apply)
      + url          = (known after apply)
    }

Plan: 3 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + inventory_details = {
      + description  = (known after apply)
      + id           = (known after apply)
      + name         = (known after apply)
      + organization = (known after apply)
      + url          = (known after apply)
      + variables    = (known after apply)
    }

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.

apply

% terraform apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create
 <= read (data resources)

Terraform will perform the following actions:

  # data.aap_inventory.network_devices will be read during apply
  # (config refers to values not yet known)
 <= data "aap_inventory" "network_devices" {
      + description  = (known after apply)
      + id           = (known after apply)
      + name         = (known after apply)
      + organization = (known after apply)
      + url          = (known after apply)
      + variables    = (known after apply)
    }

  # aap_group.ios will be created
  + resource "aap_group" "ios" {
      + description  = "Managed by Terraform"
      + id           = (known after apply)
      + inventory_id = (known after apply)
      + name         = "ios"
      + url          = (known after apply)
      + variables    = jsonencode(
            {
              + ansible_network_os = "cisco.ios.ios"
            }
        )
    }

  # aap_host.ios01 will be created
  + resource "aap_host" "ios01" {
      + description  = "Managed by Terraform"
      + enabled      = true
      + groups       = [
          + (known after apply),
        ]
      + id           = (known after apply)
      + inventory_id = (known after apply)
      + name         = "ios01"
      + url          = (known after apply)
      + variables    = jsonencode(
            {
              + ansible_host = "192.168.1.11"
            }
        )
    }

  # aap_inventory.network_devices will be created
  + resource "aap_inventory" "network_devices" {
      + description  = "Managed by Terraform"
      + id           = (known after apply)
      + name         = "network_devices"
      + organization = (known after apply)
      + url          = (known after apply)
    }

Plan: 3 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + inventory_details = {
      + description  = (known after apply)
      + id           = (known after apply)
      + name         = (known after apply)
      + organization = (known after apply)
      + url          = (known after apply)
      + variables    = (known after apply)
    }

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aap_inventory.network_devices: Creating...
aap_inventory.network_devices: Creation complete after 0s [name=network_devices]
data.aap_inventory.network_devices: Reading...
aap_group.ios: Creating...
data.aap_inventory.network_devices: Read complete after 0s [name=network_devices]
aap_group.ios: Creation complete after 0s [name=ios]
aap_host.ios01: Creating...
aap_host.ios01: Creation complete after 1s [name=ios01]

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Outputs:

inventory_details = {
  "description" = "Managed by Terraform"
  "id" = 4
  "name" = "network_devices"
  "organization" = 1
  "url" = "/api/v2/inventories/4/"
  "variables" = tostring(null)
}

処理はすぐに終わりました。

Outputs: には aap_inventory データソースで取得したインベントリの値が表示されました。

確認

無事に、インベントリー、グループ、ホストが作成されていました。

インベントリー

グループ

ホスト

destroy

% terraform destroy
aap_inventory.network_devices: Refreshing state... [name=network_devices]
data.aap_inventory.network_devices: Reading...
aap_group.ios: Refreshing state... [name=ios]
data.aap_inventory.network_devices: Read complete after 0s [name=network_devices]
aap_host.ios01: Refreshing state... [name=ios01]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # aap_group.ios will be destroyed
  - resource "aap_group" "ios" {
      - description  = "Managed by Terraform" -> null
      - id           = 3 -> null
      - inventory_id = 4 -> null
      - name         = "ios" -> null
      - url          = "/api/v2/groups/3/" -> null
      - variables    = jsonencode(
            {
              - ansible_network_os = "cisco.ios.ios"
            }
        ) -> null
    }

  # aap_host.ios01 will be destroyed
  - resource "aap_host" "ios01" {
      - description  = "Managed by Terraform" -> null
      - enabled      = true -> null
      - groups       = [
          - 3,
        ] -> null
      - id           = 4 -> null
      - inventory_id = 4 -> null
      - name         = "ios01" -> null
      - url          = "/api/v2/hosts/4/" -> null
      - variables    = jsonencode(
            {
              - ansible_host = "192.168.1.11"
            }
        ) -> null
    }

  # aap_inventory.network_devices will be destroyed
  - resource "aap_inventory" "network_devices" {
      - description  = "Managed by Terraform" -> null
      - id           = 4 -> null
      - name         = "network_devices" -> null
      - organization = 1 -> null
      - url          = "/api/v2/inventories/4/" -> null
    }

Plan: 0 to add, 0 to change, 3 to destroy.

Changes to Outputs:
  - inventory_details = {
      - description  = "Managed by Terraform"
      - id           = 4
      - name         = "network_devices"
      - organization = 1
      - url          = "/api/v2/inventories/4/"
      - variables    = null
    } -> null

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

aap_host.ios01: Destroying... [name=ios01]
aap_host.ios01: Destruction complete after 1s
aap_group.ios: Destroying... [name=ios]
aap_group.ios: Destruction complete after 0s
aap_inventory.network_devices: Destroying... [name=network_devices]
aap_inventory.network_devices: Destruction complete after 0s

Destroy complete! Resources: 3 destroyed.

画面を確認すると、無事にインベントリー、グループ、ホストが削除されていました。もちろん、今回作成する前に存在していたその他のリソースは残ったままです。

おわりに

簡単ですが、Terraform の ansible/aap プロバイダーを使って、インベントリー、グループ、ホストの作成と削除をためしてみました。

バージョン 1.0.0 がリリースされたばかりということで、クレデンシャルやジョブテンプレートの管理には対応していなくて、まだまだ機能は限定的です。ですが、Automation Controller の設定と Terraform の使用感は相性がいいようにも思いました。たとえば、インベントリーとグループ、ホストの関係を、暗黙的な依存で表現でき、かつ tf ファイルの記述順自体は意識しなくてよい点です。

今後も ansible/aap プロバイダーには注目したいと思います。

今のところ、Automation Controller / AWX を設定の自動化は、機能が充実している Ansible の ansible.controller / awx.awx コレクションを使うのが良いと思います。

[Ansible] ハンドラー名はユニークにしておいた方が良い理由と検証

はじめに

Ansible には、タスクのステータスが changed のときだけ実行したい処理を定義するために、ハンドラーという機能があります。

docs.ansible.com

ハンドラーには、通常のタスクと同様に name を指定できます。ハンドラーで指定した name をこの記事ではハンドラー名と呼びます。

タスクとハンドラーを関連付ける方法は複数あります。その一つは、通常のタスクの notify ディレクティブで、呼びたいハンドラー名を指定する方法です。 (実際には一通りタスクの処理が終わってからハンドラーが呼ばれます。ロールを含めた処理順はこちら

そのため、ハンドラー名は重要な識別子の役割を持ちます。もし、同じハンドラー名のハンドラーが複数あると思わぬ結果を招く場合あります。ハンドラー名はユニークにしておくべきと、ドキュメントに記載があります。

Each handler should have a globally unique name.

同じくドキュメントに記載されている、グロバルスコープで扱われるという点も重要です。

Handlers from roles are not just contained in their roles but rather inserted into the global scope with all other handlers from a play.

この記事では、同じ名前のハンドラーがある場合の挙動を、通常の場合と比較して検証します。

  • 検証環境
    • ansible-core 2.15.2

1. 通常の場合(ユニークなハンドラー名の場合)

まずは、比較のためにユニークなハンドラー名で構成された通常の場合の挙動です。

フォルダ・ファイル構成

それぞれハンドラーを持つ 2つのロール role1role2 からなる構成とします。

.
├── playbook.yml
└── roles
    ├── role1
    │   ├── handlers
    │   │   └── main.yml
    │   └── tasks
    │       └── main.yml
    └── role2
        ├── handlers
        │   └── main.yml
        └── tasks
            └── main.yml

role1 ロール

role1/tasks/main.yml:

---
- name: Task with changed
  ansible.builtin.command:    # changed
    cmd: "echo Hello!"
  notify:
    - Test Handler in role1

role1/handlers/main.yml:

---
- name: Test Handler in role1   # ロール固有の名前
  ansible.builtin.debug:
    msg: "handler {{ ansible_role_name }}"

role2 ロール

role1 とすべて同じです)

role2/tasks/main.yml:

---
- name: Task with changed
  ansible.builtin.command:    # changed
    cmd: "echo Hello!"
  notify:
    - Test Handler in role2

role2/handlers/main.yml:

---
- name: Test Handler in role2   # ロール固有の名前
  ansible.builtin.debug:
    msg: "handler {{ ansible_role_name }}"

Playbook

2つのロールを呼び出すPlaybookです。

playbook.yml:

---
- name: Test Play
  hosts: localhost
  connection: local
  gather_facts: false

  tasks:
    - name: Import role1
      ansible.builtin.import_role:
        name: role1

    - name: Import role2
      ansible.builtin.import_role:
        name: role2

実行結果

以下のような実行順序になります。

  1. role1 のタスク
  2. role2 のタスク
  3. role1 のハンドラー
  4. role2 のハンドラー

コマンド実行ログは以下のとおりです。

$ ansible-playbook -i localhost, playbook.yml

PLAY [Test Play] ****************************************************************************************************

TASK [role1 : Task with changed] ************************************************************************************
changed: [localhost]

TASK [role2 : Test Handler in role1] ********************************************************************************
changed: [localhost]

RUNNING HANDLER [role1 : Test Handler in role1] *********************************************************************
ok: [localhost] => {
    "msg": "handler role1"
}

RUNNING HANDLER [role2 : Test Handler in role2] *********************************************************************
ok: [localhost] => {
    "msg": "handler role2"
}

PLAY RECAP **********************************************************************************************************
localhost                  : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

2. 同じ名前のハンドラーがある場合

次に、同じ名前のハンドラーが複数に含まれている場合です。

role1 ロール

role1/tasks/main.yml:

---
- name: Task with changed
  ansible.builtin.command:    # changed
    cmd: "echo Hello!"
  notify:
    - Test Handler

role1/handlers/main.yml:

---
- name: Test Handler    # 他のロールにあるハンドラーと同じ名前
  ansible.builtin.debug:
    msg: "handler {{ ansible_role_name }}"

role2 ロール

role1 とすべて同じです)

role2/tasks/main.yml:

---
- name: Task with changed
  ansible.builtin.command:    # changed
    cmd: "echo Hello!"
  notify:
    - Test Handler

role2/handlers/main.yml:

---
- name: Test Handler    # 他のロールにあるハンドラーと同じ名前
  ansible.builtin.debug:
    msg: "handler {{ ansible_role_name }}"

Playbook

2つのロールを呼び出すPlaybookです。

playbook.yml:

---
- name: Test Play
  hosts: localhost
  connection: local
  gather_facts: false

  tasks:
    - name: Import role1
      ansible.builtin.import_role:
        name: role1

    - name: Import role2
      ansible.builtin.import_role:
        name: role2

実行結果

以下のような実行順序になります。

  1. role1 のタスク
  2. role2 のタスク
  3. role2 のハンドラー

role1 のハンドラーが呼ばれていないところがポイントです。

コマンド実行ログは以下のとおりです。

$ ansible-playbook -i localhost, playbook.yml

PLAY [Test Play] ****************************************************************************************************

TASK [role1 : Task with changed] ************************************************************************************
changed: [localhost]

TASK [role2 : Task with changed] ************************************************************************************
changed: [localhost]

RUNNING HANDLER [role2 : Test Handler] ******************************************************************************
ok: [localhost] => {
    "msg": "handler role2"
}

PLAY RECAP **********************************************************************************************************
localhost                  : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0  

この、role1 のハンドラーが呼ばれなかったところが、ドキュメントの言葉でいう

shadowing all of the previous handlers with the same name.

かなと思います。

なお、ansible.builtin.import_role でインポートするロールを逆( role2role1 の順)にしたら、role1 のハンドラーのみが呼ばれました。

おまけ

ハンドラーはグローバルで有効という性質上、以下のようなこともできます。

特定のロールのハンドラーを指定

notify で ロール名 : ハンドラー名 を指定すると、ロールを明示的に指定できます。ドキュメントにも方法が載っています(最近知りました)。

たとえば、role1/tasks/main.yml で、以下のように role2 内の ハンドラーを Test Handler 呼ぶとします。

---
- name: Task with changed
  ansible.builtin.command:
    cmd: "echo Hello!"
  notify:
    - "role2 : Test Handler"

これで、role2/handlers/main.yml の以下のハンドラーが呼ばれます。

---
- name: Test Handler
  ansible.builtin.debug:
    msg: "handler {{ ansible_role_name }}"

ただし、role2 も Playbook 側で読み込んでおく必要があります。

ハンドラーのみのロールでハンドラーを共通化

ハンドラーのみのロールを用意して、他のロールからハンドラーを呼ぶようにして、ハンドラーを共通化するような方法です。

.
├── playbook.yml
└── roles
    ├── role1
    │   └── tasks
    │       └── main.yml
    └── role_handler_only
        └── handlers
            └── main.yml

role_handler_only/handlers/main.yml:

---
- name: Test Handler      # role_handler_only ロールはこれのみ
  ansible.builtin.debug:
    msg: "handler {{ ansible_role_name }}"

role1/tasks/main.yml:

---
- name: Task with changed # role1 ロールはこれのみ
  ansible.builtin.command:
    cmd: "echo Hello!"
  notify:
    - Test Handler

playbook.yml:

---
- name: Test Play
  hosts: localhost
  connection: local
  gather_facts: false

  tasks:
    - name: Import role1
      ansible.builtin.import_role:
        name: role1

    - name: Import role_handler_only
      ansible.builtin.import_role:
        name: role_handler_only  # ハンドラーのみのロール

実行ログ:

ansible-playbook -i localhost, playbook.yml

PLAY [Test Play] *************************************************************************************************************

TASK [role1 : Task with changed] *********************************************************************************************
changed: [localhost]

RUNNING HANDLER [role_handler_only : Test Handler] ***************************************************************************
ok: [localhost] => {
    "msg": "handler role_handler_only"
}

PLAY RECAP *******************************************************************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

ハンドラーのみのロールの実装のヒントは、以下の記事から得ました。ありがとうございます。

qiita.com

おわりに

同じ名前のハンドラーがあることで、起こる現象を検証しました。

ざっくり、ハンドラー名は Play 内でグローバルなスコープで処理される、というイメージでとらえておけばよいかなと思いました。