09 Apr 2018, 15:00

俺のAzure CLI 2018春版

春の環境リフレッシュ祭り

最近KubernetesのCLI、kubectlを使う機会が多いのですが、なかなかイケてるんですよ。かゆい所に手が届く感じ。そこで、いい機会なのでAzure CLIまわりも最新の機能やツールで整えようか、というのが今回の動機。気づかないうちに、界隈が充実していた。

俺のおすすめ 3選

  • デフォルト設定
    • リソースグループやロケーション、出力形式などのデフォルト設定ができる
  • エイリアス
    • サブコマンドにエイリアスを付けられる
    • 引数付きの込み入った表現もできる
  • VS Code プラグイン
    • Azure CLI Toolsプラグイン でazコマンドの編集をコードアシストしてくれる
    • 編集画面上でコマンド選択して実行できる

デフォルト設定

$AZURE_CONFIG_DIR/configファイルで構成設定ができます。$AZURE_CONFIG_DIR の既定値は、Linux/macOS の場合$HOME/.azure、Windowsは%USERPROFILE%.azure。

Azure CLI 2.0 の構成

まず変えたいところは、コマンドの出力形式。デフォルトはJSON。わたしのお気持ちは、普段はTable形式、掘りたい時だけJSON。なのでデフォルトをtableに変えます。

[core]
output = table

そしてデフォルトのリソースグループを設定します。以前は「デフォルト設定すると、気づかないところで事故るから、やらない」という主義だったのですが、Kubernetesのdefault namespaceの扱いを見て「ああ、これもありかなぁ」と改宗したところ。

[defaults]
group = default-ejp-rg

他にもロケーションやストレージアカウントなどを設定できます。ロケーションはリソースグループの属性を継承させたい、もしくは明示したい場合が多いので、設定していません。

ということで、急ぎUbuntuの仮想マシンが欲しいぜという場合、az vm createコマンドの必須パラメーター、-gと-lを省略できるようになったので、さくっと以下のコマンドでできるようになりました。

デフォルト指定したリソースグループを、任意のロケーションに作ってある前提です。

az vm create -n yoursmplvm01 --image UbuntuLTS

エイリアス

$AZURE_CONFIG_DIR/aliasにエイリアスを書けます。

Azure CLI 2.0 のエイリアス拡張機能

前提はAzure CLI v2.0.28以降です。以下のコマンドでエイリアス拡張を導入できます。現時点ではプレビュー扱いなのでご注意を。

az extension add --name alias

ひとまずわたしは以下3カテゴリのエイリアスを登録しました。

頻繁に打つからできる限り短くしたい系

[ls]
command = list

[nw]
command = network

[pip]
command = public-ip

[fa]
command = functionapp

例えばデフォルトリソースグループでパブリックIP公開してるか確認したいな、と思った時は、az network public-ip listじゃなくて、こう打てます。

$ az nw pip ls
Name                  ResourceGroup    Location    Zones    AddressVersion    AllocationMethod      IdleTimeoutInMinutes
ProvisioningState
--------------------  ---------------  ----------  -------  ----------------  ------------------  ----------------------
-------------------
yoursmplvm01PublicIP  default-ejp-rg   japaneast            IPv4              Dynamic                                  4
Succeeded

クエリー打つのがめんどくさい系

VMに紐づいてるパブリックIPを確認したいときは、こんなエイリアス。

[get-vm-pip]
command = vm list-ip-addresses --query [0].virtualMachine.network.publicIpAddresses[0].ipAddress

実行すると。

$ az get-vm-pip -n yoursmplvm01
Result
-------------
52.185.133.68

引数を確認するのがめんどくさい系

リソースグループを消したくないけど、中身だけ消したいってケース、よくありますよね。そんなエイリアスも作りました。–template-uriで指定しているGistには、空っぽのAzure Resource Manager デプロイメントテンプレートが置いてあります。このuriをいちいち確認するのがめんどくさいので、エイリアスに。

[empty-rg]
command = group deployment create --mode Complete --template-uri https://gist.githubusercontent.com/ToruMakabe/28ad5177a6de525866027961aa33b1e7/raw/9b455bfc9608c637e1980d9286b7f77e76a5c74b/azuredeploy_empty.json

以下のコマンドを打つだけで、リソースグループの中身をバッサリ消せます。投げっぱなしでさっさとPC閉じて帰りたいときは –no-waitオプションを。

$ az empty-rg

位置引数Jinja2テンプレートを使ったエイリアスも作れるので、込み入ったブツを、という人は挑戦してみてください。

VS Code プラグイン (Azure CLI Tools )

Azure CLIのVS Code向けプラグインがあります。コードアシストと編集画面からの実行が2大機能。紹介ページのGifアニメを見るのが分かりやすいです。

Azure CLI Tools

プラグインを入れて、拡張子.azcliでファイルを作ればプラグインが効きます。長いコマンドを補完支援付きでコーディングしたい、スクリプトを各行実行して確認しながら作りたい、なんて場合におすすめです。

