【IaCで作るselenium環境】07_AzureVMをTerraformで構築する

直近の関連エントリ

――――――――――――――――――――――――――――――――――

こんにちは。SHIFTのスクラムマスター・テスト自動化エンジニアの石丸です。

前回、AzureVM上に構築したAndroidコンテナで実際にテストを実行しました。今回はVMの構築部分をTerraformに代替していきたいと思います。



■今回のテーマ

今回のテーマは「TerraformによるVM構築」です。

前回まででかなりテスト環境が整ってきましたが、それでもひとつ不満点があります。
VMを準備するのが色々面倒な点です。

・VM初期構築時にGUIでポチポチ準備しないとならない
・DNSとかポートの設定も、さらに追加で画面ポチポチしないとならない
・VMだけ消すとゴミが残る(DNS設定はリソースグループに残ったまま)

今回はこれらをTerraformで構成ファイル化し、コマンド1発(3発?)で構築までしてしまいましょう。

■前提

・Terraform Version 0.14.2(現時点でのScoopでの最新)
・Scoop
・Azure CLI Version 2.16.0(現時点での最新)
・windows環境での構築
・第6回までの内容が作業済みであること


■前回のおさらい

前回の最後はこんな感じでした。

今回の構成_06

この図の右側、「AzureVM」の部分が今回の作業対象です。


■Terraformって?

Ansibleではマシンの「中身」をプロビジョニングしました。
Terraformはマシンそのものをプロビジョニングします。

しかもTerraformでプロビジョニングできる環境は多岐にわたります。
AWS、GCP、そして今回使用するAzureも全てTerraformでプロビジョニング可能です。

第4回ではAzureのVM環境を構築するのに画面から設定をポチポチして作っていましたが、Terraformは設定ファイルに環境情報を書いてコマンドを叩くだけで構築可能です。


■Terraformの準備

まずは使えるようにセットアップです。
手順は全部で6つです。

手順1:Scoopのインストール

Terraformは一般的にはtfenvというバージョン管理ツールからインストールされることが多いのですが、バージョン管理の概念を手順にいれると話がややこしくなるため、今回はベタでインストールします。

インストールにはScoopを使用します。

パッケージマネージャとしては恐らくChocolateyが使われることが多いと思いますがScoopのほうが個人的に好みなので今回はこちらで進めようと思います。

公式サイトにある通り、インストールは簡単です。
PowerShellを開いて以下のコマンドを実行します。

> Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh')

Scoopを利用すると色々なものがコマンド一行で簡単にインストールできるようになります。

手順2:Terraformのインストール

PowerShellからScoopのsearchコマンドを実行します。

> scoop search terraform

'main' bucket:
   terraform-provider-ibm (1.17.0)
   terraform (0.14.2)

Version 0.14.2の利用が確認できました。
こちらをインストールします。

> scoop install terraform

(中略)

'terraform' (0.14.2) was installed successfully!

> terraform -v
Terraform v0.14.2

無事インストールできました。

手順3:Azure CLIのインストール

ローカルPCからコマンドでAzureを操作するツールをセットアップします。
Scoopからインストール可能です。

> scoop search azure-cli
'main' bucket:
   azure-cli (2.16.0)

> scoop install azure-cli

(中略)

'azure-cli' (2.16.0) was installed successfully!

> az -v
azure-cli                         2.16.0

core                              2.16.0
telemetry                          1.0.6

こちらも無事インストールができました。

手順4:サービスプリンシパルの発行

通常AzureVMを作る際は画面からログインします。
が、Terraformはコマンドから直接AzureにアクセスしてVMを構築するため、Terraform用の認証機能が必要になります。

ここで使用するのがサービスプリンシパルという特殊な情報です。
まずはこれを発行します。
サービスプリンシパルの作り方には数パターンあるのですが、今回は

・サブスクリプションID
・AppID
・パスワード
・テナントID

これらを使用して作成します。

==================================================【注意】
サービスプリンシパルは機微情報なので絶対に晒さないでください!
当エントリ内も重要情報は全て"000"でマスキングしています。
==================================================

まずサブスクリプションIDの確認をします。
ローカルにAzure-CLIを入れているのでazコマンドが使えるようになっています。まずログインします。 

> az login

するとブラウザで認証処理が始まります。
自身のアカウントを選択してログインしてください。

正常にログインすると以下のように出力されます。

