02 May 2018, 17:00

WSLENVでWSLとWindowsの環境変数を共有する(Go開発環境編)

見た目は地味だが役に立つ

Windows 10 April 2018 Update (別名: バージョン1803)がリリースされました。タイムラインなど目立つ機能が注目されていますが、開発者支援系の機能、ツールも拡充されています。特に、WSL/Windowsの連携、相互運用まわりは着実に進化しています。そのうちのひとつが、このエントリーで紹介するWSLENVです。

WSLENVは、WSL/Windows間で環境変数を共有する仕組みです。ただ単純に共有するだけでなく、ルールに従って変換も行います。これが地味に便利。でも地味だから、あまり話題になっていない。なので具体例で紹介しよう、というのがこのエントリーの目的です。

TL;DR

英語が読めて、「あ、それ便利ね」とピンとくる人は以下を。

Share Environment Vars between WSL and Windows

Go開発環境を例に

前述のリンクでも紹介されていますが、Goの開発環境はWSLENVの代表的なユースケースです。GOPATHをいい感じにWSL/Windowsで共有できます。掘り下げていきましょう。

想定開発者像、ペルソナ

  • Windows端末を使っている
  • Go言語を使っている
  • CLIはbash/WSL中心
    • スクリプト書くならPowerShellもいいけど、インタラクティブな操作はbashが楽
    • アプリをDockerコンテナーとしてビルドするなど、OSSエコシステム、ツールとの連携を考慮
  • とはいえエディタ/IDEはWindows側で動かしたい、最近はVS Code中心

前提条件

  • WSL、WindowsそれぞれにGoを導入
    • バージョン管理のためにも、パッケージマネージャーがおすすめ
    • わたしはWSL(Ubuntu)でapt、WindowsではChocolateyを使ってGoを導入しています
  • GOPATHは %USERPROFILE%go とする
    • ユーザー名を tomakabeとすると C:\Users\tomakabe\go
    • setx GOPATH “$env:USERPROFILE\go” で設定
    • WSLでもこのディレクトリをGOPATHとする
  • VS Code + Go拡張をWindowsに導入
  • WindowsのCLIはPowerShellを利用

そぞろ歩き その1(WindowsでのGo開発)

では、何が課題で、WSLがどのようにそれを解決するか、見ていきましょう。

まず、Windowsで環境変数GOPATHを確認します。

PS C:\WINDOWS\system32> Get-ChildItem env:GOPATH

Name                           Value
----                           -----
GOPATH                         C:\Users\tomakabe\go

GOPATHに移動し、ディレクトリ構造を確認します。この環境にはすでにディレクトリbinとsrcがあり、binにはいくつかexeが入っています。VS CodeのGo拡張を入れると導入を促されるツール群は、ここに格納され、構文チェックや補完でVS Codeと連動します。

PS C:\WINDOWS\system32> cd C:\Users\tomakabe\go
PS C:\Users\tomakabe\go> ls


    ディレクトリ: C:\Users\tomakabe\go


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----       2018/05/02     11:10                bin
d-----       2018/05/02     11:06                src

PS C:\Users\tomakabe\go> ls .\bin\


    ディレクトリ: C:\Users\tomakabe\go\bin


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----       2018/05/02     11:10       14835200 dlv.exe
-a----       2018/05/02     11:09        4239360 go-outline.exe
-a----       2018/05/02     11:09        4045824 go-symbols.exe
-a----       2018/05/02     11:08       11094528 gocode.exe
-a----       2018/05/02     11:09        5708288 godef.exe
[snip]

サンプルコードのディレクトリへ移動し、中身を確認します。シンプルな挨拶アプリです。

PS C:\Users\tomakabe\go> cd .\src\github.com\ToruMakabe\work\
PS C:\Users\tomakabe\go\src\github.com\ToruMakabe\work> cat .\hello.go
package main

import "fmt"

func main() {
        fmt.Println("Hello Go on the new WSL")
}

ビルドして動かしてみましょう。Windows環境ではデフォルトで実行ファイルとしてexeが作られます。

PS C:\Users\tomakabe\go\src\github.com\ToruMakabe\work> go build .\hello.go
PS C:\Users\tomakabe\go\src\github.com\ToruMakabe\work> ls


    ディレクトリ: C:\Users\tomakabe\go\src\github.com\ToruMakabe\work


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----       2018/05/02     11:54        2049536 hello.exe
-a----       2018/05/02     11:10             91 hello.go


