ククログ

株式会社クリアコード > ククログ > Windows用デスクトップアプリケーションをクラウド上の使い捨て検証環境で検証する

Windows用デスクトップアプリケーションをクラウド上の使い捨て検証環境で検証する

Mozillaサポートに従事している結城です。

当社のFirefox/Thunderbirdサポートは多数のお客さまにお使い頂いていますが、インストーラの設定やWindowsのバージョンなどはお客さまによってまちまちです。検証環境をどうやって整えるかは、受託業務では無視できないポイントです。

最近、当社のFirefox/Thunderbirdサポートの検証環境をクラウドに移行しました。この移行でやりたかったのは以下のことでした。

  • Windows 10のVMをクラウド上に置く。

  • 検証環境は常時すぐ動かせる状態で置いておくのではなく、必要に応じてオンデマンドで用意し、検証の度に使い捨てにする。

  • 検証環境は、起動した時点で必要な条件の整備やテストケースなどの準備が一通り完了している。

この記事では、これらをどのように実現したのかをご紹介します。非常に長い記事になってしまいましたので、休み休み読んで頂ければ幸いです。

(2020年8月21日 12時30分追記:当初、Azure上でWindows 10を使う場合の選択肢にWindows 10の買い切り型ボリュームライセンスを挙げていましたが、こちらは事実誤認でした。関連する記述を訂正しました。)

移行の動機

実際にやったことの解説の前に、そもそも何故移行をしたかったのかを簡単に説明します。

元々当社では長年、FirefoxとThunderbirdのサポート業務において、社内に設置されているデスクトップPCをホストマシンとしたVMや、物理的なWindows PCを検証環境として使っていました1。この検証環境は、セットアップ手順が自動化されていないことから、一度作った環境をずっと使い続ける運用になってしまい、その結果以下のような問題が起こっていました。

  • 前回検証時の影響が次回検証時に想定外の影響を及ぼしてしまう。2

  • 1台の物理PCでは複数のお客さま向けの検証を並行して行えない。3

それでもこの運用が継続していた最大の理由は、検証環境をガッツリ使用する機会が、Firefox ESR4やThunderbirdのメジャーアップデートのタイミングに集中していたからです。その時期が過ぎると、検証環境は散発するお客さまからのお問い合わせを承けてのタイミングにしか使われないため、「まあ、1年の中でごく限られた時期くらいのことだし……」と見過ごされてきていました。

しかし、コロナウィルス感染拡大防止のためにリモート勤務が主体になった結果、この「1年に1回の限られた時期」すらも乗り切れない恐れが出てきました。前述の問題に加え、さらに以下の問題が表面化してきたからです。

  • VMホストをメンテナンスしにくい。5

  • インターネット越しに社内ホストのVMを操作するときの操作性が劣悪である。6

  • 物理PCをそんなにたくさん作業者の自宅に持ち込めない。

冒頭に挙げた「クラウドで」「使い捨てで」「起動時点で検証の準備が整った」検証環境が欲しいというニーズは、このような背景から生じたものです。

費用の見積もり

環境のセットアップをソフトウェアで完結できて便利なのは間違いありませんが、条件次第ではクラウドは割高になるともいいます。クラウドを使う場合と、物理PCあるいはレンタルサーバーでVMホストを調達する場合、どちらがコスト的に有利なのでしょうか。

当社のFirefox/Thunderbirdサポートのお客さまの多くは、現在はWindows 10をクライアントとして使われています。よって、「Windows 10を使う」ということを前提に費用を検討してみることにします。

クラウドの場合

ライセンス費用

結論を先に述べると、クラウドで検証目的のために手軽にWindwos 10を使うには、「Azureで、Windows 10 Enterprise E3/E5/A3/A5のボリュームライセンス、もしくはVisual Studioサブスクリプションを買う」ことになります。以下、公式のドキュメントであるWindows 10 on クラウドのライセンス認証についてWindows 10 サブスクリプションのライセンス認証から得られる情報に基づいて、詳しく述べていきます。

まず「Azureで」という点についてですが、これは以下の理由から、「クラウドで手軽に」という条件を満たせる選択肢が実質的にAzureのみとなるためです。

  • Azure以外のクラウドで使う場合、技術的な条件ではなく利用許諾の条件として、そのクラウドがQMTHの認証を承けている必要がある。

  • IIJdocomoKDDIなどがQMTH認証を得ているが、AWSやGoogle CloudはQMTH認証を得ていない。

  • 後述するTerraformの対応サービス一覧には、これらのQMTH認証クラウドが載っていない7

また、「Windows 10をインストールして動かす権利」と「AzureでWindows 10を動かす権利」も別になっていて、前者だけあっても後者がないとAzure上ではWindows 10を使えません。後者の権利は、Windows 10 Enterprise E3/E5のボリュームライセンスの一部として含まれるほか、Visual Studioサブスクリプションの特典に含まれている8ため、このいずれかを購入する必要があるということになります。