[
 {
   "cloudName": "AzureCloud",
   "id": "00000000-0000-0000-0000-000000000000",
   "isDefault": true,
   "name": "無料試用版",
   "state": "Enabled",
   "tenantId": "00000000-0000-0000-0000-000000000000",
   "user": {
     "name": "user@example.com",
     "type": "user"
   }
 }
]

わかりづらいですが、上記の"id"がサブスクリプションIDです。
このサブスクリプションIDを使用し、サービスプリンシパルを発行します。

> az ad sp create-for-rbac --role="Contributor" --scopes="/subscriptions/サブスクリプションID"

以下のように発行されます。

Creating a role assignment under the scope of "/subscriptions/サブスクリプションID"
{
   "appId": "00000000-0000-0000-0000-000000000000",

(中略)

   "password": "0000-0000-0000-0000-000000000000",
   "tenant": "00000000-0000-0000-0000-000000000000"
}

これで必要な4つの情報が出そろいました。
これらをローカルPCの環境変数に入れておきます。 

$env:ARM_CLIENT_ID="上記のappID"
$env:ARM_CLIENT_SECRET="上記のpassword"
$env:ARM_SUBSCRIPTION_ID="サブスクリプションID"
$env:ARM_TENANT_ID="上記のtenant"

PowerShellで以下のコマンドから一覧に表示されていれば設定OKです。

>  Get-ChildItem env:
Name                           Value
----                           -----
ARM_CLIENT_ID                  00000000-0000-0000-0000-000000000000
ARM_CLIENT_SECRET              0000-0000-0000-0000-000000000000
ARM_SUBSCRIPTION_ID            00000000-0000-0000-0000-000000000000
ARM_TENANT_ID                  00000000-0000-0000-0000-000000000000

環境変数に入れることで、認証情報を隠蔽します。
絶対にtfファイル等に書かないようにしてください。


手順5:sshキーの作成

今回はAzurePortalで作成したときのように自動的にpemファイルは発行されないので、事前にsshキーを作成しておきます。

PowerShellでssh-keygenします。

> ssh-keygen -t rsa -b 4096

ちなみにTerraformからAzureに繋げる際は、現時点はRSAのみの利用となります。
実行すると.sshフォルダに公開鍵(id_rsa.pub)・秘密鍵(id_rsa)が作成されるので、公開鍵をtfファイル内に、秘密鍵をDockerfileに記載します。

記載方法はそれぞれ以下を参照ください。


手順6:Terraformファイル(tfファイル)の準備

Terraformファイルを準備していきます。

格納フォルダはここでは以下とします。
C:\tmp\terraform

selenoid_normal.tf

provider "azurerm" {
 version = "=2.40.0"
 features {}

}

# Azure 接続とリソース グループを作成する
resource "azurerm_resource_group" "tfrg" {
 name     = "tfrg"
 location = "eastus"

 tags = {
     environment = "Terraform"
 }
}

# 仮想ネットワークの作成
resource "azurerm_virtual_network" "tfrgVnet" {
 name                = "tfrgVnet"
 address_space       = ["10.0.0.0/16"]
 location            = azurerm_resource_group.tfrg.location
 resource_group_name = azurerm_resource_group.tfrg.name

 tags = {
     environment = "Terraform"
 }
}

# サブネットの設定
resource "azurerm_subnet" "tfsubnet" {
 name                 = "tfrgSubnet"
 resource_group_name  = azurerm_resource_group.tfrg.name
 virtual_network_name = azurerm_virtual_network.tfrgVnet.name
 address_prefix       = "10.0.3.0/24"
}

# パブリック IP アドレスの作成
resource "azurerm_public_ip" "tfpublicip01" {
 name                = "tfpublicip01"
 location            = "eastus"
 resource_group_name = azurerm_resource_group.tfrg.name
 allocation_method   = "Dynamic"
 domain_name_label   = "myselenoidvm001"

 tags = {
     environment = "Terraform"
 }
}

