Ansibleで接続可能なEC2のWindows Serverデプロイを自動化する
はじめに
この記事は、「Ansible Advent Calendar 2020」18日目の記事です。
今年も残すところあと僅かとなりました。皆様いかがお過ごしでしょうか。
皆様はWindowsをターゲットノードにしてAnsibleのテストしたいと思ったときに、どのような環境を利用していますか? すぐ使える常設の環境があればよいですが、ライセンスや諸事情で常設の環境を用意できない場合や、ワンタイムで破棄する使い捨ての環境がほしいケースも多々あると思います。
テクニカルサポートでは毎日頻繁にトラブルシューティングを行います。トラブルシューティングの際に必ず必要になるのが、問題を再現する検証環境です。テクニカルサポートがどのような仕事をしているのか興味のある方は、以前書いた『テクニカルサポートに的確に問い合わせる技術』も一読いただければ幸いです。
人によって検証環境の作り方や流儀は異なるのですが、私は多くのケースでVagrantやAWS EC2などのIaaSなどを活用し、環境を使い捨てにしています。これには以下のような理由があります。
- 前回の検証内容によって環境差異が発生し、間違った検証環境となることを防ぐ
- バージョンなどを都度合わせる必要があるため、常設する場合は大量の環境を管理する必要が出てくる
- 管理がめんどい
Linuxでは比較的手軽なのですが、Windowsの環境を用意しようとするとどうしても手順が煩雑になりがちです。Vagrantなどでできなくもないのですが、自分でboxを用意したり何かと手間がかかります。
そこで便利なのがAWS EC2などのIaaS環境です。ライセンスやインスタンスの使用料金はかかってしまいますが、AWS側でWindowsのAMIが提供されているのですぐにWindows環境に接続し検証することができます。また、Ansibleで接続する管理対象はコンテナよりも通常の仮想マシンやIaaS上のインスタンスなどのケースが多いため、そういった意味でも検証に適しています。
WinRMがデフォルトで使えない問題
AnsibleでWindows環境に接続する場合に必要となるのが、WinRMです。(SSH接続も厳密にいえば利用可能ですがここでは除外します)
しかし、EC2上に公式に提供されているWindows Server 2019のイメージは、WinRMがデフォルトで有効化されていません。そのため、手動でWinRMを有効にするか、コミュニティが提供するスクリプトを実行して有効化する必要がありました。
さらにいえば、Windowsへ接続する際のパスワードは、EC2のキーペアをアップロードしてパスワードをブラウザから取得する必要があります。そのため、Linux環境のようにキーペアを使ってパスワードレスでSSH接続するといったことができません。
つまり、
- EC2ダッシュボードに行きWindows環境をデプロイ
- インスタンスが起動するのを待ってパスワードを取得
- RDPクライアントで接続し、WinRMを有効化するスクリプトを実行
ここまでやってようやくAnsibleで管理することができます。自動化の検証のために環境を作成しているのに、温かみのある手作業だらけです。なんとかして自動化したいですよね。
ユーザーデータスクリプトを活用する
EC2では、「ユーザーデータスクリプト」を利用して、インスタンス起動時にコマンドを実行させることができます。
Windows インスタンスでの起動時のコマンドの実行(https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/WindowsGuide/ec2-windows-user-data.html)
ここで、コミュニティが用意しているスクリプトを実行することで、Ansibleで管理する際に必要な設定を一括で実行することができます[1]。
Playbookを作成する
というわけで、この作業を自動化してみたものをGitHub上に公開しました[2]。
そこまで複雑なPlaybookではないので要点のみを説明します。
ConfigureRemotingForAnsible.ps1
を実行するスクリプトを<powershell>
タグで囲い、init.ps1 ファイルを作成
1. 先程説明したユーザーデータスクリプトを事前に作成しておきます。このスクリプトがインスタンス作成時に実行されることになります。
<powershell>
$url = "https://raw.githubusercontent.com/ansible/ansible/devel/examples/scripts/ConfigureRemotingForAnsible.ps1"
$file = "$env:temp\ConfigureRemotingForAnsible.ps1"
(New-Object -TypeName System.Net.WebClient).DownloadFile($url, $file)
powershell.exe -ExecutionPolicy ByPass -File $file
</powershell>
2. ec2モジュールでWindows Server 2019のインスタンスを起動
AWSが提供している公式のWindows Server 2019のAMIからインスタンスを起動します。 user_data
に先程のinit.ps1の中身を渡しています。それ以外は至って普通の指定です。
- name: Launch Windows Instance on EC2
ec2:
wait: yes
key_name: "{{ aws_keyname }}"
image: "{{ aws_imageid }}"
count: 1
group: "{{ aws_security_group }}"
instance_type: "{{ aws_instance_type }}"
instance_tags:
Name: "{{ aws_instance_name }}"
region: "{{ aws_region }}"
user_data: "{{ init_script.stdout }}"
register: ec2
3. Windowsインスタンスのパスワードを取得する
コミュニティモジュールですが、EC2上のWindowsインスタンスのパスワードを取得する「ec2_win_password」というモジュールが存在します。今回はそちらを利用し、生成したWindowsインスタンスのパスワードを取得しています。インスタンス生成時に指定したキーファイルが必要になるので、Playbookと同じディレクトリに事前に配置しておきます。
- name: Getting a password for new instance
ec2_win_password:
instance_id: "{{ item.id }}"
region: "{{ aws_region }}"
key_file: "{{ aws_keypath }}"
wait: yes
wait_timeout: 300
register: win
loop: "{{ ec2.instances }}"
4. 作成したインスタンスに接続する
add_host
で生成したインスタンスを追加し、接続に必要な変数も併せて定義します。
- name: Add new instance to host group
add_host:
hostname: "{{ item.public_ip }}"
groupname: "launched"
ansible_user: Administrator
ansible_password: "{{ (win.results | first).win_password }}"
ansible_winrm_transport: ntlm
ansible_winrm_server_cert_validation: ignore
loop: "{{ ec2.instances }}"
次のPlayでWindowsノードに接続できるまで待機し、接続できるようになったら win_ping
モジュールを実行しています。
- hosts: launched
gather_facts: no
connection: winrm
tasks:
- wait_for_connection:
timeout: 300
- name: Test Ping for new Windows node
win_ping:
実際に実行してみる
では早速実行してみます。このPlaybookを実行する前に、AWSへ接続するための
環境変数(AWS_ACCESS_KEY/AWS_SECRET_KEY)を定義しておいてください。
# ansible-playbook -i hosts deploy_win_instance.yml
PLAY [localhost] *************************************************************************************************************************************************
TASK [Check init.ps1 exists] *************************************************************************************************************************************
ok: [localhost]
TASK [(Optional) Download init.ps1 if does not exist] ************************************************************************************************************
skipping: [localhost]
TASK [Load ps1 script to variable(init_script)] ******************************************************************************************************************
changed: [localhost]
TASK [Launch Windows Instance on EC2] ****************************************************************************************************************************
changed: [localhost]
TASK [Getting a password for new instance] ***********************************************************************************************************************
changed: [localhost] => (item={'id': 'i-0293f8xxxxxxxx', 'ami_launch_index': '0', 'private_ip': '172.31.xx.xxx',(中略)}, 'tenancy': 'default'})
TASK [Add new instance to host group] ****************************************************************************************************************************
changed: [localhost] => (item={'id': 'i-0293f8xxxxxxxx', 'ami_launch_index': '0', 'private_ip': '172.31.xx.xxx',(中略)}, 'tenancy': 'default'})
PLAY [launched] **************************************************************************************************************************************************
TASK [wait_for_connection] ***************************************************************************************************************************************
ok: [xxx.xxx.xxx.xxx]
TASK [Test Ping for new Windows node] ****************************************************************************************************************************
ok: [xxx.xxx.xxx.xxx]
PLAY RECAP *******************************************************************************************************************************************************
xxx.xxx.xxx.xxx : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
localhost : ok=5 changed=4 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
win_ping
モジュールの実行が成功していることがわかります。今回はテスト用にwin_pingを実行しましたが、実際にはこのあとに続けて処理を記述することができますので、一切手作業を挟むことなくWindowsインスタンスに対してAnsibleで接続し、変更を加えることができます。
おわりに
エラー処理やロール化など、やりたいことは山ほどあるのですが、一旦「一応動く」というレベルで公開しておくことにしました。注釈にも記載していますが、このPlaybookに対する保証等は一切ないので、「こんなこともできるんだ」レベルで参考程度にとどめていただければ幸いです。
Discussion