ボリュームライセンスとは、Windowsのライセンスを3つ以上まとめて購入する場合に取れる選択肢です。マシン1台あたりにすると1~2万円の買い切りになるようです。Windows 10をクラウド上で使える権利が含まれるライセンスは、買い切りタイプの一般的なWindows 10ライセンスではなく、サブスクリプション形式のWindows 10 Enterprise E3/E5のライセンスになるそうで、Windows 10 Enterprise E3の場合は月額760円、年間で1万円弱になるようです。検証環境の数だけライセンスを購入すると、このN倍が毎年の固定費増となります。

VSサブスクリプションは、開発者・検証作業者向けの選択肢です。VSサブスクリプションの「標準サブスクリプション」プランには検証用としてMicrosoft製品のライセンスが付属していて、本稿執筆時点のレートではVSサブスクリプション1ユーザーにつき初年度約12~3万円9、2年目以降は8万5千円10ほどで、使うか使わないか・使用頻度に関わらず毎年の固定費増となります。検証用に使えるWindows 10のライセンス数は限りが無いので、検証環境の数が多い場合はこちらの方が割安になります。

ライセンス費用以外の費用

Azureの場合、費用はVMが動作している時間に対してだけでなく、仮にVMが終了していても、ストレージ領域を消費していれば、VMの状態に関わらずその容量に応じて費用が発生します。

当社では他の事業ですでにAzureを使用しており、重たい処理を長時間実行させるようなハードな使い方をして、月あたり1案件で1~2万円程度に収まっているという実績がありました。Firefox/Thunderbirdサポートの検証では、そのようなハードな使い方はまずしませんし、そもそも冒頭に述べたニーズのとおりの「検証するときだけ作成して使い捨てにする」運用であれば、費用はもっと下がります。

また、先述のVSサブスクリプションにはAzureのクレジットが毎月6千円分付与されるという特典があります11。VSサブスクリプションとAzureの組み合わせで使用した場合、当社のFirefox/Thunderbirdサポートでの使い方であれば、Azureの使用自体にかかるコストは、この特典のクレジット分で十分にまかなえると見込まれます(実際、検証用のVMを1つ丸1日使用してもコストは100円程度でした)。

クラウドでない場合(物理ホストまたはレンタルサーバー)

ライセンス費用

Windows 10プリインストールのPCを調達する場合、Windows 10のライセンスはハードウェア購入費の中に含まれる形になります。

VMホストを調達してそこにWindows 10のVMを立てる場合や、既存のハードウェアを流用してWindows 10をインストールする場合は、Windows 10のライセンスの調達の必要があります。この場合、Windows 10のライセンス購入やVSサブスクリプション契約の費用がかかります。

ただし、Azureの場合と異なり、こちらのケースではサブスクリプション形式のWindows 10 Enterprise E3/E5は必要なく、買い切りのライセンスが使えます。Windows 10 Professionalの場合、定価は3万円弱ですが、ボリュームライセンスで購入するともう少し安くなるのではないかと予想されます。

また、VSサブスクリプションの2年目以降の契約も、こちらのケースでは必須ではありません。VSサブスクリプションの特典で付与されたライセンスは契約終了後も永続的に使えるため、初年度のみ契約して2年目以降は継続しないというやり方を取ることができます。

ライセンス費用以外の費用

PCを購入するのであればその代金が、レンタルサーバーを借りるのであればその費用が必要です。これらはすべて、使うか使わないかに関わらず発生する固定費となります。

また、「そうして調達したPCやレンタルサーバー自体の運用」のコストも発生します。作業者目線では「自分が作業すればいいだけじゃないか」と感じるところですが、「工数単価の高い技術者に対して、検証環境の運用のためだけに拘束される時間が発生し、他の作業に割り当てられたはずの時間が圧迫される」のは、会社的にとっては損失になるということです。

どうするのが一番得か?

考慮するべき要素はいくつかあります。例えば以下の要領です。

  • リモート勤務はどのくらい継続するのか? リモート勤務が長期的に継続するのであれば、オフィスにいないと使えない物理ホストでは不便なので、割高であったとしてもクラウドを選択した方がよい。

  • その環境をどのくらい継続的に使うのか? 長く使うのであれば、ハードウェアを買って使い倒すのが最も割安と言える。検証対象が頻繁に変わるのであれば、柔軟性が高いクラウドの方が適している。

  • 作業者の工数単価はどのくらいか? 物理ホストやレンタルサーバーの運用に要する人的コストと、その人を何らかの案件にアサインして得られる収益を比較して、前者が上回るのであればクラウドを、後者が上回るのであれば物理ホストを選択するのが適している。

  • 検証対象の環境の種類は均一か? 検証環境の種類が多い場合、資産管理が煩雑になるため、「検証環境を作ってすぐ使い捨てにする」運用を取りやすいクラウドが適している。

  • 検証環境の数はどのくらいか? 検証環境の数が非常に多くなる場合、ボリュームライセンス購入よりVSサブスクリプションの方が割安となる。

諸々の条件を鑑みて、当社では「VSサブスクリプションを購入してAzureを使う」ことにしました。

使い捨ての検証環境の作り方

Terraform(テラフォーム)というツールを使うと、静的な設定ファイルに基づいてAzure上に所定の設定でVMや仮想ネットワークを構築・破棄できます。このブログでも過去に全3回にわたってTerraformの基本的な使い方の解説記事を掲載しました(1, 2, 3)。実のところ、今回の検証環境の移行はこれらの記事が発端となってのことでした。