注意点

  • エイリアスには補完が効かない
    • bashでのCLI実行、VS Code Azure CLI Toolsともに、現時点(20184)でエイリアスには補完が効きません
  • ソースコード管理に不要なファイルを含めない
    • $AZURE_CONFIG_DIR/ 下には、aliasやconfigの他に、認証トークンやプロファイルといったシークレット情報が置かれます。なのでGitなどでソースコード管理する場合は、aliasとconfig以外は除外したほうがいいでしょう

06 Apr 2018, 18:00

TerraformでAzure VM/VMSSの最新のカスタムイメージを指定する方法

カスタムイメージではlatest指定できない

Azure Marketplaceで提供されているVM/VMSSのイメージは、latest指定により最新のイメージを取得できます。いっぽうでカスタムイメージの場合、同様の属性を管理していないので、できません。

ではVM/VMSSを作成するとき、どうやって最新のカスタムイメージ名を指定すればいいでしょうか。

  1. 最新のイメージ名を確認のうえ、手で指定する
  2. 自動化パイプラインで、イメージ作成とVM/VMSS作成ステップでイメージ名を共有する

2のケースは、JenkinsでPackerとTerraformを同じジョブで流すケースがわかりやすい。変数BUILD_NUMBERを共有すればいいですね。でもイメージに変更がなく、Terraformだけ流したい時、パイプラインを頭から流してイメージ作成をやり直すのは、無駄なわけです。

Terraformではイメージ名取得に正規表現とソートが可能

Terraformでは見出しの通り、捗る表現ができます。

イメージを取得するとき、name_regexでイメージ名を引っ張り、sort_descendingを指定すればOK。以下の例は、イメージ名をubuntu1604-xxxxというルールで作ると決めた場合の例です。イメージを作るたびに末尾をインクリメントしてください。ソートはイメージ名全体の文字列比較なので、末尾の番号の決めた桁は埋めること。

ということで降順で最上位、つまり最新のイメージ名を取得できます。

data "azurerm_image" "poc" {
  name_regex          = "ubuntu1604-[0-9]*"
  sort_descending     = true
  resource_group_name = "${var.managed_image_resource_group_name}"
}

あとはVM/VMSSリソース定義内で、取得したイメージのidを渡します。

  storage_profile_image_reference {
    id = "${data.azurerm_image.poc.id}"
  }

便利である。

30 Mar 2018, 16:30

Azure MarketplaceからMSI対応でセキュアなTerraform環境を整える

TerraformのプロビジョニングがMarketplaceから可能に

Terraform使ってますか。Azureのリソースプロビジョニングの基本はAzure Resource Manager Template Deployである、がわたしの持論ですが、Terraformを使う/併用する方がいいな、というケースは結構あります。使い分けはこの資料も参考に。

さて、先日Azure MarketplaceからTerraform入りの仮想マシンをプロビジョニングできるようになりました。Ubuntuに以下のアプリが導入、構成されます。

  • Terraform (latest)
  • Azure CLI 2.0
  • Managed Service Identity (MSI) VM Extension
  • Unzip
  • JQ
  • apt-transport-https

いろいろセットアップしてくれるのでしみじみ便利なのですが、ポイントはManaged Service Identity (MSI)です。

シークレットをコードにベタ書きする問題

MSIの何がうれしいいのでしょう。分かりやすい例を挙げると「GitHubにシークレットを書いたコードをpushする、お漏らし事案」を避ける仕組みです。もちそんそれだけではありませんが。

Azure リソースの管理対象サービス ID (MSI)

詳細の説明は公式ドキュメントに譲りますが、ざっくり説明すると

アプリに認証・認可用のシークレットを書かなくても、アプリの動く仮想マシン上にあるローカルエンドポイントにアクセスすると、Azureのサービスを使うためのトークンが得られるよ

です。

GitHub上に疑わしいシークレットがないかスキャンする取り組みもはじまっているのですが、できればお世話になりなくない。MSIを活用しましょう。

TerraformはMSIに対応している

TerraformでAzureのリソースをプロビジョニングするには、もちろん認証・認可が必要です。従来はサービスプリンシパルを作成し、そのIDやシークレットをTerraformの実行環境に配布していました。でも、できれば配布したくないですよね。実行環境を特定の仮想マシンに限定し、MSIを使えば、解決できます。

ところでMSIを使うには、ローカルエンドポイントにトークンを取りに行くよう、アプリを作らなければいけません。

Authenticating to Azure Resource Manager using Managed Service Identity

Terraformは対応済みです。環境変数 ARM_USE_MSI をtrueにしてTerraformを実行すればOK。

試してみよう

実は、すでに使い方を解説した公式ドキュメントがあります。

Azure Marketplace イメージを使用して管理対象サービス ID を使用する Terraform Linux 仮想マシンを作成する

