sakuracloud_exporter v0.7.0 − コレクターごとの無効化/fakeモードの追加 など

Prometheusのさくらのクラウド向けExporterであるsakuracloud_exporterのv0.7.0をリリースしました。

github.com

v0.7.0での主な変更/修正点は以下の通りです。

今回の修正はちょっと特殊な状況向けではありますが、ハマれば非常に便利なものだと思います。 順に紹介します。

コレクターごとの有効/無効切り替え

sakuracloud_exporterではさくらのクラウドAPIでリソース一覧を取得〜必要に応じて各リソースごとに詳細取得のためにさらにAPI呼び出し、という処理を行なっています。 このため、さくらのクラウド上に大量のリソースを持つようなケースでは一連のAPI呼び出しに長い時間がかかってしまうことがあります。

そこでこの問題への対応の一環としてコレクターごとに有効/無効を指定できるようになりました。
以下のフラグを用いて各コレクターごとに設定が可能となっています。

  • --no-collector.auto-backup: 自動バックアップ
  • --no-collector.coupon : クーポン
  • --no-collector.database: データベース
  • --no-collector.internet : スイッチ+ルータ
  • --no-collector.load-balancer: ロードバランサ
  • --no-collector.mobile-gateway: モバイルゲートウェイ
  • --no-collector.nfs: NFS
  • --no-collector.proxy-lb: エンハンスドロードバランサ
  • --no-collector.server: サーバ
  • --no-collector.sim: SIM
  • --no-collector.vpc-router: VPCルータ
  • --no-collector.zone: ゾーン情報

デフォルトでは全コレクターが有効となっています。

特定のコレクターだけ無効にするようなケースや、特定のコレクターだけ有効にして複数のexporterを起動するようなケースなどにご利用いただけると思います。

さくらのクラウドAPI呼び出しの代わりにローカルファイルから値を取得する「fakeモード」

このバージョンからGo言語向けさくらのクラウドAPIライブラリ libsacloud v2.0が利用されるようになりました。

libsacloud v2.0では主にテスト用の機能として、さくらのクラウドAPI呼び出しをラップしてダミーの応答を返す「fakeドライバー」という仕組みが導入されました。fakeドライバーはステートフルな処理(リソースの登録〜登録したリソースを後から参照、など)を行うためにステートを保持しています。ステートは保存先をインメモリ or ファイルから選べるようになっています。

このfakeドライバーを利用することでsakuracloud_exporterでサーバがダウンした状況やメンテナンスがスケジュールされた状況などを意図的に作り出すことを容易にするのが今回導入された「fakeモード」です。

fakeモードはibsacloudのfakeドライバーを利用してさくらのクラウドAPI呼び出しの代わりにステートファイルから値を取得してメトリクスを算出するモードです。

fakeモードの有効化

fakeモードの有効化は

  • 起動時のオプション--fake-modeの指定
  • 環境変数FAKE_MODEの指定

の何れかで可能です。

値はステートファイルのパスを指定します。

例えばカレントディレクトリのfake-store.jsonというファイルを利用する場合は以下のように指定します。

$ sakuracloud_exporter --fake-mode fake-store.json

ファイルが存在しない場合は新規作成されます。 (とはいえその状態だと何もリソースが存在しないためあまり意味はないでしょうが)

fakeモードではこのファイルからサーバやディスクといったリソースの情報を読み取ってメトリクスを出力します。

ステートファイルはJSONとなっており、以下のようなものです。

[
    {
        "Availability": "available",
        "CPU": 1,
        "CreatedAt": "2019-08-05T11:35:39.075614+09:00",
        "Description": "desc",
        "ID": "100000000022",
        "InstanceHostInfoURL": "",
        "InstanceHostName": "sac-is1a-svXXX",
        "InstanceStatus": "up",
        "InterfaceDriver": "virtio",
        "Interfaces": [
            {
                "HostName": "",
                "ID": 100000000028,
                "IPAddress": "",
                "MACAddress": "00:00:5e:00:53:01",
                "PacketFilterID": "",
                "PacketFilterName": "",
                "PacketFilterRequiredHostVersion": 0,
                "SubnetBandWidthMbps": 0,
                "SubnetDefaultRoute": "",
                "SubnetNetworkAddress": "",
                "SubnetNetworkMaskLen": 0,
                "SwitchID": 100000000008,
                "SwitchName": "",
                "SwitchScope": "",
                "UpstreamType": "",
                "UserIPAddress": "",
                "UserSubnetDefaultRoute": "",
                "UserSubnetNetworkMaskLen": 0
            }
        ],
        "MemoryMB": 1024,
        "Name": "example",
        "ResourceType": "Server",
        "ServerPlanCommitment": "standard",
        "ServerPlanGeneration": 100,
        "ServerPlanID": 100001001,
        "ServerPlanName": "世代:100 メモリ:001 CPU:001",
        "Tags": [
            "example",
            "server"
        ],
        "ZoneName": "is1a"
    }
]