当社で使用している検証環境設定用のレシピのテンプレート

これらの手順を元に作成した、当社のFirefox/Thunderbirdサポート業務での検証環境として必要な諸々の初期設定を行うように調整した main.tf および関連ファイル一式を公開しています本稿執筆時点のリビジョン)。 こちらはLinux環境での使用を前提としていて12、基本的な使い方は以下の要領です。

  1. このブログのTerraformに関する連載のその1に従って、必要なコマンドをインストールして認証情報を取得し、~/.bashrc などに以下の内容を追加しておく、
    export SUBSCRIPTION_ID="********-****-****-*****************"
    export ARM_SUBSCRIPTION_ID="$SUBSCRIPTION_ID"
    export ARM_CLIENT_ID="********-****-****-*****************"
    export ARM_CLIENT_SECRET="**********************************"
    export ARM_TENANT_ID="********-****-****-*****************"
    
  2. pwgenコマンドをインストールする。(Ubuntuであればsudo apt install pwgenを実行)
  3. これらのファイルがあるディレクトリに cd して makemake apply )を実行する。
    • もしAnsibleがエラーで実行完了できなかった場合は、make apply-playbook でAnsibleのみ再実行する。
  4. rdp/add_password.bat の位置に出力されたバッチファイルをWindows上で実行し、当該ディレクトリに保存されているRDPファイル13に認証情報を付与する。
  5. 管理者権限を持たない一般ユーザー(rdp/ユーザー.rdp)または管理者ユーザー(rdp/管理者.rdp)としてログインし、検証を実施する。
    • 管理者パスワードが必要になった場合、password.txt または terraform.tfvars の中に出力されているものを使う。
  6. 検証が完了したら、make destroy を実行して検証環境を破棄する。
    • もしterraformがエラーで実行完了できなかった場合は、リソースグループ等がAzure上に残留しているため、Azureのダッシュボードから手作業で削除する。また、make clean でローカルの不要なファイルも削除する。

筆者が実行したときは、make の実行からVMが利用可能な状態になるまでに約10分を要しました。後述の日本語化を行う場合は、日本語パックのインストールに10分ほど時間がかかるため、make の実行からVMが利用可能になるまでの待ち時間は約20分強となります。このくらいの待ち時間であれば、「必要なときに起動して、用が済んだら破棄」というカジュアルな使い方も、無理なくできるのではないでしょうか。

ここからは、このテンプレートをどのようにして作成したかを順を追って解説してみます。

なお、ここからの内容は以下の3つの記事の内容を履修済みという想定で記述します。TerraformもAzureも初めて使うという方は、まずチュートリアルとして、これらを完遂する所まで実践しておいて下さい。

VMイメージの選択

過去記事の例ではWindows Serverを使用していますが、今回使いたいのはデスクトップPC向けのWindows 10です。

Terraformの設定に記述するイメージ名は、「Windows 10 2004」のような書き方はできず、内部的な名前で指定しないといけません。どんな値を設定すればよいかは、az コマンドでイメージの一覧を取得して調べる必要があります。以下は、デスクトップPC向けのWindowsのVMイメージの詳細な一覧をJSON形式で取得する例です。VMイメージ一覧の問い合わせには時間がかかるため、結果はローカルに保存しておくのがお薦めです。

$ az vm image list --publisher MicrosoftWindowsDesktop --all | jq . > vm-list-windows-all.json

Windows 10のイメージは、この結果の中に以下のような形で出現します。

  {
    "offer": "Windows-10",
    "publisher": "MicrosoftWindowsDesktop",
    "sku": "19h1-ent",
    "urn": "MicrosoftWindowsDesktop:Windows-10:19h1-ent:18362.959.2007101755",
    "version": "18362.959.2007101755"
  },

Windowsのバージョンとエディションは、この中の sku 14の値で区別します。先頭部分はコードネームの短縮形、その後にエディションや補足情報が続いており、以下のような意味になっています。

skuの値の例 コードネーム / Windowsのバージョン エディション 補足
rs1-enterprisen-g2 Redstone 1 (RS1) / Windows 10 1607 Enterprise N(Media Player削除版)、第2世代仮想マシン版
rs4-pro-zh-cn Redstone 4 (RS4) / Windows 10 1803 Professional 中国語版
rs5-enterprise-g2 Redstone 5 (RS5) / Windows 10 1809 Enterprise 第2世代仮想マシン版
19h1-ent 19H1 / Windows 10 1903 Enterprise
19h2-pro 19H2 / Windows 10 1909 Professional
20h1-evd 20H1 / Windows 10 2004 Enterprise マルチセッション(旧称:Enterprise for Virtual Desktops)

先のテンプレートでは、sku の初期状態は 19h1-pro 、つまり「Windows 10 1903 Professional」と指定しています。

なお、「起動時点で検証の準備が整った」検証環境を用意するという観点では、あらかじめ検証環境として準備を整えたVMのイメージをスナップショットとして取っておく方法もあります。ただ、今回の用途の前提では、

  • スナップショットがあっても、仮想ネットワークなどはその都度用意する必要がある。

  • スナップショットを保存するためのストレージ領域の保持に費用が発生する。検証を行っていない時間の方が圧倒的に長いと、ほとんどの費用が無駄になる。

  • 顧客ごとにWindowsのバージョンや設定を変える必要があり、せっかくイメージを作成してもあまり省力化にならない。