手順は十分なのですが、理解を深めるための補足情報が、もうちょっと欲しいところです。なので補ってみましょう。

MarketplaceからTerraform入り仮想マシンを作る

まず、Marketplaceからのデプロイでどんな仮想マシンが作られたのか、気になります。デプロイに利用されたテンプレートをのぞいてみましょう。注目は以下3つのリソースです。抜き出します。

  • MSI VM拡張の導入
  • VMに対してリソースグループスコープでContributorロールを割り当て
  • スクリプト実行 VM拡張でTerraform関連のプロビジョニング
[snip]
        {
            "type": "Microsoft.Compute/virtualMachines/extensions",
            "name": "[concat(parameters('vmName'),'/MSILinuxExtension')]",
            "apiVersion": "2017-12-01",
            "location": "[parameters('location')]",
            "properties": {
                "publisher": "Microsoft.ManagedIdentity",
                "type": "ManagedIdentityExtensionForLinux",
                "typeHandlerVersion": "1.0",
                "autoUpgradeMinorVersion": true,
                "settings": {
                    "port": 50342
                },
                "protectedSettings": {}
            },
            "dependsOn": [
                "[concat('Microsoft.Compute/virtualMachines/', parameters('vmName'))]"
            ]
        },
        {
            "type": "Microsoft.Authorization/roleAssignments",
            "name": "[variables('resourceGuid')]",
            "apiVersion": "2017-09-01",
            "properties": {
                "roleDefinitionId": "[variables('contributor')]",
                "principalId": "[reference(concat(resourceId('Microsoft.Compute/virtualMachines/', parameters('vmName')),'/providers/Microsoft.ManagedIdentity/Identities/default'),'2015-08-31-PREVIEW').principalId]",
                "scope": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name)]"
            },
            "dependsOn": [
                "[resourceId('Microsoft.Compute/virtualMachines/extensions/', parameters('vmName'),'MSILinuxExtension')]"
            ]
        },
        {
            "type": "Microsoft.Compute/virtualMachines/extensions",
            "name": "[concat(parameters('vmName'),'/customscriptextension')]",
            "apiVersion": "2017-03-30",
            "location": "[parameters('location')]",
            "properties": {
                "publisher": "Microsoft.Azure.Extensions",
                "type": "CustomScript",
                "typeHandlerVersion": "2.0",
                "autoUpgradeMinorVersion": true,
                "settings": {
                    "fileUris": [
                        "[concat(parameters('artifactsLocation'), '/scripts/infra.sh', parameters('artifactsLocationSasToken'))]",
                        "[concat(parameters('artifactsLocation'), '/scripts/install.sh', parameters('artifactsLocationSasToken'))]",
                        "[concat(parameters('artifactsLocation'), '/scripts/azureProviderAndCreds.tf', parameters('artifactsLocationSasToken'))]"
                    ]
                },
                "protectedSettings": {
                    "commandToExecute": "[concat('bash infra.sh && bash install.sh ', variables('installParm1'), variables('installParm2'), variables('installParm3'), variables('installParm4'), ' -k ', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('stateStorageAccountName')), '2017-10-01').keys[0].value, ' -l ', reference(concat(resourceId('Microsoft.Compute/virtualMachines/', parameters('vmName')),'/providers/Microsoft.ManagedIdentity/Identities/default'),'2015-08-31-PREVIEW').principalId)]"
                }
            },
            "dependsOn": [
                "[resourceId('Microsoft.Authorization/roleAssignments', variables('resourceGuid'))]"
            ]
        }
[snip]

VMにログインし、環境を確認

では出来上がったVMにsshし、いろいろのぞいてみましょう。

$ ssh your-vm-public-ip

Terraformのバージョンは、現時点で最新の0.11.5が入っています。

$ terraform -v
Terraform v0.11.5

環境変数ARM_USE_MSIはtrueに設定されています。

$ echo $ARM_USE_MSI
true

MSIも有効化されています(SystemAssigned)。

$ az vm identity show -g tf-msi-poc-ejp-rg -n tfmsipocvm01
{
  "additionalProperties": {},
  "identityIds": null,
  "principalId": "aaaa-aaaa-aaaa-aaaa-aaaa",
  "tenantId": "tttt-tttt-tttt-tttt",
  "type": "SystemAssigned"
}

さて、このVMはMSIが使えるようになったわけですが、操作できるリソースのスコープは、このVMが属するリソースグループに限定されてます。新たなリソースグループを作成したい場合は、ロールを付与し、スコープを広げます。~/にtfEnv.shというスクリプトが用意されています。用意されたスクリプトを実行すると、サブスクリプションスコープのContributorがVMに割り当てられます。必要に応じて変更しましょう。

$ ls
tfEnv.sh  tfTemplate