# ネットワーク セキュリティ グループの作成
resource "azurerm_network_security_group" "tfnsg01" {
 name                = "myNetworkSecurityGroup01"
 location            = "eastus"
 resource_group_name = azurerm_resource_group.tfrg.name

 security_rule {
   name                       = "SSH"
   priority                   = 1001
   direction                  = "Inbound"
   access                     = "Allow"
   protocol                   = "Tcp"
   source_port_range          = "*"
   destination_port_range     = "22"
   source_address_prefix      = "*"
   destination_address_prefix = "*"
 }

 security_rule {
   name                       = "Port_8083"
   priority                   = 1011
   direction                  = "Inbound"
   access                     = "Allow"
   protocol                   = "Tcp"
   source_port_range          = "*"
   destination_port_range     = "8083"
   source_address_prefix      = "*"
   destination_address_prefix = "*"
 }

 security_rule {
   name                       = "Port_5900"
   priority                   = 1021
   direction                  = "Inbound"
   access                     = "Allow"
   protocol                   = "Tcp"
   source_port_range          = "*"
   destination_port_range     = "5900"
   source_address_prefix      = "*"
   destination_address_prefix = "*"
 }

 tags = {
     environment = "Terraform"
 }
}

# 仮想ネットワーク インターフェイス カードの作成
resource "azurerm_network_interface" "tfnic01" {
 name                = "tfnic01"
 location            = azurerm_resource_group.tfrg.location
 resource_group_name = azurerm_resource_group.tfrg.name

 ip_configuration {
   name                          = "tfNicConfiguration"
   subnet_id                     = azurerm_subnet.tfsubnet.id
   private_ip_address_allocation = "Dynamic"
   public_ip_address_id          = azurerm_public_ip.tfpublicip01.id
 }

 tags = {
     environment = "Terraform"
 }
}

# セキュリティグループインターフェースの作成
resource "azurerm_network_interface_security_group_association" "example01" {
 network_interface_id      = azurerm_network_interface.tfnic01.id
 network_security_group_id = azurerm_network_security_group.tfnsg01.id
}

# VM
resource "azurerm_linux_virtual_machine" "tfvm01" {
 name                = "selenoidvmtf"
 resource_group_name = azurerm_resource_group.tfrg.name
 location            = azurerm_resource_group.tfrg.location
 size                  = "Standard_F2s"
 disable_password_authentication = true
 admin_username      = "azureuser"
 network_interface_ids = [
   azurerm_network_interface.tfnic01.id,
 ]

 admin_ssh_key {
   username   = "azureuser"
   public_key = file("~/.ssh/id_rsa.pub")
 }

 os_disk {
   caching              = "ReadWrite"
   storage_account_type = "Premium_LRS"
 }

 source_image_reference {
   publisher = "Canonical"
   offer     = "UbuntuServer"
   sku       = "18.04-LTS"
   version   = "latest"
 }

 tags = {
     environment = "Terraform"
 }
}

# shutdownスケジュール
resource "azurerm_dev_test_global_vm_shutdown_schedule" "tfvm01sd" {
 virtual_machine_id = azurerm_linux_virtual_machine.tfvm01.id
 location           = azurerm_resource_group.tfrg.location
 enabled            = true

 daily_recurrence_time = "1900"
 timezone              = "Pacific Standard Time"

 notification_settings {
   enabled         = false
 }
}

ポイントは「結構VM以外にも作る必要があるものが多い」という点です。

リソースグループ配下

第5回で作成したVMのリソースグループを見ると、VM(仮想マシン)以外にも複数リソースが作成されるのがわかります。
これらはVMを作成すると自動的に準備されるリソースです。

・パブリックIPアドレス
・ネットワークセキュリティグループ
・仮想ネットワークIFカード(画像では何故かチェコ語に。。)
・仮想ネットワーク
・SSHキー
・ディスク

このうちSSHキーは、今回は事前に作成したものをVMに転送するので不要です。(public_key = file("~/.ssh/id_rsa.pub")と記載の箇所)

はじめはややこしいと思いますので上記のtfファイルをそのままご利用ください。ただし「パブリック IP アドレスの作成」のセクションで
domain_name_label = "myselenoidvm001"
という記述がありますが、ここに関しては皆さんの作りたいDNS名に合わせて変更してください。


■Terraformの実行

tfファイルが整ったら実行していきます。
Terraformの実行には、大きく分けて以下の4手順があります。

・terraform init(初期化処理)
・terraform plan(tfファイルの仮実行)
・terraform apply(本実行。新規作成や更新を行う)
・terraform destroy(破棄)

これらを順番に行っていきます。

terraform init
以下の通り実行します。

> terraform init

Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/azurerm versions matching "~> 2.40.0"...
- Finding latest version of hashicorp/tls...
- Installing hashicorp/azurerm v2.40.0...
- Installed hashicorp/azurerm v2.40.0 (signed by HashiCorp)
- Installing hashicorp/tls v3.0.0...
- Installed hashicorp/tls v3.0.0 (signed by HashiCorp)