といった具合であまり旨味がないことから、独自のイメージは作らないことにしました。

自動シャットダウンの設定(2022年1月14日追記)

この記事で想定しているVMは使い捨て用なので、検証が終わったら削除する前提です。 しかしながら、何らかの手違いでVMを削除し忘れてしまった場合、そのままでは起動時間ベースでクレジットが消費されてしまいます。 (実際、この記事で紹介している例の構成のVMを約20日間放置してしまった結果、筆者は約4000円のクレジット消費を発生させてしまいました……)

AzureのVMには、指定時刻に自動でシャットダウンする機能があり、業務時間外に残留しているVMを自動終了するようにしておくと、VMを削除し忘れた場合の金銭的なダメージを軽減することができます。 Terraformのazurermでは、azurerm_dev_test_global_vm_shutdown_scheduleという機能でこの設定も自動化できます。 以下はその具体的な記述例です。

resource "azurerm_dev_test_global_vm_shutdown_schedule" "firefoxverify" {
  virtual_machine_id    = azurerm_virtual_machine.firefoxverify_vm.id
  location              = azurerm_resource_group.firefoxverify.location
  enabled               = true
  # 時刻の指定。「2200」は「22:00」で、午後10時を指す。
  daily_recurrence_time = "2200"
  # 時刻のタイムゾーン。日本時刻で指定する場合は「Tokyo Standard Time」。
  timezone              = "Tokyo Standard Time"
  # 自動シャットダウン前の通知設定。ここでは、黙って終了するように指定している。
  notification_settings {
    enabled = false
  }
}

検証環境内の設定

TerraformはAzure上にVMを用意するだけでなく、そのVM上でのPowerShellスクリプトの実行など、ある程度の環境設定も行えます。しかし筆者はPowerShellに明るくないため、PowerShellだけで必要なことすべてを実現するのは難しそうです。幸い、先のTerraform導入解説記事のその3ではAnsibleを使える状態にする所までの手順が解説されていますので、以後の検証環境内の設定はAnsibleで行うことにします。

先の記事の通りに作業を進めた段階では、main.tf の最後にはAnsibleでの接続用のインベントリを出力する local_file の定義があるはずです。その直後に、以下のようにAnsibleのPlaybookを作成する記述を追加します。

resource "local_file" "playbook" {
  filename = "ansible/playbook.yml"
  content  = <<EOL
- hosts: windows
  tasks:
    # ここに環境設定のための指定を書いていく
EOL
}

この状態で terraform apply を実行すると ansible/playbook.yml の位置にファイルが出力されます。それに続けて ansible-playbook -i ansible/hosts ansible/playbook.yml と実行すれば、できあがったばかりのVMをAnsibleで設定できるという具合です。

ということで、ここから先はAnsibleのPlaybookの書き方の話となります。

UACの無効化

Windowsでは、管理者権限の確認を求められたとき、画面が暗くなって認証のダイアログが表示されます。この「画面が暗くなる」という部分がUACと呼ばれる機能で、攻撃者によるダイアログの偽装や内容の読み取りなどを防ぐ効果があるのですが、副作用として、パスワード入力欄へのコピー&ペーストができなくなるという制限が生じます。

通常運用であればUACは有効にしておくことが推奨されますが、検証環境ではそこまでの安全性は必要ありません。また、インストーラを何度も実行するような検証ではその都度認証を求められるため、パスワードをコピー&ペーストできないと恐ろしく効率が悪いです。そのため、セットアップ時点でUACを無効化しておくことにします。

UACは、Windowsのレジストリの HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System 配下にある PromptOnSecureDesktop の値を DWORD 型で 0 に設定すれば無効化できます。これは、Ansibleでは win_regedit ディレクティブで実現できます。main.tf 内のAnsibleのPlaybook定義の tasks: に以下のようにタスクを追記します。

  tasks:
    (略)
    - name: Allow copy and paste to the UAC dialog
      win_regedit:
        key: HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System
        value: PromptOnSecureDesktop
        data: 00000000
        datatype: dword

日本語のワークグループ名にする

過去のサポート事例の中で、Windowsのワークグループ名に日本語が含まれている場合にのみ起こるトラブルがありました。そのような状況を想定し、検証環境も日本語のワークグループ名にしておくことにします。ワークグループ名の変更は、Ansibleでは win_domain_membership ディレクティブで行えます。

  tasks:
    (略)
    - name: Set non-ASCII workgroup name
      win_domain_membership:
        domain_admin_user: "${var.windows-username}"
        domain_admin_password: "${var.windows-password}"
        state: workgroup
        workgroup_name: ワークグループ

VMを日本語化する

AzureのVMイメージからセットアップした環境は、表示言語は英語、使用地は米国地域になっています。メニュー表記や時刻、キー配列などの諸々の「日本語化」を、Ansibleで行います。