PS C:\Users\tomakabe\go\src\github.com\ToruMakabe\work> .\hello.exe
Hello Go on the new WSL

ここまでは従来のWindowsにおけるGo開発環境です。ではWSLに話を移しましょう。

そぞろ歩き その2(WSLでのGo開発)

WSLにつなぎます。ターミナルは任意ですが、わたしはVS Codeの統合ターミナルが好きです。コードを書きながら操作できるので。

GOPATHを確認します。空っぽです。WSLは既定でWindowsから環境変数PATHを受け取ります。PATHは特別扱いです。ですが、他の環境変数は、指定しないと渡されません。よってWindowsで設定していても、WSLから見るとGOPATHは空っぽです。

~ $ echo $GOPATH

$HOMEもきれいな状態です。

~ $ ls
~ $

ではGOPATHに指定したい、先ほどWindowsで確認したディレクトリへ移動します。ちなみにWindowsのCドライブはWSLで/mnt/c/に変換されます。先ほど確認したbin、srcが見えています。

~ $ cd /mnt/c/Users/tomakabe/go/
/mnt/c/Users/tomakabe/go $ ls
bin  src

ではここで実験。試しにパッケージをインポートしてみましょう。定番のgoimportsをインポートしてみます。わざとらしいですが、なんだか嫌な予感がします。

/mnt/c/Users/tomakabe/go $ go get -v golang.org/x/tools/cmd/goimports
Fetching https://golang.org/x/tools/cmd/goimports?go-get=1
Parsing meta tags from https://golang.org/x/tools/cmd/goimports?go-get=1 (status code 200)
get "golang.org/x/tools/cmd/goimports": found meta tag get.metaImport{Prefix:"golang.org/x/tools", VCS:"git", RepoRoot:"https://go.googlesource.com/tools"} at https://golang.org/x/tools/cmd/goimports?go-get=1
get "golang.org/x/tools/cmd/goimports": verifying non-authoritative meta tag
Fetching https://golang.org/x/tools?go-get=1
Parsing meta tags from https://golang.org/x/tools?go-get=1 (status code 200)
golang.org/x/tools (download)
created GOPATH=/home/tomakabe/go; see 'go help gopath'

嫌な予感は予定調和で的中します。GOPATHがいらっしゃらないので、/home/tomakabe/go とみなしてしまいました。先ほど確認した際、$HOMEはきれいな状態でした、が。新たにお作りになられたようです。

/mnt/c/Users/tomakabe/go $ ls ~/
go
/mnt/c/Users/tomakabe/go $ ls ~/go
bin  src

これではWSLとWindowsで、ソースもバイナリーも別々の管理になってしまいます。これはつらい。ああ、GOPATHを共有できればいいのに。

そぞろ歩き その3(解決編)

そこで登場するのが、WSLENVです。Windowsで作業します。Windowsの環境変数GOPATHを、環境変数WSLENVへスイッチとともに設定します。/pスイッチは、「この環境変数はパスを格納しているから、いい感じにして」という指定です。

PS C:\Users\tomakabe\go\src\github.com\ToruMakabe\work> setx WSLENV "$env:WSLENV`:GOPATH/p"

成功: 指定した値は保存されました。

いい感じって何よ。それは環境に合わせたパス表現の変換です。WSLで見てみましょう。WSLENVを読ませる必要があるため、VS Codeを再起動します。そして、ターミナルで確認します。

/mnt/c/Users/tomakabe/go $ echo $GOPATH
/mnt/c/Users/tomakabe/go

GOPATHが読めるようになりました。かつ、Windowsのパス表現であるC:\Users\tomakabe\goから、WSLの表現である/mnt/c/Users/tomakabe/goへと変換して渡しています。素晴らしい。これでGOPATHはひとつになり、ソースやバイナリー、パッケージの管理を統一できます。

ではWSLでサンプルコードを触ってみましょう。ソースのあるディレクトリへ移動します。ソースと先ほどビルドしたexeがあります。

/mnt/c/Users/tomakabe/go $ cd src/github.com/ToruMakabe/work/
/mnt/c/Users/tomakabe/go/src/github.com/ToruMakabe/work $ ls
hello.exe  hello.go

WSL上でビルドします。ELFバイナリー hello が作られました。

/mnt/c/Users/tomakabe/go/src/github.com/ToruMakabe/work $ go build hello.go
/mnt/c/Users/tomakabe/go/src/github.com/ToruMakabe/work $ ls
hello  hello.exe  hello.go
/mnt/c/Users/tomakabe/go/src/github.com/ToruMakabe/work $ file ./hello
./hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
/mnt/c/Users/tomakabe/go/src/github.com/ToruMakabe/work $ ./hello
Hello Go on the new WSL

