Ansible で Azure 上の Windows 10 開発環境の整備と win_dsc モジュールを試す

この記事は、[FUJITSU Advent Calendar 2017](https://qiita.com/advent-calendar/2017/fujitsu) の 13日目の記事です。
11日目の記事[「ansibleでリモート側にある複数のzipファイルを展開する方法」](https://qiita.com/chigiriki/items/bd12adad7d17d68f086b) に引き続き、Ansible について書きます。

MS が今年リリースした機能の中で、個人的に素晴らしいと感じた 2 つの機能をベースに、Azure 上に Windows 開発環境を構築してみました。また、今年 9 月に[リリース](https://groups.google.com/forum/#!topic/ansible-announce/IrLR3pjNZnk)された Ansible 2.4 で正式追加となったモジュール win_dsc を実際に触ってみました。

## tl;dr

- [Nested Virtualization in Azure](https://blogs.technet.microsoft.com/jpitpro/2017/08/01/nested-virtualization-in-azure/) は開発環境にやさしい
- [Ansible](https://www.ansible.com/) on [Windows Subsystem for Linux](https://msdn.microsoft.com/ja-jp/commandline/wsl/about) は開発環境の整備に使える
- Ansible v2.4 から導入された [win_dsc](http://docs.ansible.com/ansible/latest/win_dsc_module.html) モジュールは、[Windows PowerShell Desired State Configuration](https://docs.microsoft.com/ja-jp/powershell/dsc/overview) の [組み込みリソース](https://docs.microsoft.com/ja-jp/powershell/dsc/builtinresource) もしくは [PowerShell Gallery](https://www.powershellgallery.com/) にあるカスタムリソースを使いたい場合には有効

## 注意事項
> The Windows Subsystem for Linux is not supported by Microsoft or Ansible and should not be used for production systems.
[- Can Ansible run on Windows? - Ansible doc](http://docs.ansible.com/ansible/devel/windows_faq.html#can-ansible-run-on-windows)

WSL で Ansible を動作させることは、Production 環境ではサポートされていないため、ご注意ください。

## Ansible と Desired State Configuration

### Ansible
RedHat が提供するシンプルな運用自動化ツールです。オープンソースコミュニティでの開発も活発です。Playbook と呼ばれる YAML 形式の設定ファイルに、宣言的にリソースの状態を記述します。Chef や Puppet との違いは [＠IT の記事](http://www.atmarkit.co.jp/ait/articles/1606/15/news017_8.html)にまとめられています。Ansible のエンジンやモジュール (ライブラリ) は、Python で開発されていますが、Windows モジュールに関しては、対象マシンとの接続に使う WinRM プロトコルと親和性が高く、.Net フレームワークの恩恵が得られる Powershell で開発されています。

Ansible は、Ansibleの処理単位である Role の共有サービス [Ansible Galaxy](https://galaxy.ansible.com/) や、Ansible エンジンに加え GUI ダッシュボードやタスクのスケジューリングを可能にする [Ansible Tower](https://www.ansible.com/tower)、そのアップストリーム版にあたる [AWX](https://github.com/ansible/awx) などを提供しています。

また、コンテナイメージを Ansible の既存の Playbook を使って作成したり、実行したりできる [Ansible Container](http://docs.ansible.com/ansible-container/) なんかもあります。

### Desired State Configuration
Microsoft が提供する Powershell をベースとした構成管理ツールです。こちらもオープンソースとして[公開](https://github.com/PowerShell/DscResources)されていますが、開発コミュニティは活発とは言えません。DSC は対象マシンへ WinRM で接続したあと、現在の状態の取得、構成に変更が必要かチェック、構成の変更の3段階に分けてタスクを実行します。Ansible でいうモジュールは、DSC ではリソースと呼ばれます。

### win_dsc モジュール
Ansible の win_dsc モジュールは、対象ホスト上に存在する DSC リソースを実行する wrapper モジュールです。もともと、[非公式のモジュール](https://github.com/trondhindenes/Ansible-win_dsc)として公開されていましたが、v2.4 からめでたく公式モジュール入りしました。ただでさえ少ない Windows モジュールの救世主になるのではと、多くの人が待ち望んでいたであろうモジュールです。(遠い目)

## Ansible on Windows Subsystem for Linux
今回は、Azure 上に Nested Virtualization に対応した [Dv3 サイズ](https://azure.microsoft.com/ja-jp/blog/introducing-the-new-dv3-and-ev3-vm-sizes/) の VM を立て、そこでいろいろと遊びました。WSL を試したいので、[Windows 10 Pro, Version 1709](https://azuremarketplace.microsoft.com/en-us/marketplace/apps/Microsoft.Windows10RS3Prox64-ARM?tab=Overview) の VM イメージを使っています。Windows Server 2016 もバージョン 1709 から Semi-Anual Channel リリースに対応していますが、[デスクトップ搭載の Windows Server は未対応](https://docs.microsoft.com/ja-jp/windows-server/get-started/semi-annual-channel-overview#release-channels-and-installation-options)なため、今回は Windows 10 を選択しました。

また、[Vagrant](https://www.vagrantup.com/) を使って [VirtualBox](https://www.virtualbox.org/) 上にゲスト OS (検証環境) を構築し、win_dsc モジュールを試します。

### ホスト OS の構成
ホスト OS の構成は以下の通りです。

#### Azure IaaS
- VM サイズ: Standard D2 v3 (2 vcpu 数、8 GB メモリ)
- OS: Windows 10, Version 1709 (OS Build 16299.64)
- VirtualBox: v5.1.30
- Vagrant: v2.0.1

### Ansible 実行環境を整備
WSL で Ubuntu のパッケージを使用しました。Ubuntu の導入に関しては、[Build Insider の記事](http://www.buildinsider.net/enterprise/wsl/01#wslinstall)に詳しく書かれているため割愛します。

```
$ cat /etc/os-release
NAME="Ubuntu"
VERSION="16.04.3 LTS (Xenial Xerus)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 16.04.3 LTS"
VERSION_ID="16.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
VERSION_CODENAME=xenial
UBUNTU_CODENAME=xenial
```

Ansible のインストール手順は[公式のドキュメント](http://docs.ansible.com/ansible/latest/intro_windows.html)を見るのが正確ですが、参考までに記載しておきます。今回はドメインに参加しているアカウントでもクレデンシャルを委譲してよしなにやってくれる CredSSP 認証によって WinRM 接続を確立するよう設定します。

```
$ sudo apt-get update
$ sudo apt-get install python-pip git libffi-dev libssl-dev -y
$ sudo pip install --upgrade pip
$ sudo pip install ansible pywinrm
$ sudo pip install pywinrm[credssp]
```

```
$ ansible --version
ansible 2.4.1.0
  config file = None
  configured module search path = [u'/home/toversus/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /home/toversus/.local/lib/python2.7/site-packages/ansible
  executable location = /home/toversus/.local/bin/ansible
  python version = 2.7.12 (default, Nov 19 2016, 06:48:10) [GCC 5.4.0 20160609]
```

#### ホスト OS との接続確認
Ansible が公式に提供している　Windows 環境用のセットアップスクリプト [examples/scripts/ConfigureRemotingForAnsible.ps1](https://github.com/ansible/ansible/blob/devel/examples/scripts/ConfigureRemotingForAnsible.ps1) をホスト OS 上で実行します。

```:powershell
PS > Invoke-WebRequest `
-Uri https://raw.githubusercontent.com/ansible/ansible/devel/examples/scripts/ConfigureRemotingForAnsible.ps1 `
-OutFile ConfigureRemotingForAnsible.ps1
PS > powershell -ExecutionPolicy RemoteSigned .\ConfigureRemotingForAnsible.ps1 -EnableCredSSP
```

インベントリファイルを作成し、接続先のマシンの情報を書いておきます。

```:development
[windows]
localhost

[windows:vars]
ansible_user=<your_account>
ansible_password=<your_password>
ansible_port=5986
ansible_connection=winrm
ansible_winrm_server_cert_validation=ignore
```

ホスト OS に対して win_ping モジュールで疎通確認します。

```
$ ansible windows -m win_ping -i development
localhost | SUCCESS => {
    "changed": false,
    "failed": false,
    "ping": "pong"
}
```

無事ホスト OS との接続を確立できました。

#### SSL 絡みのエラーが出たら...
```
localhost | UNREACHABLE! => {
    "changed": false,
    "msg": "ssl: HTTPSConnectionPool(host='localhost', port=5986): Max retries exceeded with url: /wsman (Caused by SSLError(SSLError(\"bad handshake: SysCallError(-1, 'Unexpected EOF')\",),))",
    "unreachable": true
}
```
ConfigureRemotingForAnsible.ps1 に「-ForceNewSSLCert」オプションを渡して、自己署名証明書を再度発行しましょう。

```:powershell
PS > powershell -ExecutionPolicy RemoteSigned .\ConfigureRemotingForAnsible.ps1 -ForceNewSSLCert
```

[参考: Setting up a Windows Host](http://docs.ansible.com/ansible/devel/windows_setup.html)

## 開発環境の整備
せっかくなので、Ansible on WSL でホスト OS を開発環境として整備してみます。

### 作業ディレクトリの構成
Ansible のベストプラクティスに書かれている[ディレクトリ構成](http://docs.ansible.com/ansible/latest/playbooks_best_practices.html#directory-layout)を参考にしました。

```
$ tree
.
├── ansible.cfg
├── dev4win.yml
├── development
└── roles
   └── dev4win
       ├── handlers
       │   └── main.yml
       └── tasks
           └── main.yml

4 directories, 5 files
```

### ansible.cfg の作成
Playbook の実行に失敗したときに、毎度 .retry ファイルを作成されると鬱陶しいので、.retry ファイルを作成しないよう設定します。また、Ansible の実行ログを保存するよう設定します。

```:ansible.cfg
[defaults]
retry_files_enabled = False
log_path=/var/tmp/ansible.log
```

### handlers の定義
特定のタスクが実行され、状態が変化した (changed を返した) 場合に、それをトリガーとして起動するタスクを定義します。例えば、あるタスクが実行されたときだけマシンを再起動したい場合に使えます。その他 handlers の特徴は [「Ansible: notify と handlers の使い方について調べた」](http://blog.amedama.jp/entry/2015/09/01/214912)に詳しく書かれています。

```:roles/dev4win/handlers/main.yml
- name: reboot a machine
  win_reboot:
    reboot_timeout_sec: 6000
    test_command: whoami

- name: restart Explorer process
  win_shell: Stop-Process -Name Explorer
```

### tasks の定義
task で定義した内容は以下の通りです。

- システムロケールの変更
- ログインユーザーのパスワード無期限化
- 「自動的に現在のフォルダーまで展開する」を有効化
- 「ファイル名拡張子の表示」を有効化
- ファイアウォールの受信ルールで ping を受け付けるよう設定
- chocolatey 経由でパッケージをインストール
    - chocolatey で入手したいパッケージは[ここ](https://chocolatey.org/packages)で探せます。

```:roles/dev4win/tasks/main.yml
- name: Get current system locale
  win_shell: (Get-WinSystemLocale).Name
  register: locale
  changed_when: false

- name: Set system locale to ja-JP
  win_shell: Set-WinSystemLocale ja-JP
  when: locale.stdout.find('ja-JP') == -1
  notify: reboot a machine

- name: Set individual user's password to never expire
  win_user:
    name: "{{ ansible_env.USERNAME }}"
    password_expired: no

- name: Enable automatically expand to current folder option
  win_regedit:
    path: HKCU:\\Software\\Microsoft/Windows\\CurrentVersion\\Explorer\\Advanced
    name: NavPaneExpandToCurrentFolder
    data: 1
    type: dword
  notify: restart Explorer process

- name: Show file name extentions
  win_regedit:
    path: HKCU:\\Software\\Microsoft/Windows\\CurrentVersion\\Explorer\\Advanced
    name: HideFileExt
    data: 0
    type: dword
  notify: restart Explorer process

- name: Enable firewall rule for receiving echo request
  win_firewall_rule:
    name: Virtual Machine Monitoring (Echo Request - ICMPv4-In)
    direction: in
    action: allow

- name: Install latest packages via chocolatey
  win_chocolatey:
    name: "{{ item }}"
    state: latest
  with_items:
    - chocolatey-core.extension
    - 7zip.install
    - visualstudiocode
    - sourcetree
    - teraterm
    - git

- name: Install fixed verion of packages via chocolatey
  win_chocolatey:
    name: "{{ item.name }}"
    version: "{{ item.version}}"
    state: present
  with_items:
    - name: virtualbox
      version: 5.1.30
    - name: vagrant
      version: 2.0.1
    - name: nodejs.install
      version: 6.12.1
    - name: nvm
      version: 1.1.5
    - name: googlechrome
      version: 62.0.3202.94
    - name: selenium-chrome-driver
      version: 2.33
```

### Playbook の作成
Playbook を作成し、先ほど定義した task をインポートします。

```:dev4win.yml
- hosts: windows
  gather_facts: yes
  tasks:
    - import_tasks: roles/dev4win/tasks/main.yml
  handlers:
    - import_tasks: roles/dev4win/handlers/main.yml
```

### Playbook の実行

全てのタスクが実行されたあと、handlers で定義したホスト OS の再起動が走ります。

```
$ ansible-playbook dev4win.yml -i development

PLAY [windows] *************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************
ok: [localhost]

TASK [Get current system locale] ***************************************************************************************
ok: [localhost]

TASK [Set system locale to ja-JP] **************************************************************************************
changed: [localhost]

TASK [Set individual user's password to never expire] ******************************************************************
ok: [localhost]

TASK [Enable automatically expand to current folder option] ************************************************************
changed: [localhost]

TASK [Show file name extentions] ***************************************************************************************
changed: [localhost]

TASK [Enable firewall rule for receiving echo request] *****************************************************************
changed: [localhost]

TASK [Install latest packages via chocolatey] **************************************************************************
changed: [localhost] => (item=chocolatey-core.extension)
changed: [localhost] => (item=7zip.install)
changed: [localhost] => (item=visualstudiocode)
changed: [localhost] => (item=sourcetree)
changed: [localhost] => (item=teraterm)
changed: [localhost] => (item=git)
 [WARNING]: Chocolatey was missing from this system, so it was installed during this task run.


TASK [Install fixed verion of packages via chocolatey] **************************************************************************
changed: [localhost] => (item={u'version': u'5.1.30', u'name': u'virtualbox'})
changed: [localhost] => (item={u'version': u'2.0.1', u'name': u'vagrant'})
changed: [localhost] => (item={u'version': u'6.12.1', u'name': u'nodejs.install'})
changed: [localhost] => (item={u'version': u'1.1.5', u'name': u'nvm'})
changed: [localhost] => (item={u'version': u'62.0.3202.94', u'name': u'googlechrome'})
changed: [localhost] => (item={u'version': u'2.33', u'name': u'selenium-chrome-driver'})
```

### 追加のタスクを定義
追加で以下のタスクを定義してみます。
- GO のインストール
- GO の作業フォルダの作成
- ユーザー環境変数に GOPATH を追加

全てのタスクを一から実行するのはだるいので、tags を設定して追加のタスクのみ実行します。

```:roles/dev4win/tasks/main.yml
- name: Install Golang v1.9.2 via chocolatey
  win_chocolatey:
    name: golang
    version: 1.9.2
    state: present
  tags: golang

- name: Create Go workplace
  win_file:
    path: "{{ ansible_env.USERPROFILE }}/go"
    state: directory
  tags: golang

- name: Set GOPATH environment variable for current user
  win_path:
    name: GOPATH
    elements: "{{ ansible_env.USERPROFILE }}/go"
    scope: user
    state: present
  tags: golang
```

### 追加のタスクの実行
tags オプションを指定することで、特定のタスクのみを実行できます。

```
$ ansible-playbook dev4win.yml -i development --tags golang

PLAY [windows] **************************************************************************************

TASK [Gathering Facts] ******************************************************************************
ok: [localhost]

TASK [Install Golang v1.9.2 via chocolatey] *********************************************************
changed: [localhost]

TASK [Create Go workplace] **************************************************************************
changed: [localhost]

TASK [Set GOPATH environment variable for current user] *********************************************
changed: [localhost]

PLAY RECAP ******************************************************************************************
localhost                  : ok=4    changed=3    unreachable=0    failed=0
```

以上で、Windows 10 開発環境の整備は終了です。

## VirtualBox 上に Windows Server 2016 評価版を構築
ベースとなる Vagrant box として「[gusztavvargadr/w16s Vagrant box](https://app.vagrantup.com/gusztavvargadr/boxes/w16s)」を使用しました。

作成した Vagrantfile は以下の通りです。「config.winrm.retry_limit」と「config.winrm.retry_delay」 は、VM の起動でタイムアウトが頻発したので付け足しました。

```ruby:Vagrantfile
# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.box = "gusztavvargadr/w16s"
  config.vm.network "private_network", ip: "192.168.56.10"
  config.winrm.retry_limit = 30
  config.winrm.retry_delay = 20
  config.vm.provider "virtualbox" do |vb|
    vb.gui = true
    vb.name = "win2016-test01"
    vb.memory = "2048"
    vb.cpus = 1
    vb.customize [ "modifyvm", :id, "--clipboard", "bidirectional", "--draganddrop", "bidirectional" ]
  end
end
```

- Windows Server 2016 評価版 box を起動します。

```:powershell
PS> vagrant up
```

### ゲスト OS で WinRM のセットアップ
ホスト OS のときと同じなので割愛します。

### 動作確認
hosts ファイルに接続先の情報を追記します。

```:staging
[winservers]
192.168.56.10

[winservers:vars]
ansible_user=vagrant
ansible_password=vagrant
ansible_port=5986
ansible_connection=winrm
ansible_winrm_server_cert_validation=ignore
ansible_winrm_transport=credssp
```

win_ping モジュールで疎通確認します。

```
$ ansible winservers -m win_ping -i staging
192.168.56.10 | SUCCESS => {
    "changed": false,
    "failed": false,
    "ping": "pong"
}
```

### win_dsc モジュールを使う
ここからやっと本題です。
今回は、[xCertificate](https://github.com/PowerShell/xCertificate) リソースを操って、証明書ストアに .pfx ファイル証明書をインポートしてみます。

#### 作業ディレクトリの構成

```
$ tree
.
├── ansible.cfg
├── dev4win.yml
├── development
├── roles
│   ├── common
│   │   ├── certs
│   │   │   └── myserver.pfx
│   │   └── tasks
│   │       └── main.yml
│   └── dev4win
│       ├── handlers
│       │   └── main.yml
│       └── tasks
│           └── main.yml
├── staging
└── winservers.yml

7 directories, 9 files
```

#### task の定義
xCertificate リソースに必要な引数は[ここ](https://github.com/PowerShell/xCertificate/blob/dev/Modules/xCertificate/DSCResources/MSFT_xPfxImport/MSFT_xPfxImport.psm1#L45)で確認できます。証明書のインポートに必要なクレデンシャルを
 PSCredential で渡しているのが分かります。Ansible で PSCredential を取り扱うときは、[注意](https://docs.ansible.com/ansible/devel/windows_dsc.html#pscredential)が必要です。クレデンシャルの情報をログに吐き出さないようにするためには、no_log:true を定義したタスクに追加する必要があります。

```:roles/common/tasks/main.yml

- name: install xCertificate DSC resource
  win_psmodule:
    name: xCertificate
    state: present

- name: Copy certs file to tmp directory
  win_copy:
    src: roles/common/certs/myserver.pfx
    dest: C:/tmp/

- name: import pfx certificate
  win_dsc:
    resource_name: xPfxImport
    Thumbprint: 8b952114b7774787a2d7c7492eb8ca15473cfffd
    Path: C:/tmp/myserver.pfx
    Location: LocalMachine
    Store: My
    Credential_username: vagrant
    Credential_password: XXXXXXXXXX
  no_log: true
```

#### Playbook の作成
先ほど定義した task をインポートする Playbook を作成します。

```:staging.yml
- hosts: staging
  gather_facts: no
  tasks:
    - import_tasks: roles/common/tasks/main.yml
```

#### Playbook の実行

```
$ ansible-playbook winservers.yml -i staging

PLAY [winservers] *********************************************************************************************************

TASK [install xCertificate DSC resource] *******************************************************************************
changed: [192.168.56.10]

TASK [Copy certs file to tmp directory] ********************************************************************************
changed: [192.168.56.10]

TASK [import pfx certificate] ******************************************************************************************
changed: [192.168.56.10]

PLAY RECAP *************************************************************************************************************
192.168.56.10              : ok=0    changed=3    unreachable=0    failed=0
```

素晴らしいですね！
2017/12/13 現在 Ansible の Windows モジュールに証明書を扱うモジュールはないので、Powershell のコマンドレットを組み合わたり、冪等性をどう担保するか考える必要がなくなり (証明書の場合は、証明書ストアに Test-Path して無かったら入れればよいだけですが)、楽になります。ただし、xCertificate は試験的なリソースとという扱いなので、何かあっても自己責任な部分は変わりません。

他にも多くの DSC リソースが [Powershell Gallery](https://www.powershellgallery.com/) で公開されているので、探してみるのも良いかもしれません。(ちなみに、私は他に使えそうなリソースを見つけることができませんでした。)

## まとめ
Ansible on WSL で Windows 開発環境を整備し、win_dsc モジュールを試してみました。win_dsc モジュールに関しては、魅力的な only on DSC リソースが個人的に少ないと感じたので、今後に期待です。WSL 自体はどんどん機能拡張していっており、次期 Windows 10 メジャーアップデートから[バックグランドタスクにも対応](https://forest.watch.impress.co.jp/docs/news/1095045.html)するかもしれません。Windows で開発 (笑) と笑われない時代が割と真面目に来る予感がしています。

**注意: この記事の内容は、個人の見解であり、所属する会社・組織を代表するものではありません。**