(中略)

Terraform has been successfully initialized!

Azureプロバイダ情報やプラグインの取得などを行います。

terraform plan
次にtfファイルの実行計画を確認を行います。
これを実行しても実際に構築はされません。あくまで計画のみ。

> terraform plan

(中略)

Terraform will perform the following actions:

 # azurerm_dev_test_global_vm_shutdown_schedule.tfvm01sd will be created
 + resource "azurerm_dev_test_global_vm_shutdown_schedule" "tfvm01sd" {
     + daily_recurrence_time = "1900"
     + enabled               = true
     + id                    = (known after apply)
     + location              = "eastus"
     + timezone              = "Pacific Standard Time"
     + virtual_machine_id    = (known after apply)

     + notification_settings {
         + enabled         = false
         + time_in_minutes = 30
       }
   }

(中略)

  # azurerm_virtual_network.tfrgVnet will be created
 + resource "azurerm_virtual_network" "tfrgVnet" {
     + address_space         = [
         + "10.0.0.0/16",
       ]
     + guid                  = (known after apply)
     + id                    = (known after apply)
     + location              = "eastus"
     + name                  = "tfrgVnet"
     + resource_group_name   = "tfrg"
     + subnet                = (known after apply)
     + tags                  = {
         + "environment" = "Terraform"
       }
     + vm_protection_enabled = false
   }

Plan: 9 to add, 0 to change, 0 to destroy.

リソースが追加になったらプラス、削除されたらマイナスされます。
正しく実行計画されたことが確認できました。

ちなみにこのタイミングでサービスプリンシパルの設定がされていないと

 Error building AzureRM Client: obtain subscription() from Azure CLI: Error parsing json result from the Azure CLI: Error waiting for the Azure CLI: exit status 1: Please run 'az login' to setup account.

と怒られてしまいます。必ず環境変数を設定しましょう。

terraform apply
では本実行します。
途中までplanと同じ出力がでますが、途中で以下のように確認がでます。

Do you want to perform these actions?
 Terraform will perform the actions described above.
 Only 'yes' will be accepted to approve.

 Enter a value:

実際に反映しますか?という確認です。
"yes"を入力します。

Apply complete! Resources: 9 added, 0 changed, 0 destroyed.

applyできました。
AzurePortalで確認します。

apply後
apply後2

tfファイルどおりのVMが構築できていますね。

terraform destroy
今回は使用しませんが、構築した環境を破棄する場合はこちらのコマンドを実行します。


■Ansibleプロビジョニング

TerraformでVMの準備ができましたが、まだ中身がからっぽです。

ここで、第5回で準備したAnsibleを少し修正して再度実行させます。
手順は6つです。

手順1:ansibleフォルダのDockerfileの修正

DNS名をmyselenoidvm001に、pemをid_rsaに変更します。

RUN set -x \
  && mkdir -p /etc/ansible/selenoid \
  && mkdir -p -m 0700 /root/.ssh/ \
  && ssh-keyscan myselenoidvm001.eastus.cloudapp.azure.com >> ~/.ssh/known_hosts

(中略)

RUN ssh-keygen -q  -N '' -t ed25519 -f  ~/.ssh/id_ed25519
RUN --mount=type=secret,id=ssh,target=/root/.ssh/id_rsa \
   cat ~/.ssh/id_ed25519.pub | ssh -i /root/.ssh/id_rsa azureuser@myselenoidvm001.eastus.cloudapp.azure.com 'cat >> .ssh/authorized_keys'
RUN ansible -i hosts myselenoidvm001.eastus.cloudapp.azure.com -m ping


手順2:ansibleフォルダのhostsの修正

DNS名をmyselenoidvm001に変更します

[selenoid]
myselenoidvm001.eastus.cloudapp.azure.com


手順3:ggrフォルダのtest.xmlの修正します。

今回は疎通確認だけしたいのでchromeのバージョン84のルーティング先をmyselenoidvm001だけとします。

<browser name="chrome" defaultVersion="84.0">
   <version number="84.0">
       <region name="1">
       <host name="myselenoidvm001.eastus.cloudapp.azure.com" port="8083" count="5" vnc="ws://myselenoidvm001.eastus.cloudapp.azure.com:8083/vnc" />
       </region>
   </version>