ステートファイルの例として、sakuracloud_exporterが利用する各種リソースが登録されているものを以下に用意しています。

sakuracloud_exporter/example-fake-store.json at 0.7.0 · sacloud/sakuracloud_exporter · GitHub

ステートファイルはテキストファイルですのでエディタなどで編集することができるようになっています。

fakeモードの利用例: サーバのメンテナンス情報の登録

ここでは通常だと手動で発生させるのが難しい、サーバのメンテナンス情報の登録をfakeモードで行なってみます。

さくらのクラウドでのメンテナンス情報とは?

さくらのクラウドではサーバが稼働しているホストの不調などでサーバの再起動が必要になる場合があります。 (再起動することで別のホストで稼働させる)

メンテナンスは通常事前に通知され、期限までにユーザーが任意のタイミングでサーバの再起動を行うことが可能となっています。 また、メンテナンス情報はウェブサイト上で公開されているほか、APIからも参照できるようになっています。

これを利用してsakuracloud_exporterではメンテナンス情報を取得するためのメトリクスを提供しており、これらを利用して独自に監視/通知の仕組みを整えることができます。 (詳しくは以下で紹介させていますので参照ください)

medium.com

このメンテナンス情報ですが、当然ながらメンテナンスを任意に発生させるというのは困難です。
たまたまメンテナンス対象のサーバが手元にあればよいですが、そうでない場合がほとんどだと思います。

このためPrometheusでメンテナンス情報を監視/通知する仕組みを用意しても動作確認がすぐには出来ません。 これは困りますよね。

このような場面でfakeモードが役に立ちます。

ということでfakeモードでのexporterの起動〜メンテナンス情報の登録までをやってみます。

fakeモードを有効にしてexporterを起動

まずはfakeモードでexporterを起動してみます。 ステートファイルは先ほど紹介したGitHub上に公開されている例を用います。

# exampleステートファイルをGitHubからダウンロード
$ curl -sLO https://raw.githubusercontent.com/sacloud/sakuracloud_exporter/0.7.0/examples/fake/generate-fake-store-json/example-fake-store.json

# fake-modeオプションを指定してexporterを起動
$ sakuracloud_exporter --fake-mode example-fake-store.json

Prometheusのコンソールからサーバの情報を参照(sakuracloud_server_info)すると2台のサーバが存在することが確認できるはずです。

f:id:febc_yamamoto:20190806094126p:plain

ID: 100000000022と ID: 100000000035の2台ですね。

また、メンテナンス情報は sakuracloud_server_maintenance_infoで確認できます。

f:id:febc_yamamoto:20190806094434p:plain

ID:100000000035にはすでにメンテナンス情報が登録されていますね。 これはメンテナンス確認用として最初から登録されているやつです。 単にメンテナンス情報としてどのようなメトリクスが参照できるか確認したいだけの場合はこちらを利用すればOKです。

今回はメンテナンス情報の登録されていないID: 100000000022の情報を操作してメンテナンス情報を登録してみます。

ステートファイルを操作してメンテナンス情報を登録

次にステートファイルexample-fake-store.jsonをエディタなどで修正してメンテナンス情報を登録します。 JSON内のID: 100000000022のサーバの部分を探し出し、InstanceHostInfoURLというフィールドにhttp://support.sakura.ad.jp/mainte/mainteentry.php?id=26595という値を設定します。

JSONは以下のようになっているはずです。