表示言語を日本語化するためには、日本語のメッセージ定義などを含んだいわゆる「言語パック」が必要となります。言語パックはインターネット経由でそれ単体をダウンロードできるようにはなっていないので、まず以下のようにしてファイルを用意します。

  1. VSサブスクリプションのダウンロードセンターで「Windows 10 Language Pack, version (VMのWindowsのバージョン)」を検索する。

  2. DVDイメージをダウンロードする。

  3. ダウンロードしたイメージを仮想ディスクとしてマウントする。

  4. ディスク内から、Microsoft-Windows-Client-Language-Pack_x64_ja-jp.cab のように、言語名「ja-jp」を含んでいて拡張子が「.cab」である名前のファイルを探す。

  5. 見つかったファイルを、AzureのホストからHTTPでダウンロード可能な状態にする。例えば、自社運用のownCloudに置いてダウンロード用のURLを発行する。

  6. ダウンロード用のURLを変数にバインドできるように、variable.tf に変数の定義を追加する。

    variable "windows-language-pack-url" {}
    
  7. terraform.tfvars で以下のようにして、5のURLを変数にバインドする。

    windows-language-pack-url = "https://owncloud.example.com/index.php/s/***************/download"
    

これで準備が整いました。後は、win_file でダウンロード先ディレクトリを作成して、win_get_url で言語パックのファイルをダウンロードし、win_shell のPowerShellスクリプトで言語パックをインストールして、win_reboot でVMを再起動するようにすればOKです。

  tasks:
    (略)
    - name: Prepare directory to download language pack
      win_file:
        path: C:\temp
        state: directory
    - name: Download language pack file
      when: not "${var.windows-language-pack-url}" == ""
      win_get_url:
        url: "${var.windows-language-pack-url}"
        dest: 'c:\temp\lp.cab'
    - name: Install language pack
      when: not "${var.windows-language-pack-url}" == ""
      win_shell: |
        $LpTemp = "c:\temp\lp.cab"
        Add-WindowsPackage -PackagePath $LpTemp -Online
        Set-WinUserLanguageList -LanguageList ja-JP,en-US -Force
        Set-WinDefaultInputMethodOverride -InputTip "0411:00000411"
        Set-WinLanguageBarOption -UseLegacySwitchMode -UseLegacyLanguageBar
        Remove-Item $LpTemp -Force
    - win_reboot:
      when: not "${var.windows-language-pack-url}" == ""

各タスクに when: not "${var.windows-language-pack-url}" == "" と条件を指定しておくと、言語パックのURLが指定されていれば日本語化し、そうでなければタスクをスキップすることができます。言語パックのインストールだけで10分ほど時間がかかるので、必要がないのれあれば日本語化しないままで検証してもよいでしょう。

地域設定の変更は、タイムゾーンを変更する win_timezone 、地域を変更する win_region 、任意のPowerShellスクリプトを実行する win_shell 、Windowsを再起動する win_reboot の各ディレクティブを組み合わせて以下のように行います。

  tasks:
    (略)
    - win_timezone:
        timezone: Tokyo Standard Time
    - name: Set location
      win_shell: Set-WinHomeLocation -GeoId 0x7A
    - name: Set UI language
      win_shell: Set-WinUILanguageOverride -Language ja-JP
    - name: Set system language
      win_shell: Set-WinSystemLocale -SystemLocale ja-JP
    - name: Set date/time format
      win_shell: Set-WinCultureFromLanguageListOptOut -OptOut $False
    - name: Set keyboard layout
      win_shell: Set-ItemProperty 'registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\i8042prt\Parameters' -Name 'LayerDriver JPN' -Value 'kbd106.dll'
    - win_reboot:
    - name: Set region globally
      win_region:
        copy_settings: yes
        location: "122"
        format: ja-JP
        unicode_language: ja-JP
    - win_reboot:

地域設定の変更は、言語パックをインストールしていなくても行えます。

管理者用のアカウントと一般用のアカウントを、日本語の名前で作成する

過去のサポート事例の中で、Windowsのユーザー名に日本語が含まれている場合にのみ起こるトラブルがありました15。そのような状況を想定し、検証環境も管理者と一般ユーザーの両方で日本語のユーザー名のアカウントを作成することにします。これは、Ansibleでは win_user ディレクティブで以下のように行えます。

  tasks:
    (略)
    - name: Create administrator user
      win_user:
        name: "管理者"
        password: "${var.windows-password}"
        password_never_expires: true
        state: present
        groups:
          - Administrators
          - Users
          - Remote Desktop Users
    - name: Create regular user
      win_user:
        name: "ユーザー"
        password: "${var.windows-password}"
        password_never_expires: true
        state: present
        groups:
          - Users
          - Remote Desktop Users

あくまで検証用ということで、パスワードはどちらもVM作成時の物を流用しています。また、パスワードの有効期限も無効化しています。

管理者か一般ユーザーかの違いは、ユーザーがどのグループに所属しているかで変わります。group: での所属グループの指定に Administrators が列挙されていれば管理者、そうでなければ一般ユーザーとなります。また、検証をリモートデスクトップ接続で行えるようにするため、ここではそれぞれを Remote Desktop Users グループに所属させて、リモートデスクトップを使える状態にしています。

各ユーザーでリモートデスクトップ接続するためのrdpファイルを自動生成する

