見出し画像

MoleculeでAnsibleのRoleをテストする - その3


こんにちは。株式会社SHIFT、自動化エンジニアの水谷です。前回前々回ではAnsibleのRoleをテストするツール"Molecule"について書きました。

それらの記事では、Linux系マシンを操作するRoleのテストを作成してDocker内でRoleを実行し、テストを自動実行するところまで、Windowsマシンを操作するRoleのテストについてのテスト方法は書いていませんでした。

というのも、MoleculeはデフォルトではAnsibleが動作しているLinux系OS上で動作するDockerでテスト環境を用意して、そこでテストを実行するため、(カーネルが異なる)Windowsのテスト環境が作成できないのです。

このため、(Ansibleが動作しているLinux系OS上の)Dockerではなく、Delegete Driverの仕組みを使ってAWSなどのクラウド上にインスタンスを立ててそこで実行するか、VMWareなどで行うなどの方法を使ってテストを行う必要があります。しかし、これではAWSの料金が発生したり、OSのライセンスが必要だったりする上、Moleculeの設定もいろいろ難しそうです。

Windows用のRoleのMoleculeでのテストをちょっと諦めかけていたのですが、ふとしたきっかけでWindows用のDocker(Docker Desktop for Windows)が使そうだと気付き、試してみたところなんとかテストができるところまでたどり着きましたので、今回は予定していなかったMoleculeの記事の"その3"としてDocker for Windowsを使ったWindows向けRoleのテスト方法紹介したいと思います。

全体像

今回の構成は、Windows 10 Professionalの入ったノートPCに、WSL2でUbuntu 20.04を動かして、このUbuntu上でAnsibleやMoleculeをインストールします。そして、Windows上にDocker Desktop for Windowsをインストールし、ここで"Windows Server Core"というWindows Serverのイメージからコンテナを起動し、これに対してRoleを実行。そして、テストを実行して、最後はコンテナを破棄する、という流れになります。

画像6

※Ubuntuは20.04以外のバージョンでも問題ありません。

WSLからWindowsのアプリを起動

この一連の動作を実現しようとする場合、そもそもWSL上で動作しているLinux系OS(今回はUbuntu 20.04を使用)から、WindowsにインストールされたDocker(docker.exe)を起動できるのか、という疑問が出ますよね? 実はUbuntuのBash上からWindowsアプリの実行ファイル名を叩けば、あっさりと起動します。例えば"calc.exe"と打てば電卓が起動するし、"notepad.exe"と打てばメモ帳が開くのです。

つまり、Docker Desktop for Windowsがインストールされていれば、"docker.exe"とタイプすればUbuntuからdocker.exeが実行できてしまうのです! 便利ですね。皆さんご存じでしたか? 私は知りませんでした。

ちなみに、"docker"とだけ打つと、Ubuntu上にインストールされている方のDockerが動きます。"docker -v"と"docker.exe -v"をそれぞれ実行してみると、下のように出力が違っていますね。

画像1

Docker Desktop for Windowsのインストールとイメージの準備

Dockerはすでに使われている方も少なくないかもしれませんが、まだの方はこちらからインストールできます。
https://hub.docker.com/editions/community/docker-ce-desktop-windows/

Docker for Windowsは、WSL2を使ってLinuxOS系のイメージを実行できたりもするのですが、今回はWindowsのイメージを動作させたいので、タスクトレイからDockerのアイコンを右クリックして"Switch to Windows containers..."を選択して、Windows系のコンテナが動作するように変更しておきます。

画像2
画像4

さて、テスト環境となるイメージの作成ですが、そのベースとなるWindows Serverのイメージは、"mcr.microsoft.com/windows/servercore"です。

https://hub.docker.com/_/microsoft-windows-servercore

自分が使用しているPCに入っているWindows 10 Professionalのバージョンが"2004"のため、イメージも同じ"2004"タグが付いたものをPullしました。

> docker.exe pull mcr.microsoft.com/windows/servercore:2004