// 該当部分だけ抜粋
    {
        "ID": "100000000022",
        "InstanceHostInfoURL": "http://support.sakura.ad.jp/mainte/mainteentry.php?id=26595",

ファイルを編集して保存するとexporterにより自動で再読み込みが行われます。 その後(Prometheusによるスクレイピングの後に)Prometheusのコンソールで再度メンテナンス情報を確認するとID: 100000000022のサーバにもメンテナンス情報が登録されたことが確認できます。

f:id:febc_yamamoto:20190806095559p:plain

これでメンテナンス情報を登録することができました。 逆にメンテナンス情報のクリアもできますのでアラートルールの確認などに便利にお使いいただけると思います。

終わりに

sakuracloud_exporter v0.7.0の紹介でした。 徐々に便利になってきてますのでぜひご利用&フィードバックいただけると嬉しいです。

以上です。

入門 Prometheus ―インフラとアプリケーションのパフォーマンスモニタリング

入門 Prometheus ―インフラとアプリケーションのパフォーマンスモニタリング

【小ネタ】Terraformで子リソースに分割されたリソース定義をdynamic blockを使って書き直す例

GitHubで質問もらったやつが例として手頃だったのでブログ書いておきました。

github.com

やりたいこと

Terraform v0.11以前までの親リソース/子リソースに分けて定義していたリソースをv0.12で導入されたdynamic blockに書き直す

v0.11以前

ここではrke_clusterリソースの子リソースとしてdata rke_node_parameterを定義しています。

# locals to allow us to control which nodes get assigned manager roles vs. controlplane and workers
locals {
  mgr_roles    = ["controlplane","etcd"]
  worker_roles = ["worker"]
}

# rke_clusterリソースに対する子リソース(node)の定義
data rke_node_parameter "nodes" {
  count   = "${var.NODE_COUNT}"
  address = "${azurestack_public_ip.vmpip.*.ip_address[count.index]}"
  user    = "myuser"
  role    = "${split(",", count.index < var.nbr_managers ? join(",", local.mgr_roles) : join(",", local.worker_roles))}"
  ssh_key = "${file("path/to/sshkey")}"
}

resource rke_cluster "cluster" {
  depends_on = ["azurestack_public_ip.vmpip", "azurestack_virtual_machine.vm"]

  # 子リソースの定義
  nodes_conf = ["${data.rke_node_parameter.nodes.*.json}"]
}

v0.12のdynamic blockを使って書き直し

子リソースdata rke_node_parameterrke_clusternodesにdynamic blockを使って書き直してます。

# locals to allow us to control which nodes get assigned manager roles vs. controlplane and workers
locals {
  mgr_roles    = ["controlplane","etcd"]
  worker_roles = ["worker"]
}

resource rke_cluster "cluster" {
  depends_on = ["azurestack_public_ip.vmpip", "azurestack_virtual_machine.vm"]

  dynamic nodes {
    for_each = azurestack_public_ip.vmpip.*
    content {
      address = nodes.value.ip_address
      user    = "myuser"
      role    = split(",", nodes.key < var.nbr_managers ? join(",", local.mgr_roles) : join(",", local.worker_roles))
      ssh_key = file("path/to/sshkey")
    }
  }
}

ポイント

まずはこちらのドキュメントには目を通しておきましょう。

www.terraform.io

その上で書き換えのポイントとしては、

  • for_eachにはvariable以外にもresourceやdataも指定できる
  • for_eachにリスト要素を指定した場合、contentブロック内で<element>.keyとすればインデックスを、<element>.valueで値を参照できる

あたりでしょうか。

contentブロック内でイテレート対象の値を参照する際は<element>.valuevalueをつける必要がある点に注意です。(よく間違える)

なお、書き直しするにはプロバイダー側で親リソース内に子リソースの定義を書けるようになっていることが必要です。
(例: azurerm_virtual_machineリソースのstorage_data_diskみたいにブロックでもリソースでも定義できるようになっていること)

終わりに

この類の書き換えはterraform 0.12upgradeで自動置き換えできないので手動で書き換える必要があります。 (書き換える必要があるかは別問題として)

dynamic blockを使えばより簡潔に書ける or 処理が早くなる(反映処理時のロックが不要になったり)などの恩恵を受けられる可能性もありますので使いすぎに注意しつつどんどん使いましょう

以上です。

さくらのクラウド: k3OSパブリックアーカイブなら手軽にRioが使える

f:id:febc_yamamoto:20190627152947p:plain

k3OSがパブリックアーカイブ

さくらのクラウドにk3OSのパブリックアーカイブが追加されましたね!

cloud-news.sakura.ad.jp

ISOイメージと違い、パブリックアーカイブであれば「ディスクの修正」という機能でIPアドレスの設定やSSH公開鍵の設定が行えるようになります。

これを使えば非常に手軽にk3sによるKubernetes環境が手に入りますね。

さらにk3sが動くということはRioが動くということです。
Rioが動かせればKnative/Istio/Prometheus/Grafana/Kiali/BuildKit/イメージレジストリなどを内包していますのでこれらも使えるようになります。

(Rioについては以下スライドを参照ください)

speakerdeck.com

ということで早速試してみました。

パブリックアーカイブからk3OSをインストールしてみる

早速k3OSパブリックアーカイブを用いてサーバ作成を行ってみます。

CLIであるUsacloudは本日リリースされたv0.25.0からk3OSに対応しましたのでこちらを利用します。

github.com

$ export PASSWORD=your-password
$ usacloud server build --os-type k3os --core 2 --memory 4 --password "$PASSWORD" --name k3os -y

もちろんコンパネから作成してもOKです。

SSHで接続

作成したらSSHで接続してみます。rancherユーザーで接続できるはずです。

$ usacloud server ssh --user rancher k3os

接続できたら各種コンポーネントが動いているのが確認できるはずです。

$ kubectl cluster-info

$ kubectl get cs

これでKubernetesが使えるようになりましたね。

Rioのダウンロード

次にRioをインストールします。まずrioコマンドをダウンロードします。

$ sudo curl -L -o /usr/local/bin/rio https://github.com/rancher/rio/releases/download/v0.1.1/rio-linux-amd64
$ sudo chmod +x /usr/local/bin/rio

これでrioコマンドが使えるようになります。

rio installの実行

次にRioの各コンポーネントKubernetes上にデプロイします。

$ rio install

しばらく待つと以下のようなメッセージが表示されデプロイが完了するはずです。

Welcome to Rio!

Run `rio run https://github.com/rancher/rio-demo` as an example

Rioを動かしてみる

まずはRioの各コンポーネントがデプロイされているか確認してみます。 確認はrio -s psを用います。 (-sまたは--systemオプションはRioのシステムコンポーネントを表示するためのオプションです。)

$ rio -s ps

手元の環境では以下のように表示されました。

f:id:febc_yamamoto:20190627150633p:plain

動いているようですね!GrafanaとKialiについては表示されたURLをブラウザで開けるようになってます。 (Grafana/Kialiともユーザー名とパスワードはadminになってます)

f:id:febc_yamamoto:20190627150822p:plain

f:id:febc_yamamoto:20190627151011p:plain

あとはrio runなどをお好みで試してみましょう!

片付け

使い終わったら不要な課金を防ぐために以下のコマンドでサーバの削除を忘れずに行っておきましょう。

$ usacloud server rm -f k3os

終わりに

k3OS+パブリックアーカイブで非常に手軽にKubernetes環境を使えるようになりましたね!
k3sが動けばRioのようなKubernetes上で動くプロダクトもちゃんと使えるはずです。 夢が広がりますね!!

ぜひお試しください!

以上です。

UsacloudでエンハンスドロードバランサのLet's Encrypt設定を行う

昨日の記事ではTerraformでエンハンスドロードバランサのLet's Encrypt設定を行えるようになったことを紹介しました。

febc-yamamoto.hatenablog.jp

昨日に続き、本日Usacloud v0.24.0がリリースされ、UsacloudにてエンハンスドロードバランサのLet's Encrypt設定が行えるようになりました。

github.com

本日はUsacloud v0.24で行われたエンハンスドロードバランサ関連の機能拡充について紹介します。

Usacloud v0.24.0

v0.24.0では以下のようなエンハンスドロードバランサ関連の機能拡充が行われました。

  • 100/500CPS対応
  • HTTPSへのリダイレクト対応
  • HTTP/2のサポート
  • Let's Encrypt対応

100/500CPS対応

enhanced-load-balancer createまたはplan-changeで100/500CPSプランを指定可能となりました。

$ usacloud enhanced-load-balancer create --plan 100 ...

HTTPSへのリダイレクト対応

公開ポートの設定時にリダイレクトの有効化ができるようになりました。

$ usacloud enhanced-load-balancer bind-port-add --mode http --port 80 --redirect-to-https <ID or Name>

--modeがhttpの時のみ指定可能ですのでご注意ください。

HTTP/2のサポート

公開ポートの設定時にリダイレクトの有効化ができるようになりました。

$ usacloud enhanced-load-balancer bind-port-add --mode https --port 443 --support-http2 <ID or Name>

こちらは--modehttpsの時のみ指定可能ですのでご注意ください。

Let's Encrypt対応

Let's Encrypt設定の有効化/証明書取得が行えるようになりました。

# Let's Encrypt設定の有効化
$ usacloud enhanced-load-balancer acme-setting --accept-tos --common-name www.example.com <ID or Name>

# 証明書の取得/更新
$ usacloud enhanced-load-balancer acme-renew <ID or Name>

# 証明書の確認
$ usacloud enhanced-load-balancer cert-info <ID or Name>

Let's Encrypt設定を有効化するには--accept-tosというフラグを指定する必要がある点に注意してください。
これはLet's Encrypt側の利用規約に同意するというフラグで、明示的に指定しないと有効化出来ないようにしています。

また、証明書発行対象のコモンネームが名前解決できるようになっている必要があります。

この辺りの詳細はさくらのクラウドのマニュアルを参照してください。

manual.sakura.ad.jp


ということでUsacloudでのエンハンスドロードバランサ関連機能拡充について紹介しました。 Terraformと上手く使い分けてみてくださいね。

以上です。

TerraformでさくらのクラウドのエンハンスドロードバランサのLet's Encrypt設定を行う

Terraform for さくらのクラウド v1.13.0リリース

本日リリースのTerraformさくらのクラウド向けプロバイダー v1.13.0にてエンハンスドロードバランサ関連の機能拡充が行われました。

  • 100/500CPSプランのサポート
  • HTTPSへのリダイレクト機能
  • HTTP/2のサポート
  • Let's Encrypt設定

Release v1.13.0 · sacloud/terraform-provider-sakuracloud · GitHub

新プラン/HTTPSへのリダイレクト/HTTP2のサポート

これらについては以下のようなtfファイルで利用可能となっています。

resource "sakuracloud_proxylb" "foobar" {
  name = "terraform-test-proxylb-acme"

  # 100CPSプランを利用
  plan = 100

  vip_failover = true
  health_check {
    protocol = "http"
    delay_loop = 10
    host_header = "usacloud.jp"
    path = "/"
  }

  bind_ports {
    proxy_mode = "http"
    port       = 80

    # HTTPSへのリダイレクトを有効化
    redirect_to_https = true
  }
  bind_ports {
    proxy_mode = "https"
    port       = 443

    # HTTP/2サポートの有効化
    support_https = true
  }
  servers {
      ipaddress = "${sakuracloud_server.server01.ipaddress}"
      port = 80
  }
}

Let's Encryptの利用

Let's Encryptを利用するには新設されたリソースsakuracloud_proxylb_acmeを利用する必要があります。

# エンハンスドロードバランサでのLet's Encrypt設定
resource sakuracloud_proxylb_acme "foobar" {
  proxylb_id = sakuracloud_proxylb.foobar.id
  accept_tos = true # 規約への同意
  common_name = "www.example.com"
  update_delay_sec = 120
}

注意点としてはさくらのクラウドのマニュアルに記載の条件を満たす必要があるという点があります。

f:id:febc_yamamoto:20190625160913p:plain

manual.sakura.ad.jp

  • 待ち受けポート(bind_ports)を適切に設定すること
  • 対象のコモンネーム(FQDN)がエンハンスドロードバランサのVIP or FQDNに向いていること

2番目の条件である、DNSレコードの設定を含めたtfファイルの例は以下の通りです。

# エンハンスドロードバランサの定義
resource "sakuracloud_proxylb" "foobar" {
  name = "terraform-test-proxylb-acme"
  plan = 100
  vip_failover = true
  health_check {
    protocol = "http"
    delay_loop = 10
    path = "/"
  }

  # Let's Encryptを利用するにはhttp/https両方のbind_portsが必要
  bind_ports {
    proxy_mode = "http"
    port       = 80

    redirect_to_https = true
  }
  bind_ports {
    proxy_mode = "https"
    port       = 443

    support_https = true
  }
  servers {
      ipaddress = "${sakuracloud_server.server01.ipaddress}"
      port = 80
  }
}

# エンハンスドロードバランサでのLet's Encrypt設定
resource sakuracloud_proxylb_acme "foobar" {
  proxylb_id = sakuracloud_proxylb.foobar.id
  accept_tos = true # 規約への同意
  common_name = "www.example.com"
  update_delay_sec = 120
}

resource sakuracloud_server "server01" {
  name = "terraform-test-server01"
  graceful_shutdown_timeout = 10
}

# エンハンスドロードバランサのVIP/FQDNを解決するためのDNSレコード設定
data sakuracloud_dns "zone" {
  name_selectors = ["example.com"]
}

resource "sakuracloud_dns_record" "record" {
  dns_id = data.sakuracloud_dns.zone.id
  name   = "www"
  type   = "CNAME"
  value  = "${sakuracloud_proxylb.foobar.fqdn}."
  ttl    = 10
}

この例ではエンハンスドロードバランサのFQDNを証明書発行対象のFQDNのCNAMEとして登録しています。

注意点として、DNSレコード登録直後は証明書発行対象のFQDNが解決できない場合があります。 このためにsakuracloud_proxylb_acmeにはupdate_delay_secという項目を設けています。
これはリソース作成時に指定秒数待つための項目です。手元の環境では120秒くらいに設定すると上手くいっていました。

終わりに

TerraformからLet's Encryptの設定〜設定を行うためのレコードの登録まで一括して行えるのは非常に楽だと思います。
ぜひご活用ください。

以上です。

ConftestでOpenPolicyAgent/Regoを使いTerraformのコードにポリシーを適用してみる

今日はConftestを用いてTerraformでのインフラコードにポリシーを適用してみます。

TerraformでのインフラコードのUnitTest

terraform validateでの構文チェック

Terraformではtfファイルの構文チェックを行ってくれるterraform validateコマンドが提供されています。 実行するとtfファイルの構文誤りやパラメータ名間違いなどを検出してくれます。

$ terraform validate

Error: Unsupported argument ← パラメータ名間違い

  on test.tf line 6, in data "sakuracloud_server" "server":
   6:   name_selectorsa = ["sakura-dev"]

An argument named "name_selectorsa" is not expected here. Did you mean
"name_selectors"?

これを利用すれば最低限tfファイルとして正しく書けているかのテストが行えます。

特にTerraform v0.12からは以下のように変数に型情報を与えることが出来るようになっており、モジュール利用時などにより厳密なチェックが行えるようになりました。

# var.nodesはaddressとuserというフィールドを持つオブジェクトのリストしか指定できない
variable "nodes" {
  type = list(object({
    address = string,
    user    = string,
  }))
}

ポリシーの適用

テストの際、terraform validateでの構文チェック以外にも様々な制約を課したい/ポリシーを適用したいことが多々あります。

例えば、

といった場合です。

Terraform EnterpriseであればSentinelを用いてポリシーの適用が行えるのですが、OSS版の場合、現時点ではSentinelを利用できません。

そこで今回はConftestを用いてポリシーの適用を行ってみました。

Conftest

Conftestについてはこちらの記事が詳しいです。

kenfdev.hateblo.jp

ConftestはYAMLあるいはJSONで定義された設定ファイルに対してテストを書けるというツールです。
面白いのは、テストに使うのがOpen Policy AgentのRegoというポリシー用の言語だという点です。

ConftestでOpenPolicyAgent/Regoを用いてポリシーを定義しておいてCIでConftestを実行すればポリシーの適用/強制ができそうです。

ConftestのリポジトリにはTerraformのコードをテストする例がありますのでそちらを参考にポリシーを書いてみます。

github.com

conftestでのterraformインフラコードのテスト

Conftestはコマンドラインツールとなっており、以下のように実行することでYAML/JSONファイルのテストが行えます。

$ conftest test <file>

デフォルトではカレントディレクトリのpolicyディレクトリ配下を参照するようになっていますのでこちらにポリシーファイル(.rego)を作成していきます。 (この挙動は-pまたは--policyオプションで変更可能です)

tfファイルをどうやってテストするの?

TerraformでのインフラコードはJSONでも記載できますが、通常はHCL(.tf)で記載していると思います。 conftestはHCLを読んでくれませんのでどうにかしてJSON/YAMLに変換する必要があります。

ConftestのリポジトリにあるTerraformの例ではplanファイルを出力した上でterraform showJSON出力オプションを指定して実行することでtfファイルを間接的にテストするという方法を取っています。

例えば、以下のようなtfファイルがある場合、

resource sakuracloud_server "server" {
  name   = "example"
  core   = 1
  memory = 1
}

これを元にplanファイル生成〜terraform showJSON出力すると以下のようになります。

# planファイルを出力
$ terraform plan --out plan.tfplan

# terraform showをJSONで出力
$ terraform show -json plan.tfplan | jq .
{
  "format_version": "0.1",
  "terraform_version": "0.12.1",
  "planned_values": {
    "root_module": {
      "resources": [
        {
          "address": "sakuracloud_server.server",
          "mode": "managed",
          "type": "sakuracloud_server",
          "name": "server",
          "provider_name": "sakuracloud",
          "schema_version": 1,
          "values": {
            "additional_nics": null,
            "commitment": "standard",
            "core": 2,
            "description": null,
            "disable_pw_auth": null,
            "graceful_shutdown_timeout": 60,
            "hostname": null,
            "icon_id": null,
            "interface_driver": "virtio",
            "memory": 4,
            "name": "test",
            "nic": "shared",
            "note_ids": null,
            "password": null,
            "private_host_id": null,
            "ssh_key_ids": null
          }
        }
      ]
    }
  },
  "resource_changes": [
    {
      "address": "sakuracloud_server.server",
      "mode": "managed",
      "type": "sakuracloud_server",
      "name": "server",
      "provider_name": "sakuracloud",
      "change": {
        "actions": [
          "create"
        ],
        "before": null,
        "after": {
          "additional_nics": null,
          "commitment": "standard",
          "core": 2,
          "description": null,
          "disable_pw_auth": null,
          "graceful_shutdown_timeout": 60,
          "hostname": null,
          "icon_id": null,
          "interface_driver": "virtio",
          "memory": 4,
          "name": "test",
          "nic": "shared",
          "note_ids": null,
          "password": null,
          "private_host_id": null,
          "ssh_key_ids": null
        },
        "after_unknown": {
          "additional_display_ipaddresses": true,
          "cdrom_id": true,
          "disks": true,
          "display_ipaddress": true,
          "dns_servers": true,
          "gateway": true,
          "id": true,
          "ipaddress": true,
          "macaddresses": true,
          "nw_address": true,
          "nw_mask_len": true,
          "packet_filter_ids": true,
          "private_host_name": true,
          "tags": true,
          "vnc_host": true,
          "vnc_password": true,
          "vnc_port": true,
          "zone": true
        }
      }
    }
  ],
  "configuration": {
    "root_module": {
      "resources": [
        {
          "address": "sakuracloud_server.server",
          "mode": "managed",
          "type": "sakuracloud_server",
          "name": "server",
          "provider_config_key": "sakuracloud",
          "expressions": {
            "core": {
              "constant_value": 2
            },
            "memory": {
              "constant_value": 4
            },
            "name": {
              "constant_value": "test"
            }
          },
          "schema_version": 1
        }
      ]
    }
  }
}

これをconftestコマンドに読ませることでテストを行います。

ポリシーの作成

今回は試しにサーバのコア数は2以上、メモリは4GB以上を指定しないといけないというポリシーにしてみます。

ポリシーファイルは以下の内容でpolicy/instance_type.regoというファイルを作成します。

package main

# コア数は2以上であること
deny[msg] {
  resource := input.resource_changes[index]
  resource.type == "sakuracloud_server"
  resource.change.after.core < 2
  msg = "sakuracloud_server.core must be greater than 2"
}

# メモリは4GB以上であること
deny[msg] {
  resource := input.resource_changes[index]
  resource.type == "sakuracloud_server"
  resource.change.after.memory < 4
  msg = "sakuracloud_server.memory must be greater than 4"
}

テスト実行(ポリシー適用)

それでは早速conftestでテストを実行(ポリシーを適用)してみます。

Makefileの作成

プランファイルの出力~JSONへの変換は毎回コマンド入力するのも大変なのでMakefileでまとめておきます。

NAME := myproject

COMMAND := terraform
PLAN = $(NAME)-plan.tfplan
SHOW = $(NAME)-show.json
CODE = $(NAME).tf


all: test

plan: $(PLAN)

$(PLAN): $(CODE)
    $(COMMAND) plan -out $(PLAN)

show: $(SHOW)

$(SHOW): plan
    $(COMMAND) show -json $(PLAN) > $(SHOW)

test: show
    cat $(SHOW) | conftest test -

clean:
    @rm -f $(PLAN) $(SHOW)

.PHONY: plan show test all clean

これでmakeするだけでテスト可能になります。 なお、このMakefileだと中間ファイルが作成されますので必要に応じてcleanを実行したり.gitignoreに追記したりしておいてください。

実行

現時点でのtfファイルは以下の通りです。

resource sakuracloud_server "server" {
  name   = "example"
  core   = 1
  memory = 1
}

ポリシーである2コア以上かつ4GBメモリ以上に反しているのでエラーとなるはずです。

$ make

# [中略]
cat example-show.json | conftest test -
   sakuracloud_server.core must be greater than 2
   sakuracloud_server.memory must be greater than 4
make: *** [test] Error 1

エラーになりましたね!

今度はtfファイルを修正して実行してみます。

resource sakuracloud_server "server" {
  name   = "example"
  core   = 2
  memory = 4
}
# [中略]

cat example-show.json | conftest test -

今度はエラーとなりませんでしたね。

あとは必要に応じてポリシーを充実させ、CIに組み込めば良さそうです。

終わりに

Open Policy Agent/Regoを初めて使ってみましたがなかなか面白いですね。 TerraformのようなインフラコードのテストでShift left testing出来ると効率がグッと上がりますのでどんどん活用していきたいです。

以上です。

参考情報

TerraformでのプロビジョニングにVNCを使う

TerraformでVNCでのプロビジョニングを行えるようにするプラグインterraform-provisioner-vncを公開しました。

github.com

Terraformでのプロビジョニング

Terraformではリソースの初期設定や削除時のクリーンアップ処理などを行えるようにプロビジョナーという仕組みが用意されています。

www.terraform.io

Terraform v0.12.1の時点では以下のようなプロビジョナーが利用可能となっています。

  • chef Provisioner
  • file Provisioner
  • habitat Provisioner
  • local-exec Provisioner
  • remote-exec Provisioner
  • salt-masterless Provisioner

SSH/WinRMが利用できないリソースのプロビジョニングは?

プロビジョナーのうちlocal-exec以外はSSHまたはWinRM(SSHのみの場合もある)でリモートに接続してなんらかの処理を行うようになっています。 これはSSH/WinRMが利用できないリソース、例えば初期状態ではリモート接続を許可していないようなリソースではこれらを利用できないということです。

この問題の解決方法の一つとして、VNC経由でコマンドを実行するのが今回公開したterraform-provisioner-vncです。 Packerでいうboot_commandを実行するものとなっており、主にSSH/WinRM出来るまでの環境を整えることを目的としています。

terraform-provisioner-vncの使い方

通常のプロビジョナーと同じく、対象のリソースやnull_resourceにprovisionerブロックを記載することで利用します。 例えばさくらのクラウドのサーバだと以下のように使います。

resource sakuracloud_server "example" {
  name     = "example"
  # ...中略

  provisioner vnc {
    host      = self.vnc_host
    port      = self.vnc_port
    password  = self.vnc_password
    boot_wait = "10s"

    # ここに実行したいコマンドを記載していく
    inline = [
      "<tab><wait>",
      "text ks=http://${var.ip}:${var.port}/anaconda-ks.cfg<enter>",
    ]
  }
}

remote-execと同じく、inline(文字列リストで指定)、script(スクリプトファイルのパスで指定)、またはscripts(ファイルパスのリストで指定)で実行するスクリプトを指定できます。
記載できる内容はPackerでのboot_commandと互換性があり、<enter>などの特殊キーもサポートしています。(入力処理はPackerそのものを利用してます)

指定できるスクリプトについてはPackerのQEMU Builderのboot_commandについてのドキュメントを参照してください。

www.packer.io

terraform-provisioner-vncのインストール

リリースページからバイナリファイルをダウンロードし~/.terraform.d/plugins配下に配置すればOKです。

利用例

利用例として、さくらのクラウド上のサーバでk3OSを起動〜ディスクへのインストールまでを行う例を挙げておきます。

参考: k3os on さくらのクラウド - febc技術メモ

k3OSをISOイメージから起動した場合、公開鍵を登録する or rancherユーザーのパスワードを設定するまでSSH接続できません。
このため、VNCプロビジョナーを用いてOSのインストール処理〜GitHubから公開鍵の取得/登録などを行います。
OSのインストール後はSSHプロビジョナーで接続して任意のプロビジョニングが実行できます。 この例ではkubectlコマンドを実行してみています。

variable server_name {
  default = "example"
}

variable github_username {
  default = "yamamoto-febc"
}

provider sakuracloud {
  zone = "is1b"
}

data sakuracloud_cdrom k3os {
  name_selectors = ["k3OS"]
}

resource sakuracloud_disk "disk" {
  name = var.server_name
}

resource sakuracloud_server "k3os" {
  name     = var.server_name
  core     = 2
  memory   = 4
  disks    = [sakuracloud_disk.disk.id]
  cdrom_id = data.sakuracloud_cdrom.k3os.id

  # VNC経由でOSのインストール処理
  provisioner vnc {
    host      = self.vnc_host
    port      = self.vnc_port
    password  = self.vnc_password
    boot_wait = "40s"

    inline = [
      "rancher<enter>",
      "<wait5>",

      "sudo os-config<enter>",
      "<wait5>",

      "<enter>",
      "<wait5>",

      "<enter>",
      "<wait5>",

      "y<enter>",
      "<wait5>",

      "${var.github_username}<enter>", # terraformの$記法も使えるしコメントも書ける
      "<wait10s>",

      "<enter>",
      "<wait5>",

      "<enter>",
      "<wait5>",

      "<enter>",
      "<wait10s>",

      "y<enter>",
      "<wait5>",
    ]
  }

  # VNCでプロビジョニング後はSSHで接続してプロビジョニング
  connection {
    type        = "ssh"
    host        = self.ipaddress
    user        = "rancher"
    private_key = file("/Users/kaz/.ssh/id_rsa")
    script_path = "/home/rancher/bootstrap.sh"
  }
  provisioner "remote-exec" {
    inline = [
      "echo ${self.ipaddress} `hostname` | sudo tee -a /etc/hosts", # /etc/hostsに自身のホスト名を追記
      "kubectl cluster-info",
      "kubectl get cs",
      "kubectl version",
    ]
  }
}

終わりに

使い所は限られますが、Packerを使うまでもないちょっとしたコマンドを実行しておきたいケースに便利だと思います。

なお、TerraformでのCustom Provisionerの仕組みはこちらにコメントされているように将来的に大きく変わる可能性もある点は注意しておいてください。
(利用する側としてはあまり関係ないかもしれませんが)

github.com

以上です。