Azureのポータルからは作成したVMに接続するためのrdpファイル(リモートデスクトップ接続クライアント用の設定ファイル)をダウンロードできますが、このときのログオンユーザーはTerraformの操作用に作成したユーザーとなります。Ansibleで作成したユーザーでログインしたい場合は、明示的にユーザーを切り替えなくてはなりません。

毎回手動でログインユーザーを切り替えるのは煩雑なので、「管理者」ユーザーと「ユーザー」ユーザーでログオンするよう設定したrdpファイルも自動生成するようにしましょう。rdpファイルはプレーンテキストに所定の内容を出力すればそれで機能するので、main.tfの末尾に以下のように定義を追加します。

resource "local_file" "admin_rdp_shortcut" {
  filename = "rdp/管理者.rdp"
  content  = <<EOL
full address:s:${data.azurerm_public_ip.firefoxverify.ip_address}:3389
prompt for credentials:i:0
administrative session:i:1
username:s:管理者
EOL
}

resource "local_file" "user_rdp_shortcut" {
  filename = "rdp/ユーザー.rdp"
  content  = <<EOL
full address:s:${data.azurerm_public_ip.firefoxverify.ip_address}:3389
prompt for credentials:i:0
administrative session:i:1
username:s:ユーザー
EOL
}

このとき気をつけないといけないのが、password:s:(平文パスワード)のように書いてもパスワード入力は省略できないという点です。セキュリティの都合上、rdpファイルに含めるパスワード情報は暗号化する必要があります。

一連の暗号化にはPowerShellを使わないといけませんが、Terraformで「VM上ではなく、Terraformを実行しているローカル環境の上で」PowerShellスクリプトを動かす方法を、筆者は見つけられませんでした16。幸い、筆者はWindows 10のWSLでTerraformを実行しているので、Windows用のバッチファイルを出力させてそれを手動で実行する、という形で解決するようにしてみました。

resource "local_file" "batch_to_add_password_lines_for_rdp_shortcuts" {
  filename = "rdp/add_password.bat"
  content  = <<EOL
@echo off
for /f "usebackq delims==" %%i IN (`dir *.rdp /b`) do powershell.exe -command "'password 51:b:' + (('${var.windows-password}' | ConvertTo-SecureString -AsPlainText -Force) | ConvertFrom-SecureString);" >> %%i
del add_password.bat
EOL
}

この指定によって生成されるバッチファイルを実行すると、rdpファイルの末尾に暗号化されたパスワードの情報が追記され、バッチファイル自体は用済みという事で消去されます。

各ユーザーのログオン後の初期状態を設定する

検証ではユーザープロファイルや設定ファイルを取り扱う場面がありますが、そういう場面ではWindowsの初期設定の「隠しファイルや拡張子は表示しない」という動作が煩わしいものです。検証環境のセットアップの一環として、最初からこれらの設定を「表示する」に変更しておきます。

これはWindowsのレジストリで HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced 配下の HiddenHideFileExt を設定すればよいのですが、ここで1つ問題があります。先に紹介した win_regedit ディレクティブを使うと、HKCU がAnsible実行時のユーザーの物になってしまって、先ほど用意したユーザーの設定を変えられないのです。

この問題を回避するためには、become_userwin_command を併用します。前者はいわゆる runas 17を行うためのディレクティブで、後者は任意のコマンド列を実行するディレクティブです。まず、runas できるようにAnsibleのPlaybookの冒頭に以下の通り記述を追加します。

- hosts: windows
  become_method: runas                                 # ←追加
  vars:                                                # ←追加
    ansible_become_password: "${var.windows-password}" # ←追加
  tasks:
    (略)

次に、実際にレジストリの値を設定するタスクを以下のように追加します。

  tasks:
    (略)
    - name: Show hidden files for the administrator user
      become: yes
      become_user: "管理者"
      win_command: reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v Hidden /t REG_DWORD /d 1 /f
    - name: Show file extensions for the administrator user
      become: yes
      become_user: "管理者"
      win_command: reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v HideFileExt /t REG_DWORD /d 0 /f
    - name: Show hidden files for the regular user
      become: yes
      become_user: "ユーザー"
      win_command: reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v Hidden /t REG_DWORD /d 1 /f
    - name: Show file extensions for the regular user
      become: yes
      become_user: "ユーザー"
      win_command: reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v HideFileExt /t REG_DWORD /d 0 /f

become: yesbecome_user: "コマンドを実行するユーザー名"win_command と併せて指定することで、そのコマンドが指定のユーザーで実行されるようになります。これによって、各ユーザーの HKCU 配下のレジストリ値を変更できる訳です。

この方法で、各ユーザーの言語設定も変更できます。以下のように、PowerShell を使って日本語に切り替えておきます。

  tasks:
    (略)
    - name: Set display language for the administrator user
      become: yes
      become_user: "管理者"
      win_shell: |
        Set-WinUILanguageOverride -Language ja-JP
        Set-WinDefaultInputMethodOverride -InputTip "0411:00000411"
    - name: Set display language for the regular user
      become: yes
      become_user: "ユーザー"
      win_shell: |
        Set-WinUILanguageOverride -Language ja-JP
        Set-WinDefaultInputMethodOverride -InputTip "0411:00000411"