$ cat tfEnv.sh
az login
az role assignment create  --assignee "aaaa-aaaa-aaaa-aaaa-aaaa" --role 'b24988ac-6180-42a0-ab88-20f7382dd24c'  --scope /subscriptions/"cccc-cccc-cccc-cccc"

$ . ~/tfEnv.sh
To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code HOGEHOGE to authenticate.
[snip]
{
  "additionalProperties": {},
  "canDelegate": null,
  "id": "/subscriptions/cccc-cccc-cccc-cccc/providers/Microsoft.Authorization/roleAssignments/ffff-ffff-ffff-ffff",
  "name": "ffff-ffff-ffff-ffff",
  "principalId": "aaaa-aaaa-aaaa-aaaa-aaaa",
  "roleDefinitionId": "/subscriptions/cccc-cccc-cccc-cccc/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c",
  "scope": "/subscriptions/cccc-cccc-cccc-cccc",
  "type": "Microsoft.Authorization/roleAssignments"
}

ちなみに、role id “b24988ac-6180-42a0-ab88-20f7382dd24c”はContributorを指します。

tfTemplateというディレクトリも用意されているようです。2つのファイルがあります。

$ ls tfTemplate/
azureProviderAndCreds.tf  remoteState.tf

azureProviderAndCreds.tfは、tfファイルのテンプレートです。コメントアウトと説明のとおり、MSIを使う場合には、このテンプレートは必要ありません。subscription_idとtenant_idは、VMのプロビジョニング時に環境変数にセットされています。そしてclient_idとclient_secretは、MSIを通じて取得されます。明示的に変えたい時のみ指定しましょう。

$ cat tfTemplate/azureProviderAndCreds.tf
#
#
# Provider and credential snippet to add to configurations
# Assumes that there's a terraform.tfvars file with the var values
#
# Uncomment the creds variables if using service principal auth
# Leave them commented to use MSI auth
#
#variable subscription_id {}
#variable tenant_id {}
#variable client_id {}
#variable client_secret {}

provider "azurerm" {
#    subscription_id = "${var.subscription_id}"
#    tenant_id = "${var.tenant_id}"
#    client_id = "${var.client_id}"
#    client_secret = "${var.client_secret}"
}

remoteState.tfは、TerraformのstateをAzureのBlob上に置く場合に使います。Blobのsoft deleteが使えるようになったこともあり、事件や事故を考慮すると、できればstateはローカルではなくBlobで管理したいところです。

$ cat tfTemplate/remoteState.tf
terraform {
 backend "azurerm" {
  storage_account_name = "storestaterandomid"
  container_name       = "terraform-state"
  key                  = "prod.terraform.tfstate"
  access_key           = "KYkCz88z+7yoyoyoiyoyoyoiyoyoyoiyoiTDZRbrwAWIPWD+rU6g=="
  }
}

soft delete設定は、別途 az storage blob service-properties delete-policy update コマンドで行ってください。

プロビジョニングしてみる

ではTerraformを動かしてみましょう。サブディレクトリsampleを作り、そこで作業します。

$ mkdir sample
$ cd sample/

stateはBlobで管理しましょう。先ほどのremoteState.tfを実行ディレクトリにコピーします。アクセスキーが入っていますので、このディレクトリをコード管理システム配下に置くのであれば、.gitignoreなどで除外をお忘れなく。

$ cp ../tfTemplate/remoteState.tf ./

ここのキーが残ってしまうのが現時点での課題。ストレージのキー問題は対応がはじまったので、いずれ解決するはずです。

ではTerraformで作るリソースを書きます。さくっとACI上にnginxコンテナーを作りましょう。

$ vim main.tf
resource "azurerm_resource_group" "tf-msi-poc" {
    name     = "tf-msi-poc-aci-wus-rg"
    location = "West US"
}

resource "random_integer" "random_int" {
    min = 100
    max = 999
}

resource "azurerm_container_group" "aci-example" {
    name                = "aci-cg-${random_integer.random_int.result}"
    location            = "${azurerm_resource_group.tf-msi-poc.location}"
    resource_group_name = "${azurerm_resource_group.tf-msi-poc.name}"
    ip_address_type     = "public"
    dns_name_label      = "tomakabe-aci-cg-${random_integer.random_int.result}"
    os_type             = "linux"

    container {
        name    = "nginx"
        image   = "nginx"
        cpu     = "0.5"
        memory  = "1.0"
        port    = "80"
    }
}

init、plan、アプラーイ。アプライ王子。

$ terraform init
$ terraform plan
$ terraform apply -auto-approve
[snip]
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

できたか確認。

$ az container show -g tf-msi-poc-aci-wus-rg -n aci-cg-736 -o table
Name        ResourceGroup          ProvisioningState    Image    IP:ports         CPU/Memory       OsType    Location
----------  ---------------------  -------------------  -------  ---------------  ---------------  --------  ----------
aci-cg-736  tf-msi-poc-aci-wus-rg  Succeeded            nginx    13.91.90.117:80  0.5 core/1.0 gb  Linux     westus
$ curl 13.91.90.117
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
[snip]