</browser>

記述が完了したら念のためGgrを再構築します。

> docker-compose down -v

(中略)

> docker-compose up -d --build


手順4:Ansibleイメージの再作成

修正が完了したらイメージを作り直します。
その際buildkitで渡すsrcはid_rsaに変更します。
またイメージ名は以前のものと変更しておきます(test02/ansible:0.0.1)。

> docker image build -t test02/ansible:0.0.1 --secret id=ssh,src=$HOME/.ssh/id_rsa .


手順5:Ansibleコンテナの実行

イメージができたらdocker run します。
その際こちらもコンテナ名を変えておきます(ansible_tf)。

> docker container run -itd --name ansible_tf test02/ansible:0.0.1

(中略)

PLAY RECAP *********************************************************************
myselenoidvm001.eastus.cloudapp.azure.com : ok=15   changed=10   unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

プロビジョニングも成功です。


手順6:テストの修正

第5回でAndroid用に修正したDemoTest.javaの記述を通常のchromeバージョン84に戻します。

browser.setCapability("version", "84.0");
driver = new RemoteWebDriver(new URL(
         "http://test:test-password@172.17.0.1:8083/wd/hub"
       ), browser);


■テスト再実行

第2回で準備したテストを再実行します。

> docker container start demo-test

(中略)

-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running com.aerokube.selenoid.DemoTest
Dec 20, 2020 1:46:04 PM org.openqa.selenium.remote.DesiredCapabilities chrome
INFO: Using `new ChromeOptions()` is preferred to `DesiredCapabilities.chrome()`
Dec 20, 2020 1:46:12 PM org.openqa.selenium.remote.ProtocolHandshake createSession
INFO: Detected dialect: W3C
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 12.668 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

(中略)

[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ demo-test ---
[INFO] Skipping execution of surefire because it has already been run for this configuration
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  02:19 min
[INFO] Finished at: 2020-12-20T13:46:39Z
[INFO] ------------------------------------------------------------------------

正常にテストが完了しました。
エビデンスの確認をします。

> scp -i ~/.ssh/id_rsa azureuser@myselenoidvm001.eastus.cloudapp.azure.com:/usr/local/selenoid/video/* c:\tmp\docker\mvn\

(中略)

a9269b9b5dda1f669433c946846b5792.mp4             100%  206KB 241.0KB/s   00:00

正しく実行できていることが確認できました。

今回作成された構成

今回の構成_07


■まとめ

実際の構築手順

1. Terraform準備
    1. Scoopのインストール
    2. Terraformのインストール(Scoop)
    3. Azure CLIのインストール(Scoop)
    4. サービスプリンシパルの発行
    5. sshキーの作成
    6. Terraformファイル(tfファイル)の準備
2. Terraform実行
    1. terraform init
    2. terraform plan
    3. terraform apply
3. Ansibleプロビジョニング
    1. ansibleフォルダのDockerfileの修正
    2. ansibleフォルダのhostsの修正
    3. ggrフォルダのtest.xmlの修正
    4. Ansibleイメージの再作成
    5. Ansibleコンテナの実行
    6. テストの修正
4. テスト再実行

==================================================
今回は長編になってしまいました。

今回の作業で確かにVMの準備は構成ファイル化することができましたが、正直手間が省けたかというと、特にAnsibleプロビジョニング部分での手間と時間がかなりかかっている印象が残るかなと思います。

さて、これをカイゼンするために次回はPackerでVMのImageを固めてしまって、簡単にVM構築できるようにしようと思います。

ではでは。

――――――――――――――――――――――――――――――――――

執筆者プロフィール:石丸圭
スクラムを中心にテストのアジリティーを高めるべく
日々仕事のリードタイム・プロセスタイムの圧縮に奮闘中。
3児のパパ(7歳4歳1歳)。

MUPうさぎクラス。
個人的なご相談はインスタDMにてどうぞー。
Instagram:@theboyalex

【ご案内】
テスト自動化のご相談は以下までお気軽にご連絡ください。
https://forms.office.com/Pages/ResponsePage.aspx?id=IkyjGtUOzUeqMMEbzjGdlSf__O4V1URMn-5BpGP8xd9UNE9ESkRPUEs1Wk9FM0REU1BXODFBSkI0MC4u

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/


みんなにも読んでほしいですか?

オススメした記事はフォロワーのタイムラインに表示されます!