検証用のWindowsアプリをインストールする

検証環境は素のWindowsなので、テキスト編集は「メモ帳」や「ワードパッド」などを使う必要があります。しかし、編集対象のファイルの文字エンコーディングや改行コードによっては、これらを使えない場合があります。できれば、EmEditorのようにもう少し高機能なテキストエディタを使える状態にしておきたい所です。どうすればよいのでしょうか?

Linuxディストリビューションであれば aptdnf などのコマンドでアプリケーションをインストールできますが、今の所Windowsで標準的にそのようなことを行うコマンドはありません。ただ、有志の運営のパッケージリポジトリを使ってそれを可能にしようというプロジェクトはあり、Chocolateyもその1つです。

Ansibleでは win_chocolatey というディレクティブを使ってChocolateyリポジトリからWindowsアプリをインストールできます。EmEditorもこのリポジトリに掲載されていますので、これを使ってEmEditorをあらかじめインストール済みの状態にしておきます。

  tasks:
    (略)
    - name: Setup chocolatey
      win_chocolatey:
        name: chocolatey
        state: present
    - name: Install EmEditor
      win_chocolatey:
        name: emeditor
        state: present
        allow_empty_checksums: yes
        ignore_checksums: yes

ここでは win_chocolatey を2回指定していますが、1回目はChocolatey自体のインストール、2回目がEmEditorのインストールとなっています。

使いたいアプリケーションがScoopの方で利用可能なのであれば、win_chocolatey の代わりに win_scoop を使って下さい(こちらは未検証です)。

Ansibleのモジュールでインストールできないアプリのインストール

ChocolateyやScoopなどのリポジトリに収録されていないアプリは多数あります。そのようなアプリは、ファイルのダウンロードや展開などの操作をAnsibleのタスクとして自分で記述する必要があります。

例えば、Windowsアプリが認識するシステムの時刻をアプリ単位で変更する「HookDate」もその1つです。Azure上のVMのシステムの時刻はホストマシン(ユーザーからは見えない)の時刻に強制的に同期されてしまうため、時刻や日付が絡む検証では重宝します。これをインストールする指定は、ファイルをダウンロードする win_get_url と、ダウンロードしたzip形式のアーカイブを展開する win_unzip とを組み合わせて、以下の要領で行えます。

  tasks:
    (略)
    - name: Download HookDate to override system time for Firefox
      when: not "${var.hookdate-download-url}" == ""
      win_get_url:
        url: "${var.hookdate-download-url}"
        dest: 'C:\Users\Public\hookdate.zip'
    - name: Extract contents
      when: not "${var.hookdate-download-url}" == ""
      win_unzip:
        src: 'C:\Users\Public\hookdate.zip'
        dest: 'c:\Users\Public'
        delete_archive: yes

ここでは、言語パックの場合と同様に、ダウンロードするファイルのURLを hookdate-download-url という変数で指定するようにしています。ダウンロード可能な位置へのファイルの設置と、variable.tf での変数定義、terraform.tfvars でのURLのバインドを行っておくことで、検証で使う必要が無い場合はタスクをスキップするようにしました。

テストケースの用意

この記事で用意している検証環境は、Firefoxサポートでの導入支援やカスタマイズ結果の検証に使うことを意図した物なので、検証時には共通のテストケースを使う場面が多いです。そこで、テストケースもダウンロードしておいて使いやすい位置に展開することにします。これは、前項同様に win_get_url でのダウンロードと win_unzip で行えます。

  tasks:
    (略)
    - name: Download firefox-support-common for testcases
      win_get_url:
        url: "https://github.com/clear-code/firefox-support-common/archive/master.zip"
        dest: 'c:\Users\Public\firefox-support-common.zip'
    - name: Extract contents
      win_unzip:
        src: 'c:\Users\Public\firefox-support-common.zip'
        dest: 'c:\Users\Public'
        delete_archive: yes
    - name: Extract only testcases
      win_copy:
        src: 'c:\Users\Public\firefox-support-common-master\testcases'
        dest: 'c:\Users\Public'
        remote_src: True
    - name: Delete needless files
      win_file:
        path: 'c:\Users\Public\firefox-support-common-master'
        state: absent

ダウンロードしたファイルを展開しただけだと階層が深くなってしまうので、ここでは win_copy で必要な分のファイルだけを取り出し、不要になった空ディレクトリを win_file で削除しています。

また、検証手順の中に「システムにインポートされたルート証明書をFirefoxが自動認識できているかどうか」を確かめる項目があります。テストケースの中には、通常は未知の認証局によって署名された証明書であるとしてエラーになるuntrusted-root.badssl.comについて、認証局の証明書をエンタープライズの証明書として配布した状態にするためのregファイルが含まれています18。これは win_regmerge でインポート済みの状態にすることができます。

  tasks:
    (略)
    - name: Add enterprise cert for verification
      win_regmerge:
        path: 'C:\Users\Public\testcases\add-badssl-com-enterprise-root.reg'

よく参照するディレクトリへのショートカットの作成

