AnsibleでWindowsをSSHサーバにする

# はじめに

今回はWindowsをAnsibleを使ってSSHサーバにしてみる。

環境は、操作する側がAnsible 1.7.2であり、操作される側はWindows 8.1 Updateである。
[AnsibleでWindowsを操作する準備をする](http://qiita.com/yunano/items/f9d5652a296931a09a70)が終わっているものとする。

使用しているインベントリファイルを再掲する。

```text:hosts
[windows]
10.0.2.172

[windows:vars]
ansible_ssh_user=<Windows側のユーザ名>
ansible_ssh_pass=<Windows側ユーザのパスワード>
ansible_ssh_port=5986
ansible_connection=winrm
```

## これまでのお話

準備編：[AnsibleでWindowsを操作する準備をする](http://qiita.com/yunano/items/f9d5652a296931a09a70)
実践編1： [AnsibleでChocolateyを使ってWindowsアプリをインストールする](http://qiita.com/yunano/items/5677685b61816f6e62a3)
実践編2： [AnsibleでWindows ServerにActive Directoryを構成する](http://qiita.com/yunano/items/8bddf084007671a38f57)

# SSHサーバにする

WindowsをSSHサーバにするために定番のcygwinを使用することにする。
以下のような手順で実施する。

* （任意で）cygwinのインストーラやルートディレクトリを配置するフォルダを作成する
* cygwinのインストーラをダウンロードする
* cygwinとopensshパッケージをインストールする
* sshdをWindowsのサービスとして登録して起動する
* Windowsファイアウォールにsshdの分の穴を開ける
* （任意で）pythonパッケージをインストールする

最終的には以下のようなplaybookになるので`ansible-playbook -i hosts ssh.yml`を実行すれば良い。

```yaml:ssh.yml
- hosts: windows
  vars:
    download_folder: C:/tools
    download_folder_bs: C:\tools
    cygwin_setup_exe: "{{ download_folder }}/setup-x86_64.exe"
    cygwin_root: "{{ download_folder }}/cygwin64"
    cygwin_packages_folder: "{{ download_folder }}/cygwin64packages"
    cygwin_remote_site: ftp://ftp.iij.ad.jp/pub/cygwin
    sshd_service_name: sshd
    ssh_firewall_name: SSH
  tasks:
    # フォルダを作成する
    - win_stat: path={{ download_folder }}
      register: folder_info
    - raw: "mkdir {{ download_folder_bs }}"
      when: not folder_info.stat.exists

    # cygwinのインストーラをダウンロードする
    - win_stat: path={{ cygwin_setup_exe }}
      register: file_info
    - win_get_url: url=http://cygwin.com/setup-x86_64.exe dest={{ cygwin_setup_exe }}
      when: not file_info.stat.exists

    # cygwinとopensshパッケージをインストールする
    - raw: "{{ cygwin_setup_exe }} -R {{ cygwin_root }} -l {{ cygwin_packages_folder }} -q -s {{ cygwin_remote_site }} -P openssh"

    # sshdをWindowsのサービスとして登録して起動する
    - raw: 'sc query | findstr /C:"SERVICE_NAME: {{ sshd_service_name }}"'
      register: service_info
      failed_when: False
    - raw: '{{ cygwin_root }}/bin/bash --login -c "/bin/ssh-host-config -y -c ntsec -u sshd_account -w {{ ansible_ssh_pass }}"'
      when: '"{{ sshd_service_name }}" not in service_info.stdout'
    - win_service: name={{ sshd_service_name }} state=started

    # Windowsファイアウォールに穴を開ける
    - raw: netsh advfirewall firewall show rule name={{ ssh_firewall_name }}
      register: firewall_info
      failed_when: False
    - raw: netsh advfirewall firewall add rule Profile=private name={{ ssh_firewall_name }} dir=in localport=22 protocol=TCP action=allow
      when: '"{{ ssh_firewall_name }}" not in firewall_info.stdout'

    # pythonパッケージをインストールする
    - raw: "{{ cygwin_setup_exe }} -R {{ cygwin_root }} -l {{ cygwin_packages_folder }} -q -s {{ cygwin_remote_site }} -P python"
```

playbookを上から説明していこう。

## フォルダを作成する

以下が該当箇所になる。

```yaml
- hosts: windows
  vars:
    download_folder: C:/tools
    download_folder_bs: C:\tools
  tasks:
    - win_stat: path={{ download_folder }}
      register: folder_info
    - raw: "mkdir {{ download_folder_bs }}"
      when: not folder_info.stat.exists
```

[AnsibleでChocolateyを使ってWindowsアプリをインストールする](http://qiita.com/yunano/items/5677685b61816f6e62a3)でファイルが存在するかどうかをwin_statモジュールで調べることができることを説明したが、同じ書き方でフォルダが存在するかも調べることができる。

rawを使ってmkdirする時は何故かフォルダの区切り文字にスラッシュが使用できずバックスラッシュでないとエラーになる。
さらに、regex_replaceフィルタはバックスラッシュが入った文字列から置換するのも、バックスラッシュが入った文字列に置換するのもどうも上手く行かずエラーになるので、仕方なくvarsにはスラッシュ版(download_folder)とバックスラッシュ版(download_folder_bs)を定義した。
まあ全部バックスラッシュでもいいんだけど。

## cygwinのインストーラをダウンロードする

以下が該当箇所になる。

```yaml
- hosts: windows
  vars:
    download_folder: C:/tools
    cygwin_setup_exe: "{{ download_folder }}/setup-x86_64.exe"
  tasks:
    - win_stat: path={{ cygwin_setup_exe }}
      register: file_info
    - win_get_url: url=http://cygwin.com/setup-x86_64.exe dest={{ cygwin_setup_exe }}
      when: not file_info.stat.exists
```

ファイルのダウンロードにwin_get_urlモジュールを使用しているが、すでにダウンロードしてあっても何度もダウンロードしてくれるので、こちらもwin_statモジュールを使ってダウンロードするかを制御している。

win_get_urlモジュールは以下の特徴がある。

* ダウンロード先のフォルダは先に存在している必要がある
* ダウンロード先はファイル名まで指定する必要がある
* urlとdestしか指定できないので細かい制御とかできない

## cygwinとopensshパッケージをインストールする

以下が該当箇所になる。

```yaml
- hosts: windows
  vars:
    download_folder: C:/tools
    cygwin_setup_exe: "{{ download_folder }}/setup-x86_64.exe"
    cygwin_root: "{{ download_folder }}/cygwin64"
    cygwin_packages_folder: "{{ download_folder }}/cygwin64packages"
    cygwin_remote_site: ftp://ftp.iij.ad.jp/pub/cygwin
  tasks:
    - raw: "{{ cygwin_setup_exe }} -R {{ cygwin_root }} -l {{ cygwin_packages_folder }} -q -s {{ cygwin_remote_site }} -P openssh"
```

cygwinのインストーラを-qオプション付きでコマンドライン実行するとサイレントインストールができる。
-Pオプションの後にはcygwinに追加でインストールしたいパッケージ名を指定する。ここではopensshである。
cygwinのGUIインストーラだとダウンロード元は ftp://ftp.iij.ad.jp などと表示されるが、-sオプションで指定するのは ftp://ftp.iij.ad.jp/pub/cygwin などとなるようだ。

## sshdをWindowsのサービスとして登録して起動する

以下が該当箇所になる。

```yaml
- hosts: windows
  vars:
    download_folder: C:/tools
    cygwin_root: "{{ download_folder }}/cygwin64"
    sshd_service_name: sshd
  tasks:
    - raw: 'sc query | findstr /C:"SERVICE_NAME: {{ sshd_service_name }}"'
      register: service_info
      failed_when: False
    - raw: '{{ cygwin_root }}/bin/bash --login -c "/bin/ssh-host-config -y -c ntsec -u sshd_account -w {{ ansible_ssh_pass }}"'
      when: '"{{ sshd_service_name }}" not in service_info.stdout'
    - win_service: name={{ sshd_service_name }} state=started
```

sshdをWindowsにサービスとして登録するにはcygwinの中でssh-host-configを実行する。
上記のようなオプションを付けてssh-host-configを実行すれば応答なしでWindowsのサービスとして登録できる。

サービスの起動にはwin_serviceモジュールを使用する。

なお、sshdサービスが起動している状態でssh-host-configを実行するとエラーになるので、最初のsc queryで起動済みかどうかをチェックしている。

## Windowsファイアウォールに穴を開ける

以下が該当箇所になる。

```yaml
- hosts: windows
  vars:
    ssh_firewall_name: SSH
  tasks:
    - raw: netsh advfirewall firewall show rule name={{ ssh_firewall_name }}
      register: firewall_info
      failed_when: False
    - raw: netsh advfirewall firewall add rule Profile=private name={{ ssh_firewall_name }} dir=in localport=22 protocol=TCP action=allow
      when: '"{{ ssh_firewall_name }}" not in firewall_info.stdout'
```

ここでは22/tcpを内向きにprivateネットワークプロファイル上に穴を開けているが、domainネットワークプロファイルを使っているなら「Profile=domain」とすれば良い。

なお、もうすでに穴が開いていた状態であっても、netsh advfirewall firewall add ruleを実行する度に（実効的には何も変化はないが）設定としては記録され残るので、すでに開いているかをチェックしてから実行している。

## pythonパッケージをインストールする

で、SSHサーバにしたんだったら、せっかくなのでSSHでもAnsibleで操作したくなるよね、というわけでcygwinにpythonパッケージをインストールする。
以下が該当箇所になる。

```yaml
- hosts: windows
  vars:
    download_folder: C:/tools
    cygwin_setup_exe: "{{ download_folder }}/setup-x86_64.exe"
    cygwin_root: "{{ download_folder }}/cygwin64"
    cygwin_packages_folder: "{{ download_folder }}/cygwin64packages"
    cygwin_remote_site: ftp://ftp.iij.ad.jp/pub/cygwin
  tasks:
    - raw: "{{ cygwin_setup_exe }} -R {{ cygwin_root }} -l {{ cygwin_packages_folder }} -q -s {{ cygwin_remote_site }} -P python"
```

パッケージ名をopensshからpythonに変えただけで、opensshパッケージをインストールした時と同じである。

playbookの説明はここまで。
実際にSSHサーバ経由で操作してみる。

`ansible-playbook -i hosts ssh.yml`を実行した後、SSHサーバにアクセスするインベントリファイルを作っておく。

```text:win-ssh-hosts
[windows]
10.0.2.172

[windows:vars]
ansible_ssh_user=<Windows側のユーザ名>
```

pingでも良いが、win_*モジュールでできないことをやってみよう、というわけでcopyモジュールを使ってみる。
`ssh-copy-id <Windows側のユーザ名>@10.0.2.172`してから以下のansibleコマンドを実行。

```
# ansible -i win-ssh-hosts -m copy -a 'src=ssh.yml dest=/cygdrive/c/tools/' windows

10.0.2.172 | success >> {
    "changed": true,
    "dest": "/cygdrive/c/tools/ssh.yml",
    "gid": 513,
    "group": "\u306a\u3057",
    "md5sum": "ae76b89df8389d9421c27777600c5ae7",
    "mode": "0644",
    "owner": "<Windows側のユーザ名>",
    "size": 1728,
    "src": "/home/<Windows側のユーザ名>/.ansible/tmp/ansible-tmp-1412609970.63-58736849846218/source",
    "state": "file",
    "uid": 1001
}
```

ちなみに、出力のgroupの"\u306a\u3057"は「なし」だ。

以下でコピーできているかを確認。問題ない。
（なお、このmd5値はコメントを入れる前）

```
# ssh <Windows側のユーザ名>@10.0.2.172 md5sum /cygdrive/c/tools/ssh.yml
ae76b89df8389d9421c27777600c5ae7 */cygdrive/c/tools/ssh.yml

# md5sum ssh.yml
ae76b89df8389d9421c27777600c5ae7  ssh.yml
```

# 追記：Ansible 2.0.1でもやってみた

Ansible 2.0で挙動が変わっていたりWindowsモジュールが増えていたりするのでそれに合わせてやってみる。

```yaml:ssh.yml
- hosts: windows
  vars:
    cygwin_setup_exe: "setup-x86_64.exe"
    cygwin_setup_sig: "{{ cygwin_setup_exe }}.sig"
    cygwin_pubkey: "pubring.asc"
    cygwin_download_site: http://cygwin.com
    local_dir: .
    remote_folder: C:/tools
    cygwin_root: "{{ remote_folder }}/cygwin64"
    cygwin_packages_folder: "{{ remote_folder }}/cygwin64packages"
    cygwin_remote_site: ftp://ftp.iij.ad.jp/pub/cygwin
    sshd_service_name: sshd
  tasks:
  # cygwinのインストーラをダウンロードする
  - block:
    - file: path={{ local_dir }}/{{ cygwin_setup_exe }} state=absent
    - file: path={{ local_dir }}/{{ cygwin_setup_sig }} state=absent
    - file: path={{ local_dir }}/{{ cygwin_pubkey }} state=absent
    - get_url: url={{ cygwin_download_site }}/{{ cygwin_setup_exe }} dest={{ local_dir }}/{{ cygwin_setup_exe }}
    - get_url: url={{ cygwin_download_site }}/{{ cygwin_setup_sig }} dest={{ local_dir }}/{{ cygwin_setup_sig }}
    - get_url: url={{ cygwin_download_site }}/key/{{ cygwin_pubkey }} dest={{ local_dir }}/{{ cygwin_pubkey }}
    - command: gpg --import {{ local_dir }}/{{ cygwin_pubkey }}
    - command: gpg --verify {{ local_dir }}/{{ cygwin_setup_sig }} {{ local_dir }}/{{ cygwin_setup_exe }}
    delegate_to: localhost
    run_once: True
    tags: download

  # フォルダを作成する
  - win_file: path={{ remote_folder }} state=directory

  # cygwinのインストーラを操作される側にコピーする
  - win_copy: src={{ local_dir }}/{{ cygwin_setup_exe }} dest={{ remote_folder }}

  # cygwinとopenssh,pythonパッケージをインストールする
  - raw: "{{ remote_folder }}/{{ cygwin_setup_exe }} -R {{ cygwin_root }} -l {{ cygwin_packages_folder }} -q -s {{ cygwin_remote_site }} -P openssh,python"

  # sshdをWindowsのサービスとして登録して起動する
  - block:
    - raw: 'Get-Service -name {{ sshd_service_name }}'
    rescue:
    - raw: '{{ cygwin_root }}/bin/bash --login -c "/bin/ssh-host-config -y -c ntsec -u sshd_account -w {{ ansible_password }}"'
  - win_service: name={{ sshd_service_name }} state=started

  # Windowsファイアウォールに穴を開ける
  - win_firewall_rule: name='Allow SSH' localport=22 protocol=TCP direction=In action=allow profile=private,domain
    failed_when: False
```

* cygwinのインストーラをダウンロードする

win_chocolateyモジュールを使うとパッケージのインストール方法がわからなかったのと、Windows機すべてでダウンロードするのもあまり好ましくないので操作する側に一旦ダウンロードするようにした。これができるのはwin_copyモジュールが追加され、またまともに使えるようになったからでもある。
ついでにダウンロードしたファイルのgpgでの検証も行っている。
またこの処理はdownloadタグを付けて「--skip-tags download」で飛ばせるようにしている。

* フォルダを作成する

Ansible 1.9.2で追加されたwin_fileモジュールが使用できる。

* cygwinのインストーラを操作される側にコピーする

Ansible 1.9.2で追加されたwin_copyモジュールだが、Ansible 2.0で実用レベルになったのでこれを使用できる。

* cygwinとopenssh,pythonパッケージをインストールする

先と特に変わりはないが、今回はopensshとpythonを一緒にインストールしている。
「Windows機すべてでダウンロードするのもあまり好ましくない」ことを考えると、Windows機の台数にもよるがcygwinのミラーリポジトリをローカルに持っておくと良いだろう。

* sshdをWindowsのサービスとして登録して起動する

rawモジュールがPowerShellコマンドを実行するようになったのでscコマンドが通らない。（PowerShellではscはSet-Contentのエイリアスである）
代わりにGet-Serviceを実行し、これがエラーになった時にサービスとして登録するようにしている。
ansible_ssh_passはansible_passwordになった。

* Windowsファイアウォールに穴を開ける

win_firewall_ruleモジュールがAnsible 2.0で追加されたので使用してみたが、必須オプションとは書いていないprofileを指定しないと登録されなかったり同じnameのものを2回実行するとエラーになったり（つまり冪等性がない。「failed_when: False」はこのせいである）で微妙である。