おまけ

サービスプリンシパルは、アプリに対して権限を付与するために必要な仕組みなのですが、使わなくなった際に消し忘れることが多いです。意識して消さないと、散らかり放題。

MSIの場合、対象のVMを消すとそのプリンシパルも消えます。爽快感ほとばしる。

$ az ad sp show --id aaaa-aaaa-aaaa-aaaa-aaaa
Resource 'aaaa-aaaa-aaaa-aaaa-aaaa' does not exist or one of its queried reference-property objects are not present.

27 Mar 2018, 00:10

Azure DNS Private Zonesの動きを確認する

プライベートゾーンのパブリックプレビュー開始

Azure DNSのプライベートゾーン対応が、全リージョンでパブリックプレビューとなりました。ゾーンとプレビューのプライベートとパブリックが入り混じって、なにやら紛らわしいですが。

さて、このプライベートゾーン対応ですが、名前のとおりAzure DNSをプライベートな仮想ネットワーク(VNet)で使えるようになります。加えて、しみじみと嬉しい便利機能がついています。

  • Split-Horizonに対応します。VNet内からの問い合わせにはプライベートゾーン、それ以外からはパブリックゾーンのレコードを返します。
  • 仮想マシンの作成時、プライベートゾーンへ自動でホスト名を追加します。
  • プライベートゾーンとVNetをリンクして利用します。複数のVNetをリンクすることが可能です。
  • リンクの種類として、仮想マシンホスト名の自動登録が行われるVNetをRegistration VNet、名前解決(正引き)のみ可能なResolution VNetがあります。
  • プライベートゾーンあたり、Registration VNetの現時点の上限数は1、Resolution VNetは10です。

公式ドキュメントはこちら。現時点の制約もまとまっているので、目を通しておきましょう。

動きを見てみよう

公式ドキュメントには想定シナリオがあり、これを読めばできることがだいたい分かります。ですが、名前解決は呼吸のようなもの、体に叩き込みたいお気持ちです。手を動かして確認します。

事前に準備する環境

下記リソースを先に作っておきます。手順は割愛。ドメイン名はexample.comとしましたが、適宜読み替えてください。

  • VNet *2
    • vnet01
    • subnet01
      • subnet01-nsg (allow ssh)
    • vnet02
    • subnet01
      • subnet01-nsg (allow ssh)
  • Azure DNS Public Zone
    • example.com

Azure CLIへDNS拡張を導入

プレビュー機能をCLIに導入します。いずれ要らなくなるかもしれませんので、要否は公式ドキュメントで確認してください。

$ az extension add --name dns

プライベートゾーンの作成

既存のゾーンを確認します。パブリックゾーンがあります。

$ az network dns zone list -o table
ZoneName      ResourceGroup             RecordSets    MaxRecordSets
------------  ----------------------  ------------  ---------------
example.com   common-global-rg                   2             5000

プライベートゾーンを作成します。Registration VNetとしてvnet01をリンクします。現時点の制約で、リンク時にはVNet上にVMが無い状態にする必要があります。

$ az network dns zone create -g private-dns-poc-ejp-rg -n example.com --zone-type Private --registration-vnets vnet01

同じ名前のゾーンが2つになりました。

$ az network dns zone list -o table
ZoneName      ResourceGroup             RecordSets    MaxRecordSets
------------  ----------------------  ------------  ---------------
example.com   common-global-rg                   2             5000
example.com   private-dns-poc-ejp-rg             1             5000

Registration VNetへVMを作成

VMを2つ作ります。1つにはインターネット経由でsshするので、パブリックIPを割り当てます。

$ BASE_NAME="private-dns-poc-ejp"
$ az network public-ip create -n vm01-pip -g ${BASE_NAME}-rg
$ az network nic create -g ${BASE_NAME}-rg -n vm01-nic --public-ip-address vm01-pip --vnet vnet01 --subnet subnet01
$ az vm create -g ${BASE_NAME}-rg -n vm01 --image Canonical:UbuntuServer:16.04.0-LTS:latest --size Standard_B1s --nics vm01-nic
$ az network nic create -g ${BASE_NAME}-rg -n vm02-nic --vnet vnet01 --subnet subnet01
$ az vm create -g ${BASE_NAME}-rg -n vm02 --image Canonical:UbuntuServer:16.04.0-LTS:latest --size Standard_B1s --nics vm02-nic

パブリックIPをパブリックゾーンへ登録

Split-Horizonの動きを確認したいので、パブリックIPをパブリックゾーンへ登録します。

$ az network public-ip show -g private-dns-poc-ejp-rg -n vm01-pip --query ipAddress
"13.78.84.84"
$ az network dns record-set a add-record -g common-global-rg -z example.com -n vm01 -a 13.78.84.84

パブリックゾーンで名前解決できることを確認します。