なお、4GBほどの大きさがありますので、Pullするのに結構時間がかかります。

次にイメージのカスタマイズですが、次の3つの変更を行います。

・WinRMの設定を行う
・WinRMのポート(5986)を開ける
・ユーザーを作成する

1つ目のWinRMの設定は、こちらのスクリプト https://github.com/ansible/ansible/blob/devel/examples/scripts/ConfigureRemotingForAnsible.ps1 を使用させていただきました。スクリプトをc:\tempにコピーして実行しています。

ということで、Dockerfileは以下のようになります。

FROM mcr.microsoft.com/windows/servecore:2004

# Configure WinRM using ConfigureRemotingForAnsible.ps1
RUN md c:\temp
COPY ConfigureRemotingForAnsible.ps1 c:\\temp\\
RUN powershell -ep bypass C:\\temp\\ConfigureRemotingForAnsible.ps1

# Expose port for WinRM
EXPOSE 5986

# Create an administrator user
RUN net user testuser Passw0rd /ADD
RUN net localgroup "Administrators" testuser /ADD

下のようにビルドして、"winservecore_molecule"という名前のイメージを作成しました。

> docker build -t winservecore_molecule .

コンテナへのアクセス確認

さて、次は1つ目の山場となる、WSLで動いているUbuntu上のAnsibleから、このイメージから作ったコンテナへのWinRM接続です。私はLinux系OSの経験も少なく(1年程度)、Dockerに関してはずぶの素人ということもあり、正直に言って結構苦労しました。というのも、あまりこのようなことをする人がいないのか、ネットからは断片的な情報しか見つからず、試行錯誤することに……。結果的には下のようなコマンドや設定で接続できました。

コンテナの起動
コンテナは、Windows上で以下のコマンドを実行して、WinRMのポート5986を55986に接続しました(後述のように、ここが重要なポイントでした)。

> docker run --interactive -p 55986:5986 winservecore_molecule

なお、とりあえずコンテナ内の様子を見たいので、インタラクティブモードにしています。

インベントリファイル
Ansibleからこのコンテナ接続するためのインベントリファイルを下のように作成しました。

[windocker]
192.168.1.68

[windocker:vars]
ansible_user=testuser
ansible_password=Passw0rd
ansible_connection=winrm
ansible_ssh_port=55986
ansible_winrm_transport=basic
ansible_winrm_server_cert_validation=ignore

2行目のIPアドレスは、私の環境におけるWindows PCのIPアドレスです。ずっとここはコンテナが持つIPアドレスを指定するものだと思っていて、思いっきりハマりました……。というのも、私の設定が正しくないからかもしれませんが、Ubuntuからはコンテナ内のWindows Serverに直接アクセスできませんでした(pingも不通)。また、ポートを55986にすることで、(WinRMの通信はコンテナ内の5986にフォワードされて)通信可能となります。そして、ユーザー名やパスワードはDockerfileで指定したものに合わせます。

win_pingの実行
では、win_pingモジュールで接続確認をしてみましょう。

$ ansible windocker -i hosts -m win_ping
画像5

このように"pong"が返ってきましたので、疎通確認完了です。