まとめ

代表例としてGoの開発環境で説明しましたが、WSLENVは他の用途でも応用できるでしょう。スイッチの説明など、詳細は先ほど紹介した、こちらを。

27 Apr 2018, 17:00

TerraformでAzureのシークレットを受け渡す(ACI/AKS編)

動機

システム開発、運用の現場では、しばしばシークレットの受け渡しをします。代表例はデータベースの接続文字列です。データベース作成時に生成した接続文字列をアプリ側で設定するのですが、ひとりでコピペするにせよ、チームメンバー間で受け渡すにせよ、めんどくさく、危険が危ないわけです。

  • いちいちポータルやCLIで接続文字列を出力、コピーして、アプリの設定ファイルや環境変数にペーストしなければいけない
    • めんどくさいし手が滑る
  • データベース管理者がアプリ開発者に接続文字列を何らかの手段で渡さないといけない
    • メールとかチャットとかファイルサーバーとか勘弁
  • もしくはアプリ開発者にデータベースの接続文字列が読める権限を与えなければいけない
    • 本番でも、それやる?
  • kubernetes(k8s)のSecretをいちいちkubectlを使って作りたくない
    • Base64符号化とか、うっかり忘れる

つらいですね。シークレットなんて意識したくないのが人情。そこで、Terraformを使った解決法を。

シナリオ

Azureでコンテナーを使うシナリオを例に紹介します。ACI(Azure Container Instances)とAKS(Azure Container Service - k8s)の2パターンです。

  • Nodeとデータストアを組み合わせた、Todoアプリケーション
  • コンテナーイメージはDocker Hubにある
  • コンテナーでデータストアを運用したくないので、データストアはマネージドサービスを使う
  • データストアはCosmos DB(MongoDB API)
  • Cosmos DBへのアクセスに必要な属性をTerraformで参照し、接続文字列(MONGO_URL)を作る
    • 接続文字列の渡し方はACI/AKSで異なる
    • ACI
      • コンテナー作成時に環境変数として接続文字列を渡す
    • AKS
      • k8sのSecretとして接続文字列をストアする
      • コンテナー作成時にSecretを参照し、環境変数として渡す

検証環境

  • Azure Cloud Shell
    • Terraform v0.11.7
    • Terraformの認証はCloud Shell組み込み
  • Terraform Azure Provider v1.4
  • Terraform kubernetes Provider v1.1
  • AKS kubernetes 1.9.6

ACIの場合

ざっと以下の流れです。

  1. リソースグループ作成
  2. Cosmos DBアカウント作成
  3. ACIコンテナーグループ作成 (Cosmos DB属性から接続文字列を生成)

var.で参照している変数は、別ファイルに書いています。

[main.tf]

resource "azurerm_resource_group" "rg" {
  name     = "${var.resource_group_name}"
  location = "${var.resource_group_location}"
}

resource "random_integer" "ri" {
  min = 10000
  max = 99999
}

resource "azurerm_cosmosdb_account" "db" {
  name                = "your-cosmos-db-${random_integer.ri.result}"
  location            = "${azurerm_resource_group.rg.location}"
  resource_group_name = "${azurerm_resource_group.rg.name}"
  offer_type          = "Standard"
  kind                = "MongoDB"

  enable_automatic_failover = true

  consistency_policy {
    consistency_level       = "BoundedStaleness"
    max_interval_in_seconds = 10
    max_staleness_prefix    = 200
  }

  geo_location {
    location          = "${azurerm_resource_group.rg.location}"
    failover_priority = 0
  }

  geo_location {
    location          = "${var.failover_location}"
    failover_priority = 1
  }
}

resource "azurerm_container_group" "aci-todo" {
  name                = "aci-todo"
  location            = "${azurerm_resource_group.rg.location}"
  resource_group_name = "${azurerm_resource_group.rg.name}"
  ip_address_type     = "public"
  dns_name_label      = "yourtodo"
  os_type             = "linux"

  container {
    name   = "todo"
    image  = "torumakabe/nodetodo"
    cpu    = "1"
    memory = "1.5"
    port   = "8080"

    environment_variables {
      "MONGO_URL" = "mongodb://${azurerm_cosmosdb_account.db.name}:${azurerm_cosmosdb_account.db.primary_master_key}@${azurerm_cosmosdb_account.db.name}.documents.azure.com:10255/?ssl=true"
    }
  }
}