$ nslookup vm01.example.com
Server:         103.5.140.1
Address:        103.5.140.1#53

Non-authoritative answer:
Name:   vm01.example.com
Address: 13.78.84.84

Registration VNetの動きを確認

vnet01のvm01へ、パブリックIP経由でsshします。

$ ssh vm01.example.com

同じRegistration VNet上のvm02を正引きします。ドメイン名無し、ホスト名だけでnslookupすると、VNetの内部ドメイン名がSuffixになります。

vm01:~$ nslookup vm02
Server:         168.63.129.16
Address:        168.63.129.16#53

Non-authoritative answer:
Name:   vm02.aioh0amlfdze5drhlpb1ktqwxd.lx.internal.cloudapp.net
Address: 10.0.0.5

ドメイン名をつけてみましょう。Nameはvnet01にリンクしたプライベートゾーンのドメイン名になりました。

vm01:~$ nslookup vm02.example.com
Server:         168.63.129.16
Address:        168.63.129.16#53

Non-authoritative answer:
Name:   vm02.example.com
Address: 10.0.0.5

逆引きもできます。

vm01:~$ nslookup 10.0.0.5
Server:         168.63.129.16
Address:        168.63.129.16#53

Non-authoritative answer:
5.0.0.10.in-addr.arpa   name = vm02.example.com.

Authoritative answers can be found from:

Split-Horizonの動きを確認

さて、いま作業をしているvm01には、インターネット経由でパブリックゾーンで名前解決してsshしたわけですが、プライベートなVNet内でnslookupするとどうなるでしょう。

vm01:~$ nslookup vm01.example.com
Server:         168.63.129.16
Address:        168.63.129.16#53

Non-authoritative answer:
Name:   vm01.example.com
Address: 10.0.0.4

プライベートゾーンで解決されました。Split-Horizonが機能していることが分かります。

あ、どうでもいいことですが、Split-Horizonって戦隊モノの必殺技みたいなネーミングですね。叫びながら地面に拳を叩きつけたい感じ。

Resolution VNetの動きを確認

vnet02を作成し、Resolution VNetとしてプライベートゾーンとリンクします。そして、vnet02にvm03を作ります。vm03へのsshまで一気に進めます。

$ BASE_NAME="private-dns-poc-ejp"
$ az network vnet create -g ${BASE_NAME}-rg -n vnet02 --address-prefix 10.1.0.0/16 --subnet-name subnet01
$ az network vnet subnet update -g ${BASE_NAME}-rg -n subnet01 --vnet-name vnet02 --network-security-group subnet01-nsg
$ az network public-ip create -n vm03-pip -g ${BASE_NAME}-rg
$ az network dns zone update -g private-dns-poc-ejp-rg -n example.com --resolution-vnets vnet02
$ az network nic create -g ${BASE_NAME}-rg -n vm03-nic --public-ip-address vm03-pip --vnet vnet02 --subnet subnet01
$ az vm create -g ${BASE_NAME}-rg -n vm03 --image Canonical:UbuntuServer:16.04.0-LTS:latest --size Standard_B1s --nics vm03-nic
$ az network public-ip show -g private-dns-poc-ejp-rg -n vm03-pip --query ipAddress
"13.78.54.133"
$ ssh 13.78.54.133

名前解決の確認が目的なので、vnet01/02間はPeeringしません。

では、vnet01上のvm01を正引きします。ドメイン名を指定しないと、解決できません。vnet02上にvm01がある、と指定されたと判断するからです。

vm03:~$ nslookup vm01
Server:         168.63.129.16
Address:        168.63.129.16#53

** server can't find vm01: SERVFAIL

ではプライベートゾーンのドメイン名をつけてみます。解決できました。

vm03:~$ nslookup vm01.example.com
Server:         168.63.129.16
Address:        168.63.129.16#53

Non-authoritative answer:
Name:   vm01.example.com
Address: 10.0.0.4

Resolution VNetからは、逆引きできません。

vm03:~$ nslookup 10.0.0.4
Server:         168.63.129.16
Address:        168.63.129.16#53

** server can't find 4.0.0.10.in-addr.arpa: NXDOMAIN

ところでRegistration VNetからResolution VNetのホスト名をnslookupするとどうなるでしょう。

$ ssh vm01.example.com
vm01:~$ nslookup vm03
Server:         168.63.129.16
Address:        168.63.129.16#53

** server can't find vm03: SERVFAIL

vm01:~$ nslookup vm03.example.com
Server:         168.63.129.16
Address:        168.63.129.16#53

** server can't find vm03.example.com: NXDOMAIN

ドメイン名あり、なしに関わらず、名前解決できません。VNetが別なのでVNetの内部DNSで解決できない、また、Resolution VNetのVMはレコードがプライベートゾーンに自動登録されないことが分かります。

26 Mar 2018, 00:08

AzureのAvailability Zonesへ分散するVMSSをTerraformで作る