ここまでに半日かかりました(汗

Moleculeの設定

続いてMolecule側の設定です。(Linux上の)Dockerを使ってLinux向けRoleをテストする場合は、ドライバーに"docker"と指定しておけば、ほぼすべて自動的に設定してくれましたが、今回の場合は、自分でコンテナの起動や破棄、WinRMへの接続情報などを記述する必要があります。

まずは、Roleを作成するのですが、その際に-dオプションで"delegated"を指定します。

$ molecule init role <role名> -d delegated

作成されたファイルを見ると、Dockerをドライバーとした場合に対して、/molecule/default/の下に、create.ymlとdestroy.ymlの2つが新たに作成されています。名前から想像できるように、create.ymlにはテスト環境を作成するPlaybookを記述し、destroy.ymlでそれを破棄するPlaybookを作成します。

create.ymlを作る
まずは、Docker for Windowsでうえで作成したコンテナを起動させます。具体的にはAnsibleから下のコマンドが実行出来ればよいことになります。

$ docker.exe run -itd --rm --name testserver -p 55986:5986 winservecore_molecule

先程の疎通確認の時と同様に、WinRMのポート5986を55986に接続しています。また、そのままでは起動後すぐにコンテナが停止してしまいますので、"-itd"オプションを指定しています。また、コンテナを破棄するときにやりやすいよう、"testserver"という名前を付けておきました。

Playbook全体は以下のようになります。

---
- name: Create
  hosts: localhost
  connection: local
  gather_facts: false
  no_log: "{{ molecule_no_log }}"
  tasks:
    - name: start docker windows
      command: docker.exe run -itd --rm --name testserver -p 55986:5986 winservecore_molecule

Windowsで実行するコマンドだからと、"win_command"モジュールを使ってはいけません。あくまでこれはUbuntu上で実行するコマンドなので、"command"モジュールを使います。

また、"molecule init role"で作成されたcreate.ymlは後半にインスタンスの接続情報などをファイルに書き出すタスクがテンプレートの形で用意されていたのですが、それはすべて削除しています。その代わり、接続情報はmolecule.ymlの方で記述することにします(詳しくは後述)。

destroy.ymlを作る
同様にコンテナの停止ですが、create.ymlで指定した名前を使い、以下のコマンドを実行することでコンテナを停止します。

$ docker.exe stop testserver

注意が必要なのは、"molecule test"で一連のテスト作業を実行する場合、最初に実行されるのがこのdestroy.ymlであることです。テスト環境が削除されずに残っている可能性を考え、最初にそれを削除するために実行されるものだと思いますが、つまりこれはコンテナが動いていない状況でも呼ばれてしまうことになります。そこで、"docker.exe ps"を実行して、"testserver"コンテナが動いている場合のみ"docker.exe stop"を実行するようにしました。

---
- name: Destroy
  hosts: localhost
  connection: local
  gather_facts: false
  no_log: "{{ molecule_no_log }}"
  tasks:
    - name: Run docker ps
      command: docker.exe ps
      register: result
      
    - name: Stop docker
      command: docker.exe stop testserver
      when: "'testserver' in result.stdout"
      
    # Mandatory configuration for Molecule to function.
    
    - name: Populate instance config
      set_fact:
        instance_conf: {}
        
    - name: Dump instance config
      copy:
        content: |
          # Molecule managed
          {{ instance_conf | to_json | from_json | to_yaml }}
        dest: "{{ molecule_instance_config }}"
      when: server.changed | default(false) | bool

なお、後半部分は自動生成されたコードをそのまま残しています。

molecule.ymlを変更する
最後に接続情報をmolecule.ymlに追記します(provisioner:内)。

---
dependency:
  name: galaxy
driver:
  name: delegated
  options:
    ansible_connection_options:
      connection: winrm
platforms:
  - name: instance
provisioner:
  name: ansible
  connection_options:
    ansible_connection: winrm
    ansible_user: testuser
    ansible_password: Passw0rd
    ansible_port: 55986
    ansible_host: 192.168.1.68
    ansible_connection: winrm
    ansible_winrm_transport: basic
    ansible_winrm_server_cert_validation: ignore
verifier:
  name: ansible

実はここもすごく苦労しました。
というのも、MoleculeのDelegeted Driverに関する情報はネットにもほとんどなく、ましてやWinRMを使う場合の例はぜんぜん見つからないため、唯一見つけられたVMWareでDelegeted Driverを作った例などを参考にして、なんとか作ることができました。"molecule init role"で自動生成されたcreate.yml内で接続情報をファイル化するコードを思い切って全部削除したことと、接続情報を"provisioner:"以下に書くことがポイント(最初はplatforms:以下に書くものだと思って失敗しました)です。ま、できてみれば、けっこうシンプルでストレートなものですが(汗

なお、"ansible_host"のところに自分のWindows PCのIPアドレスをハードコードしてしまっていますが、これは何とかしたいところです。"ipconfig.exe"やPowershellでGet-NetIPAddressコマンドを実行して、その結果からIPアドレスを抽出してセットするようにすれば何とかなるのかなと思いますが、まだ試していません。

実際のRoleでテスト

簡単な例として、イベントログを削除するRoleを作ってみました。

tasks/main.ymlは、下のようなシンプルなものです。

---
- name: Clear EventLog
  win_eventlog:
    name: "{{ item }}"
    state: clear
  loop: "{{ target_event }}"

"target_event"変数(配列)に入っている種類のイベントログを順に削除していきます。

この変数はconverge.ymlで以下のように設定され、Roleが呼び出されます。

---
- name: Converge
  hosts: all
  vars:
    target_event:
    - 'Application'
    - 'HardwareEvents'
    - 'Internet Explorer'
    - 'Key Management Service'
    - 'Security'
    - 'Windows PowerShell'
    - 'System'
 tasks:
    - name: "Include clear-eventlog"
      include_role:
        name: "clear-eventlog"

verify.ymlに記述したテストは簡単なものとして、"System"イベントログの個数が10個以下になっていればPassとしました(イベントはすぐに発生して記録されますので、0になっている時間がとても短いため)。

---
- name: Verify
  hosts: all
  gather_facts: false
  tasks:
    - name: Get System eventlog
      win_shell: 'get-eventlog system'
      register: result
      
    - name: show result
      debug:
        msg: "length: {{ result.stdout_lines | length }}"
        
    - name: Example assertion
      assert:
        that: "{{ result.stdout_lines | length }} < 10"

それから、このRole自体は冪等性を持っているのですが、実際に2回続けて実行すると(削除してもその直後にイベントが記録されるため)、2回目も"changed"が現れてしまいます。そこで、(必ずFailして止まってしまう)冪等性のチェックは行わないよう、molecule.ymlに以下の行を追加しました。

scenario:
  name: default
  test_sequence:
    - destroy
    - create
    - converge
    - verify
    - destroy

これで、デフォルトのテストシーケンスが上書きされ、冪等性チェックが存在しないシーケンスとなります。

では、"molecule test"を実行して見ましょう。

画像3

無事にコンテナが作られ、Roleが実行されてすべてのイベントログが削除され、"System"イベントログは6個しなかい状態になっていることが確認できました。コンテナが削除されたことも、Docker Desktopから確認しましたので、一連の動作は期待通りに動いたことになります。

まとめ

ということで、なんとかMoleculeでWindows用Roleのテストができました。しかし、まだいくつか問題があります。

1つはコードをGitHub等で共有して共同開発する場合やCI化を進めようとすると、コンテナイメージをPullしてくるところを自動化し、IPアドレスをハードコードしている部分をなんとかする必要がある点です。

また、2つ目に、Windows Server CoreにはWindows Serverと異なる点が少なからずある点です。それによる影響として、このコンテナ内で実行できないモジュールが存在し、例えばリブートが含まれるRoleは動作しない、とか、あるいはWindowsの機能の一部(例えばBitlockerなど)は期待通りには動作しません。これらに関するRoleのテストは、この環境ではできないことになり、やはりAWSやVMWare等の環境でテストする必要がありそうです。

それでも、1台のノートPCでお金をかけずにRoleのテストが実行できる環境が手に入ったのは大きく、今後はLinux系OSを操作するRoleも、Windows系OSを操作する多くのRoleも、Moleculeを使ってテストも同時に書きながら開発していけそうです。
――――――――――――――――――――――――――――――――――

執筆者プロフィール:水谷 裕一
大手外資系IT企業で15年間テストエンジニアとして、多数のプロジェクトでテストの自動化作業を経験。その後画像処理系ベンチャーを経てSHIFTに入社。
SHIFTでは、テストの自動化案件を2件こなした後、株式会社リアルグローブ・オートメーティッド(RGA)にPMとして出向中。RGAでは主にAnsibleに関する案件をプレーイングマネジャーとして担当している。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/