containerのenvironment_variablesブロックでCosmos DBの属性を参照し、接続文字列を生成しています。簡単ですね。これで接続文字列コピペ作業から解放されます。

AKS

AKSの場合、流れは以下の通りです。

  1. リソースグループ作成
  2. Cosmos DBアカウント作成
  3. AKSクラスター作成
  4. k8s Secretを作成 (Cosmos DB属性から接続文字列生成)
  5. k8s Secretをコンテナーの環境変数として参照し、アプリをデプロイ

[main.tf]

resource "azurerm_resource_group" "rg" {
  name     = "${var.resource_group_name}"
  location = "${var.resource_group_location}"
}

resource "random_integer" "ri" {
  min = 10000
  max = 99999
}

resource "azurerm_cosmosdb_account" "db" {
  name                = "your-cosmos-db-${random_integer.ri.result}"
  location            = "${azurerm_resource_group.rg.location}"
  resource_group_name = "${azurerm_resource_group.rg.name}"
  offer_type          = "Standard"
  kind                = "MongoDB"

  enable_automatic_failover = true

  consistency_policy {
    consistency_level       = "BoundedStaleness"
    max_interval_in_seconds = 10
    max_staleness_prefix    = 200
  }

  geo_location {
    location          = "${azurerm_resource_group.rg.location}"
    failover_priority = 0
  }

  geo_location {
    location          = "${var.failover_location}"
    failover_priority = 1
  }
}

resource "azurerm_kubernetes_cluster" "aks" {
  name                = "yourakstf"
  location            = "${azurerm_resource_group.rg.location}"
  resource_group_name = "${azurerm_resource_group.rg.name}"
  dns_prefix          = "yourakstf"
  kubernetes_version  = "1.9.6"

  linux_profile {
    admin_username = "${var.admin_username}"

    ssh_key {
      key_data = "${var.key_data}"
    }
  }

  agent_pool_profile {
    name            = "default"
    count           = 3
    vm_size         = "Standard_B2ms"
    os_type         = "Linux"
    os_disk_size_gb = 30
  }

  service_principal {
    client_id     = "${var.client_id}"
    client_secret = "${var.client_secret}"
  }
}

provider "kubernetes" {
  host = "${azurerm_kubernetes_cluster.aks.kube_config.0.host}"

  client_certificate     = "${base64decode(azurerm_kubernetes_cluster.aks.kube_config.0.client_certificate)}"
  client_key             = "${base64decode(azurerm_kubernetes_cluster.aks.kube_config.0.client_key)}"
  cluster_ca_certificate = "${base64decode(azurerm_kubernetes_cluster.aks.kube_config.0.cluster_ca_certificate)}"
}

resource "kubernetes_secret" "cosmosdb_secret" {
  metadata {
    name = "cosmosdb-secret"
  }

  data {
    MONGO_URL = "mongodb://${azurerm_cosmosdb_account.db.name}:${azurerm_cosmosdb_account.db.primary_master_key}@${azurerm_cosmosdb_account.db.name}.documents.azure.com:10255/?ssl=true"
  }
}

Cosmos DB、AKSクラスターを作ったのち、kubernetesプロバイダーを使ってSecretを登録しています。複数のプロバイダーを組み合わせられる、Terraformの特長が活きています。

そしてアプリのデプロイ時に、登録したSecretを指定します。ここからはkubernetesワールドなので、kubectlなどを使います。マニフェストは以下のように。

[todo.yaml]

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: todoapp
spec:
  selector:
    matchLabels:
      app: todoapp
  replicas: 2
  template:
    metadata:
      labels:
        app: todoapp
    spec:
      containers:
        - name: todoapp
          image: torumakabe/nodetodo
          ports:
            - containerPort: 8080
          env:
            - name: MONGO_URL
              valueFrom:
                secretKeyRef:
                  name: cosmosdb-secret
                  key: MONGO_URL
---
apiVersion: v1
kind: Service
metadata:
  name: todoapp
spec:
  type: LoadBalancer
  ports:
    - port: 80
      targetPort: 8080
  selector:
    app: todoapp

シークレットの中身を見ることなく、コピペもせず、もちろんメールやチャットやファイルも使わず、アプリからCosmos DBへ接続できました。

シークレットに限らず、Terraformの属性参照、変数表現は強力ですので、ぜひ活用してみてください。数多くのAzureリソースが対応しています。

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 [].virtualMachine.network.publicIpAddresses[].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.