動機

Terraform Azure Provider 1.3.0で、VMSSを作る際にAvailability Zonesを指定できるようになりました。Availability Zonesはインフラの根っこの仕組みなので、現在(20183)限定されたリージョンで長めのプレビュー期間がとられています。ですが、GAやグローバル展開を見据え、素振りしておきましょう。

前提条件

  • Availability Zones対応リージョンを選びます。現在は5リージョンです。この記事ではEast US 2とします。
  • Availability Zonesのプレビューにサインアップ済みとします。
  • bashでsshの公開鍵が~/.ssh/id_rsa.pubにあると想定します。
  • 動作確認した環境は以下です。
    • Terraform 0.11.2
    • Terraform Azure Provider 1.3.0
    • WSL (ubuntu 16.04)
    • macos (High Sierra 10.13.3)

コード

以下のファイルを同じディレクトリに作成します。

Terraform メインコード

VMSSと周辺リソースを作ります。

  • 最終行近くの “zones = [1, 2, 3]” がポイントです。これだけで、インスタンスを散らす先のゾーンを指定できます。
  • クロスゾーン負荷分散、冗長化するため、Load BalancerとパブリックIPのSKUをStandardにします。

[main.tf]

resource "azurerm_resource_group" "poc" {
  name     = "${var.resource_group_name}"
  location = "East US 2"
}

resource "azurerm_virtual_network" "poc" {
  name                = "vnet01"
  resource_group_name = "${azurerm_resource_group.poc.name}"
  location            = "${azurerm_resource_group.poc.location}"
  address_space       = ["10.0.0.0/16"]
}

resource "azurerm_subnet" "poc" {
  name                      = "subnet01"
  resource_group_name       = "${azurerm_resource_group.poc.name}"
  virtual_network_name      = "${azurerm_virtual_network.poc.name}"
  address_prefix            = "10.0.2.0/24"
  network_security_group_id = "${azurerm_network_security_group.poc.id}"
}

resource "azurerm_network_security_group" "poc" {
  name                = "nsg01"
  resource_group_name = "${azurerm_resource_group.poc.name}"
  location            = "${azurerm_resource_group.poc.location}"

  security_rule = [
    {
      name                       = "allow_http"
      priority                   = 100
      direction                  = "Inbound"
      access                     = "Allow"
      protocol                   = "Tcp"
      source_port_range          = "*"
      destination_port_range     = "80"
      source_address_prefix      = "*"
      destination_address_prefix = "*"
    },
    {
      name                       = "allow_ssh"
      priority                   = 101
      direction                  = "Inbound"
      access                     = "Allow"
      protocol                   = "Tcp"
      source_port_range          = "*"
      destination_port_range     = "22"
      source_address_prefix      = "*"
      destination_address_prefix = "*"
    },
  ]
}

resource "azurerm_public_ip" "poc" {
  name                         = "pip01"
  resource_group_name          = "${azurerm_resource_group.poc.name}"
  location                     = "${azurerm_resource_group.poc.location}"
  public_ip_address_allocation = "static"
  domain_name_label            = "${var.scaleset_name}"

  sku = "Standard"
}

resource "azurerm_lb" "poc" {
  name                = "lb01"
  resource_group_name = "${azurerm_resource_group.poc.name}"
  location            = "${azurerm_resource_group.poc.location}"

  frontend_ip_configuration {
    name                 = "fipConf01"
    public_ip_address_id = "${azurerm_public_ip.poc.id}"
  }

  sku = "Standard"
}

resource "azurerm_lb_backend_address_pool" "poc" {
  name                = "bePool01"
  resource_group_name = "${azurerm_resource_group.poc.name}"
  loadbalancer_id     = "${azurerm_lb.poc.id}"
}

resource "azurerm_lb_rule" "poc" {
  name                           = "lbRule"
  resource_group_name            = "${azurerm_resource_group.poc.name}"
  loadbalancer_id                = "${azurerm_lb.poc.id}"
  protocol                       = "Tcp"
  frontend_port                  = 80
  backend_port                   = 80
  frontend_ip_configuration_name = "fipConf01"
  backend_address_pool_id        = "${azurerm_lb_backend_address_pool.poc.id}"
  probe_id                       = "${azurerm_lb_probe.poc.id}"
}

resource "azurerm_lb_probe" "poc" {
  name                = "http-probe"
  resource_group_name = "${azurerm_resource_group.poc.name}"
  loadbalancer_id     = "${azurerm_lb.poc.id}"
  port                = 80
}

resource "azurerm_lb_nat_pool" "poc" {
  count                          = 3
  name                           = "ssh"
  resource_group_name            = "${azurerm_resource_group.poc.name}"
  loadbalancer_id                = "${azurerm_lb.poc.id}"
  protocol                       = "Tcp"
  frontend_port_start            = 50000
  frontend_port_end              = 50119
  backend_port                   = 22
  frontend_ip_configuration_name = "fipConf01"
}