検証においては、ユーザープロファイルを手動で削除して新規インストール直後の状態に戻したり、インストール先の構成ファイルを変更したりといった操作が度々発生します。そのような場面でよく開くことになるディレクトリへのショートカットを、作業時に使いやすいよう全ユーザー共通のデスクトップ上に作成しておくことにしました。これは win_shortcut で以下のようにして行えます。

  tasks:
    (略)
    - name: Create shortcut to Program Files
      win_shortcut:
        src: '%ProgramFiles%'
        dest: '%Public%\Desktop\Program Files.lnk'
    - name: Create shortcut to Program Files (x86)
      win_shortcut:
        src: '%ProgramFiles(x86)%'
        dest: '%Public%\Desktop\Program Files (x86).lnk'
    - name: Create shortcut to testcases
      win_shortcut:
        src: '%Public%\testcases'
        dest: '%Public%\Desktop\testcases.lnk'
    - name: Create shortcut to AppData
      win_shortcut:
        src: '%AppData%'
        dest: '%Public%\Desktop\AppData.lnk'
    - name: Create shortcut to LocalAppData
      win_shortcut:
        src: '%LocalAppData%'
        dest: '%Public%\Desktop\LocalAppData.lnk'

Hostsの設定

Firefoxのカスタマイズでは、特定のホストに対してサイト別設定をあらかじめ反映したり、セキュリティやプライバシー保護のための機能を特定のホストでだけ無効化したり、といった設定を行うことがあります。このカスタマイズの検証用として、本番環境と同じホスト名で実際にWebページを表示できる状態にしておくため、Hostsファイルを編集しておくことにします。これは win_hosts で行えます。

  tasks:
    (略)
    - name: Add popup blocker exception hosts for Security-4-5 and Security-4-6
      win_hosts:
        state: present
        canonical_name: hostname.example.com
        ip_address: '93.184.216.34'

なお、ここでホスト名を割り当てている 93.184.216.34 は、example.comの本稿執筆時点でのIPアドレスです。

まとめ

以上、サポート業務の上で必要となるWindows 10の検証環境について、クラウド上で使い捨てにする運用を取るために必要なコストの検討の例と、TerraformとAnsibleを使った「使い捨て前提の検証環境」の作成過程をご紹介しました。

クラウドやプロビジョニングツールは「開発環境の整備」や「運用中の状況の変化に合わせた迅速なスケールアウト」などを目的にWeb業界で使われる事が多いですが、「デスクトップアプリケーションの法人向け技術サポート」という分野でも、このように業務に役立てることができます。「なんとなく、今までずっとこうやってきたので、こうすると一番楽/安上がりだから……」で今となっては非効率的なやり方を継続してしまっていないか、やり方を改めるコストを過剰に高く見積もってしまっていないか、折に触れ再考してみてはいかがでしょうか?

  1. 創業当時は今のような便利なクラウドサービスがなく、その頃からの運用をそのまま継続してきていた。

  2. 検証の過程で実験的に行ったWindowsの設定変更の戻し忘れ、前回インストール時のレジストリ値が悪影響を及ぼす、など。

  3. FirefoxやThunderbirdのインストール先が既定の通りだと、設定ファイルが衝突してしまう。また、それを避けて別の位置にインストールしてしまうと、実際の使用環境との差異が無駄に増えてしまい、検証の妨げになる。

  4. 法人向けの長期サポート版。

  5. オフィスにいないので、ディスク容量が不足してもディスクの設置作業ができない、ホストの電源が落ちたり無反応になったりしてしまってもすぐに再起動できない、など。

  6. 事務所のインターネット回線が、一般家庭用と同程度のトラフィックを想定した契約内容のため、このような使い方ではボトルネックになってしまう。

  7. 調べればこの中にもQMTH認証クラウド事業者があるのかもしれないが、そこまで調べ始めると「手軽に」という前提から外れてくるため、今回は未調査。

  8. 「MSDN サブスクリプションをお持ちのお客様には、毎月 Azure の無料特典が提供されています。この無料特典で提供される Azure に限り、Windows 7/8/10 といったクライアントOSを開発・テスト目的でご利用いただける例外(*)があります。」との記述がある。「MSDNサブスクリプション」は「Visual Studioサブスクリプション」の旧称。

  9. 契約上は1199ドル。

  10. 契約上は799ドル。

  11. 次月へは持ち越せず、使わなかった分は消失する。

  12. Windows環境では、Git BashやWindowsネイティブ版のPythonやTerraformなどを組み合わせて実行しても期待通りの結果を得られないため、WSL上での実行をお薦めします(筆者はそうしています)。また、2023年1月現在、WSL2上でTerraformを実行するとDNSでの名前解決に失敗する場合があり、回避策として、/etc/wsl.confの手動での書き換えによる/etc/resolv.confの自動生成の停止や、設定の明記などの対応を取らなくてはならないようです

  13. リモートデスクトップ接続用のショートカット。

  14. SKU: Stock Keeping Unit、在庫管理単位。Windowsではエディション構成にあたる。

  15. ホーム以下のパスに日本語が含まれるようになるため。

  16. 調査が甘いだけの可能性はあるので、指摘をいただけると幸いです。

  17. Linux等での sudo に相当する。

  18. 訪問してもエラーにならないことをもって、ルート証明書のインポートに成功していると判断する。