Edited at

AnsibleでWindowsをSSHサーバにする

More than 3 years have passed since last update.


はじめに

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

環境は、操作する側がAnsible 1.7.2であり、操作される側はWindows 8.1 Updateである。

AnsibleでWindowsを操作する準備をするが終わっているものとする。

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


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を操作する準備をする

実践編1: AnsibleでChocolateyを使ってWindowsアプリをインストールする

実践編2: AnsibleでWindows ServerにActive Directoryを構成する


SSHサーバにする

WindowsをSSHサーバにするために定番のcygwinを使用することにする。

以下のような手順で実施する。


  • (任意で)cygwinのインストーラやルートディレクトリを配置するフォルダを作成する

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

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

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

  • Windowsファイアウォールにsshdの分の穴を開ける

  • (任意で)pythonパッケージをインストールする

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


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を上から説明していこう。


フォルダを作成する

以下が該当箇所になる。

- 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アプリをインストールするでファイルが存在するかどうかをwin_statモジュールで調べることができることを説明したが、同じ書き方でフォルダが存在するかも調べることができる。

rawを使ってmkdirする時は何故かフォルダの区切り文字にスラッシュが使用できずバックスラッシュでないとエラーになる。

さらに、regex_replaceフィルタはバックスラッシュが入った文字列から置換するのも、バックスラッシュが入った文字列に置換するのもどうも上手く行かずエラーになるので、仕方なくvarsにはスラッシュ版(download_folder)とバックスラッシュ版(download_folder_bs)を定義した。

まあ全部バックスラッシュでもいいんだけど。


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

以下が該当箇所になる。

- 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パッケージをインストールする

以下が該当箇所になる。

- 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のサービスとして登録して起動する

以下が該当箇所になる。

- 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ファイアウォールに穴を開ける

以下が該当箇所になる。

- 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パッケージをインストールする。

以下が該当箇所になる。

- 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サーバにアクセスするインベントリファイルを作っておく。


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モジュールが増えていたりするのでそれに合わせてやってみる。


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」はこのせいである)で微妙である。