data "template_cloudinit_config" "poc" {
  gzip          = true
  base64_encode = true

  part {
    content_type = "text/cloud-config"
    content      = "${file("${path.module}/cloud-config.yaml")}"
  }
}

resource "azurerm_virtual_machine_scale_set" "poc" {
  name                = "${var.scaleset_name}"
  resource_group_name = "${azurerm_resource_group.poc.name}"
  location            = "${azurerm_resource_group.poc.location}"
  upgrade_policy_mode = "Manual"

  sku {
    name     = "Standard_B1s"
    tier     = "Standard"
    capacity = 3
  }

  storage_profile_image_reference {
    publisher = "Canonical"
    offer     = "UbuntuServer"
    sku       = "16.04-LTS"
    version   = "latest"
  }

  storage_profile_os_disk {
    name              = ""
    caching           = "ReadWrite"
    create_option     = "FromImage"
    managed_disk_type = "Standard_LRS"
  }

  os_profile {
    computer_name_prefix = "pocvmss"
    admin_username       = "${var.admin_username}"
    admin_password       = ""
    custom_data          = "${data.template_cloudinit_config.poc.rendered}"
  }

  os_profile_linux_config {
    disable_password_authentication = true

    ssh_keys {
      path     = "/home/${var.admin_username}/.ssh/authorized_keys"
      key_data = "${file("~/.ssh/id_rsa.pub")}"
    }
  }

  network_profile {
    name    = "terraformnetworkprofile"
    primary = true

    ip_configuration {
      name                                   = "PoCIPConfiguration"
      subnet_id                              = "${azurerm_subnet.poc.id}"
      load_balancer_backend_address_pool_ids = ["${azurerm_lb_backend_address_pool.poc.id}"]
      load_balancer_inbound_nat_rules_ids    = ["${element(azurerm_lb_nat_pool.poc.*.id, count.index)}"]
    }
  }

  zones = [1, 2, 3]
}

cloud-init configファイル

各インスタンスがどのゾーンで動いているか確認したいので、インスタンス作成時にcloud-initでWebサーバーを仕込みます。メタデータからインスタンス名と実行ゾーンを引っ張り、nginxのドキュメントルートに書きます。

[cloud-config.yaml]

#cloud-config
package_upgrade: true
packages:
  - nginx
runcmd:
  - 'echo "[Instance Name]: `curl -H Metadata:true "http://169.254.169.254/metadata/instance/compute/name?api-version=2017-12-01&format=text"`    [Zone]: `curl -H Metadata:true "http://169.254.169.254/metadata/instance/compute/zone?api-version=2017-12-01&format=text"`" > /var/www/html/index.nginx-debian.html'

インスタンス作成時、パッケージの導入やアップデートに時間をかけたくない場合は、Packerなどで前もってカスタムイメージを作っておくのも手です。

Terraform 変数ファイル

変数は別ファイルへ。

[variables.tf]

variable "resource_group_name" {
  default = "your-rg"
}

variable "scaleset_name" {
  default = "yourvmss01"
}

variable "admin_username" {
  default = "yourname"
}

実行

では実行。

$ terraform init
$ terraform plan
$ terraform apply

5分くらいで完了しました。このサンプルでは、この後のcloud-initのパッケージ処理に時間がかかります。待てない場合は前述の通り、カスタムイメージを使いましょう。

インスタンスへのsshを通すよう、Load BalancerにNATを設定していますので、cloud-initの進捗は確認できます。

$ ssh -p 50000 yourname@yourvmss01.eastus2.cloudapp.azure.com
$ tail -f /var/log/cloud-init-output.log
Cloud-init v. 17.1 finished at Sun, 25 Mar 2018 10:41:40 +0000. Datasource DataSourceAzure [seed=/dev/sr0].  Up 611.51 seconds

ではWebサーバーにアクセスしてみましょう。

$ while true; do curl yourvmss01.eastus2.cloudapp.azure.com; sleep 1; done;
[Instance Name]: yourvmss01_2    [Zone]: 3
[Instance Name]: yourvmss01_0    [Zone]: 1
[Instance Name]: yourvmss01_2    [Zone]: 3
[Instance Name]: yourvmss01_1    [Zone]: 2

VMSSのインスタンスがゾーンに分散されたことが分かります。

では、このままスケールアウトしてみましょう。main.tfのazurerm_virtual_machine_scale_set.poc.sku.capacityを3から4にし、再度applyします。

[Instance Name]: yourvmss01_1    [Zone]: 2
[Instance Name]: yourvmss01_3    [Zone]: 1
[Instance Name]: yourvmss01_3    [Zone]: 1
[Instance Name]: yourvmss01_1    [Zone]: 2
[Instance Name]: yourvmss01_3    [Zone]: 1

ダウンタイムなしに、yourvmss01_3が追加されました。すこぶる簡単。