🐣

Ansibleで接続可能なEC2のWindows Serverデプロイを自動化する

2020/12/17に公開

はじめに

この記事は、「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を有効にするか、コミュニティが提供するスクリプトを実行して有効化する必要がありました。
https://github.com/ansible/ansible/blob/devel/examples/scripts/ConfigureRemotingForAnsible.ps1

さらにいえば、Windowsへ接続する際のパスワードは、EC2のキーペアをアップロードしてパスワードをブラウザから取得する必要があります。そのため、Linux環境のようにキーペアを使ってパスワードレスでSSH接続するといったことができません。

つまり、

  1. EC2ダッシュボードに行きWindows環境をデプロイ
  2. インスタンスが起動するのを待ってパスワードを取得
  3. 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]
https://github.com/hiyokotaisa/ansible_deploy_win_ec2/blob/main/deploy_win_instance.yml

そこまで複雑なPlaybookではないので要点のみを説明します。

1. ConfigureRemotingForAnsible.ps1を実行するスクリプトを<powershell>タグで囲い、init.ps1 ファイルを作成

先程説明したユーザーデータスクリプトを事前に作成しておきます。このスクリプトがインスタンス作成時に実行されることになります。

<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に対する保証等は一切ないので、「こんなこともできるんだ」レベルで参考程度にとどめていただければ幸いです。

脚注
  1. このスクリプトの実行は検証目的であり、実運用環境での利用を想定していません。本番環境など実際の運用時は、このスクリプトではなく必要な作業を抽出したうえで実行してください。 ↩︎

  2. このPlaybookはあくまでサンプルであり、動作の保証やその他サポート等一切致しかねますので、あくまでサンプルとしてご活用ください ↩︎

Discussion