【さくらのクラウド】アーカイブを利用したバックアップ取得
平成30年北海道胆振東部地震で被災された皆さまに謹んでお見舞い申し上げます。
今回の震災では石狩リージョン(石狩第1/第2ゾーン)において非常用発電機設備を起動・停止するための制御回路の故障により専用サーバの一部に影響が出ていたとのことですが、
クラウドをはじめとしたその他のサービスには影響は出なかった模様です。
とはいえ一時は石狩データセンターの自家発電設備により稼働しているという状態になっていました。
現在は北海道電力からの電力供給が復旧したとのことですが、仮に非常用発電設備の燃料供給が滞ったり商用電源の復旧が遅れた場合はサービスの停止といった影響が出ていた可能性もあります。
なので、これを機に利用しているクラウド上のサービスのBCP/DRといった運用について再確認をされる方も多いと思います。
この記事ではその一環として、さくらのクラウド上で稼働しているサーバのディスクを ローカルマシンや石狩以外のサーバにバックアップするための方法をご紹介いたします。
バックアップ方法あれこれ
さくらのクラウドにてディスクのバックアップを取得するにはいくつかの方法があります。
サーバ内のデータ領域などを自前でバックアップする方法については定期的にデータベースのダンプを取得する/静的ファイルは冗長化されたストレージに格納するなど様々な方法がありますが、環境に応じて方法が異なり一概にどの方法が良いとは言いにくい面があります。 このため、この記事では比較的汎用的に適用できる、2番目のさくらのクラウドが提供しているアーカイブ機能を中心に扱います。
アーカイブ機能
さくらのクラウドでのアーカイブ機能とは、仮想ディスクからバックアップを作成する機能
のことを指します。
現在稼働中のサーバのディスクのイメージバックアップを取得する機能ですね。
取得したアーカイブはサーバ作成時のソースとして用いる他に、FTPSを用いてダウンロードしたり、他ゾーンにコピーしたりすることができます。
また、アーカイブはサイズと保存日数により課金されるようになっています。
2018/09時点でのアーカイブ価格
アーカイブをどこに保存するか
取得したアーカイブの保存先候補としては大きく分けると以下のようなものがあります。
- 作成元ディスクと同一ゾーン内に保存
- 他ゾーンにコピーして保存
- ローカルマシンなどにダウンロードして保存
それぞれに一長一短があります。
保存先 | 長所 | 短所 |
---|---|---|
同一ゾーン | - コピー\転送の時間がないので高速 - 後述する 自動バックアップ 機能が利用可能 |
- ゾーン\リージョン障害の場合に利用できない可能性 |
他ゾーンにコピー | - ゾーン\リージョン障害時に影響を受けにくい | - 作成時にコピー\転送分の時間がかかる - 作成はコントロールパネルから行う必要がある(API非対応) |
ローカルマシンなどにダウンロード | - プラットフォーム障害の影響を受けにくい - ダウンロード後にアーカイブを削除することでコストを抑えられる |
- ダウンロードに時間がかかる - 復元時に再度アップロードする時間がかかる |
実際にはそれぞれをバランスよく組み合わせて利用することになるかと思います。
次に保存先ごとにアーカイブ取得方法について見ていきます。
アーカイブ取得方法
作成元ディスクと同一ゾーンに保存
最も単純な方法です。コントロールパネルやCLIから作成元ディスクを指定してアーカイブを作成します。
コントロールパネルの場合
アーカイブ作成画面にて作成元ディスクを指定して作成します。
この方法は自動バックアップ
というさくらのクラウドの機能を利用して自動化することも可能です。
詳細は以下のドキュメントを参照してください。
CLIの場合
さくらのクラウド CLI Usacloudでも取得可能です。 Usacloudのセットアップ(APIキー設定など)をあらかじめ行っておけば以下のように取得できます。
# アーカイブ作成 $ usacloud archive create --source-disk-id <ディスクID> --name <作成するアーカイブの名称>
他ゾーンにコピーして保存
次に他ゾーンにコピーする方法です。
ディスクのアーカイブ作成は同一ゾーンに対してのみ行えるようになっています。
このため、まずは 作成元ディスクと同一ゾーンに保存
の手順でアーカイブを作成し、その後そのアーカイブを他ゾーンにコピーするという手順になります。
なお、アーカイブを他ゾーンにコピーする方法はコントロールパネルでのみ可能です。APIやCLIからは実行できませんのでご注意ください。
作成自体は簡単で、アーカイブ作成画面にてコピー元アーカイブのゾーンとアーカイブを選択して作成するだけです。
ただ、一時的なものだと思いますが、他ゾーンにコピーする処理が混雑しているのか、石狩->東京への転送が503エラーとなることがありました。
この場合、少し時間を置いて試すか、次に紹介する ローカルマシンなどにダウンロードして保存
の方法を試すのが良さそうです。
ローカルマシンなどにダウンロードして保存
さくらのクラウドのアーカイブはFTPSによるダウンロードが行えるようになっています。
コントロールパネルやAPIでダウンロードを指示するとFTPS接続情報(ホスト名/ユーザー名/パスワードなど)が発行されます。 これを用いてwinSCPやFileZillaなどのFTPクライアントツールでFTPSによるダウンロードを行います。
ダウンロードするアーカイブはあらかじめ作成しておく必要がありますので、まずは 作成元ディスクと同一ゾーンに保存
の手順でアーカイブを作成しておきます。
コントロールパネルの場合
保存したアーカイブの詳細画面の右上にFTPを開始
というボタンがあります。
これをクリックするとFTPS接続情報が表示されます。
この情報を用いてFTPクライアントツールでダウンロードを行います。
ダウンロード完了後は FTPを完了
ボタンでFTPS接続情報を無効化しておきましょう。
CLIの場合
UsacloudはFTPSクライアント機能を持っています。以下のようにすればアーカイブのダウンロードが可能です。
# この例では /tmp/archive.rawに保存 $ usacloud archive download --file-destination /tmp/archive.raw <アーカイブのID or 名称>
標準入力への出力も対応しているため、そのままgzip
に渡して圧縮も可能です。
$ usacloud archive download -y <アーカイブのID or 名称> | gzip -c > /tmp/archive.raw
参考までに、アーカイブ取得〜ダウンロード〜gzip圧縮までを行うシェルスクリプトを掲載しておきます。
#!/bin/bash # 対象ディスクの名称を設定 SOURCE_DISK_NAME=<ディスク名> # ゾーン ZONE=<対象ゾーン(is1a or is1b or tk1a)> # ===================================== # ディスク名からIDに変換 SOURCE_DISK_ID=`usacloud --zone $ZONE disk read -q ${SOURCE_DISK_NAME}` if [ -z "$SOURCE_DISK_ID" ]; then echo "Disk '${SOURCE_DISK_NAME} is not found' exit fi # アーカイブ作成 ARCHIVE_ID=`usacloud --zone $ZONE archive create -y -q --source-disk-id=${SOURCE_DISK_ID} --name ${SOURCE_DISK_NAME}` # ダウンロード & gzip usacloud --zone $ZONE archive download -y $ARCHIVE_ID | gzip -c > /tmp/${ARCHIVE_ID}.raw.gz # アーカイブ削除 $ usacloud archive delete -y ${ARCHIVE_ID}
オプション: usacloud
+curl
でのダウンロード
以下の記事にあるようにcurl
コマンドを利用することも可能です。
Usacloudと併用すると以下のような感じになります。
# アーカイブのID ARCHIVE_ID=<アーカイブのID> # ftp-openコマンドでFTP接続情報を取得、--formatオプションでcurlコマンドを作りevalする eval $(usacloud archive ftp-open -y --format 'curl -u {{.User}}:{{.Password}} --ftp-ssl --ftp-pasv -k --compressed ftp://{{.HostName}}:21/archive.img -o /tmp/${ARCHIVE_ID}.raw' ${ARCHIVE_ID}) # 終わったらFTPクローズして削除 $ usacloud archive ftp-close -y ${ARCHIVE_ID} $ usacloud archive delete -y ${ARCHIVE_ID}
UsacloudのFTPS処理はあまり最適化してない(すいません…)のでこちらの方が早いかも?と思い試してみましたがあまり変わらずでした。
ちゃんと試したら違いがあるかもしれませんが今回はそこまで調べていません。また機会があれば試してみます。
組み合わせ
アーカイブ取得の各方法は単独で利用するのではなく組み合わせて利用することが多いと思います。
普段のバックアップとして自動バックアップ
を用い、予算に応じて他ゾーンへのコピーやローカルマシンへのダウンロードなどを組み合わせるといった具合です。
各環境に最適な方法をご検討ください。
おまけ:その他のバックアップツール
ググって見たらバックアップツールはUsacloud以外にもちょいちょいありました。
まとめ
この記事では様々なアーカイブ取得方法について紹介しました。
障害はどんなプラットフォームでも避けて通れないものです。 普段から障害を考慮した設計、バックアップ、訓練を行っておきたいものですね。
障害に負けずがんばっていきましょう!!
おまけ: コミュニティによるサポート
さくらのクラウドのユーザーコミュニティSacloud
にはSlackワークスペースがあります。
ツールの使い方をはじめとしたQAなどを行えますので、お困りの際はぜひご参加ください。
さくらのクラウド ユーザーコミュニティSacloud
のSlackワークスペース
以上です。
Terraform for さくらのクラウド 最新情報(2018年10月版)
本日Terraform for さくらのクラウド v1.7をリリースしました。
Terraform for さくらのクラウドはこまめなバージョンアップを行なっており、最新情報を追いきれていない方もいらっしゃるかと思います。
そこで今回は最近追加された機能のサマリーとして影響の大きな変更や新規追加された機能などをピックアップしてご紹介します。
最近追加された/変更された主な機能一覧
最近(直近3ヶ月くらい)で追加/変更された大きなものとして以下のようなものがあります。
サーバ/ディスク関連
アプライアンス関連
その他
他にも細々した変更も行われています。すべての変更内容はCHANGELOG.mdにまとめられています。
順番にご紹介していきます。
サーバ/ディスク関連
サーバ/ディスク新プラン対応
2016年7月から新規申し込み受付を停止していた石狩第1ゾーンが先日再び利用できるようになりました。
これにより石狩第1/石狩第2/東京第1と3つのゾーンが再び利用可能になり、ゾーンをまたぐ冗長構成の構築が容易になりました。
また、これに伴いサーバ/ディスクプランも新しくなりました。
従来プランと比較して値下げされているほか、大容量ディスク(最大4TB)が利用できるようになりました。
Terraform for さくらのクラウドではv1.7以降で新プランに対応しており、v1.7以降でサーバを作成すると自動的に新プランでサーバ作成されるようになっています。
1点ご注意いただきたいのが、Terraform for さくらのクラウドでは旧プランの利用はサポートしていないという点です。
これまで石狩第1ゾーンを利用していた方(2016/7以前から利用しており、かつ石狩第1ゾーンにリソースを作成していた)に限り引き続き旧プランも利用可能なのですが、 Terraform for さくらのクラウドでは新プランにのみ対応しています。 もしTerraformでも旧プランを利用したいという方がいらっしゃいましたらGitHubなどからご要望ください。
ディスクの修正関連のパラメータをサーバリソースに移動
これまでディスクの修正機能で設定する項目についてはディスクリソースにて指定するようになっていました。
これまでのディスクの修正関連パラメータの指定例
data sakuracloud_archive "ubuntu" { os_type = "ubuntu" } # ディスクリソース resource sakuracloud_disk "foobar" { name = "mydisk" source_archive_id = "${data.sakuracloud_archive.ubuntu.id}" # ディスクの修正関連のパラメータ hostname = "myserver" password = "p@ssw0rd" ssh_key_ids = ["100000000000", "200000000000"] disable_pw_auth = true note_ids = ["100000000000", "200000000000"] } # サーバリソース resource sakuracloud_server "foobar" { name = "myserver" disks = ["${sakuracloud_disk.foobar.id}"] }
ホスト名やパスワードといった項目はサーバではなくディスクに紐づいていることからディスクリソース内で設定を行うようにしていましたが、 少々直感的ではなく、どこで設定すべきか迷うことがありました。
また、一部の項目(主にネットワーク関連)についてはディスクとサーバとを接続した後に改めてディスクの修正APIを呼ぶ必要があり、 これまでもディスクリソース/サーバリソースの両方でディスクの修正APIを呼び出していました。
これらの改善のため、ディスクの修正関連パラメータはサーバリソース側で指定するように変更されました。
変更後のディスクの修正関連パラメータの指定例
data sakuracloud_archive "ubuntu" { os_type = "ubuntu" } # ディスクリソース resource sakuracloud_disk "foobar" { name = "mydisk" source_archive_id = "${data.sakuracloud_archive.ubuntu.id}" } # サーバリソース resource sakuracloud_server "foobar" { name = "myserver" disks = ["${sakuracloud_disk.foobar.id}"] # ディスクの修正関連のパラメータ hostname = "myserver" password = "p@ssw0rd" ssh_key_ids = ["100000000000", "200000000000"] disable_pw_auth = true note_ids = ["100000000000", "200000000000"] }
なお、最新版のv1.7では従来通りディスクリソースで指定する方法も引き続きサポートされています。
ただし非推奨となっており将来のバージョンで指定できなくなる予定ですので早めの移行をお勧めします。
移行はtfファイル上に記載しているディスクの修正関連のパラメータをコピペでサーバリソース側に移動するだけでOKです。
(terrafory apply
するタイミングで再度ディスクの修正が行われます。)
アプライアンス関連
続いてアプライアンス関連についてです。
石狩第1ゾーンでの利用可能リソース拡大
これまで石狩第1ゾーンではデータベースアプライアンスやモバイルゲートウェイの利用はできませんでした。
しかし先ほども紹介した石狩第1ゾーンの受付再開に伴いこれらのリソースが利用可能となりました。
これにより、ほぼ全てのリソースが全ゾーンで利用可能な状態となりました。
例外は以下2つです。
- 専有ホスト(東京第1/石狩第2でのみ利用可能)
- 自動バックアップ(現在、石狩第1での受付は停止中)
Terraform for さくらのクラウドではv1.7以降で石狩第1ゾーンを指定可能となっています。
モバイルゲートウェイでのトラフィックコントロール機能
SIMからさくらのクラウドへ直結することでデバイスのセキュアな通信を行えるようにする「セキュアモバイルコネクト」という機能があります。
専用のSIM(Amazonなどで購入可能)を監視カメラやドライブレコーダーといったSIMのさせるデバイスで利用することで、インターネットを経由することなくさくらのクラウドへ直結させることができるようになります。
さくらのセキュアモバイルコネクト マルチSIM(データ通信専用)
- 出版社/メーカー: さくらインターネット株式会社
- メディア: エレクトロニクス
- この商品を含むブログを見る
セキュアモバイルコネクトを利用するにはSIMを購入することに加え、さくらのクラウド側にSIMとの通知の受け口となる「モバイルゲートウェイ」を作成/設定する必要があります。 モバイルゲートウェイは月額6480円/台、SIM一枚あたり月額12円の基本利用料、通信量課金として1MBあたり6円となっています。 (モバイルゲートウェイあたり1万枚のSIMが登録可能、またモバイルゲートウェイには毎月500MBまでのデータ通信量が含まれている)
Terraform for さくらのクラウドではSIMの登録、モバイルゲートウェイの作成/設定、SIMとモバイルゲートウェイの紐付けが行えるようになっています。
SIM/モバイルゲートウェイを利用する例
# SIMの定義 resource sakuracloud_sim "sim" { name = "example-sim" iccid = "<SIMに記載されているICCID>" passcode = "<SIMに記載されているPasscode>" imei = "<端末識別番号(IMEIロックする場合のみ)>" #enabled = true mobile_gateway_id = "${sakuracloud_mobile_gateway.mgw.id}" # 接続するモバイルゲートウェイのID ipaddress = "192.168.100.2" # SIMに割り当てるIPアドレス } # モバイルゲートウェイの定義 resource sakuracloud_mobile_gateway "mgw" { name = "example-mobile-gateway" dns_server1 = "8.8.8.8" # DNSサーバ1 dns_server2 = "8.8.4.4" # DNSサーバ2 }
モバイルゲートウェイでのトラフィックコントロール機能
今月初めにモバイルゲートウェイに「トラフィックコントロール」機能が追加されました。
これは指定した通信量を超えたら速度制限を実施するというようなことができる機能です。
これもTerraformから設定できるようになっています。
# モバイルゲートウェイの定義 resource sakuracloud_mobile_gateway "mgw" { name = "example-mobile-gateway" dns_server1 = "8.8.8.8" # DNSサーバ1 dns_server2 = "8.8.4.4" # DNSサーバ2 # トラフィックコントロール traffic_control = { quota = 256 # 通信量閾値(MB) auto_traffic_shaping = true # 帯域制限 有効/無効 band_width_limit = 64 # 帯域制限値(Kbps) enable_email = true # Emailでの通知 enable_slack = true # Slackでの通知 slack_webhook = "https://hooks.slack.com/services/xxx/xxx/xxx" } }
データベースアプライアンスでのリードレプリカ機能
データベースアプライアンスにてレプリケーションの設定が行えるようになりました。
現在はPostgreSQLでのリードレプリカ(読み取り専用のスレーブデータベース)の作成がサポートされています。
リードレプリカの作成例
# マスターとなるデータベースアプライアンスの定義 resource sakuracloud_database "master" { database_type = "postgresql" # 現在、リードレプリカ作成はPostgreSQLのみ対応 plan = "10g" user_name = "defuser" user_password = "<パスワード>" # レプリケーション用ユーザーのパスワードを指定することでレプリケーションが有効になる replica_password = "<パスワード>" switch_id = "${sakuracloud_switch.sw.id}" ipaddress1 = "192.168.11.101" nw_mask_len = 24 default_route = "192.168.11.1" name = "master" } # リードレプリカの定義 resource sakuracloud_database_read_replica "foobar" { master_id = "${sakuracloud_database.foobar.id}" # マスター側データベースのID ipaddress1 = "192.168.11.111" # IPアドレス # IPアドレス以外のネットワーク関連項目が未指定の場合、マスター側から引き継ぐ #switch_id = "${sakuracloud_switch.sw.id}" #nw_mask_len = 24 #default_route = "192.168.11.1" name = "slave" }
マスター側でレプリケーションを有効にしておく必要がある点に注意が必要です。
レプリケーションを有効にする(かつレプリケーションのマスターとして構成する)には、マスター側でレプリケーション用ユーザーのパスワードを指定すればOKです。
(replica_password
という項目です)
リードレプリカ側ではマスターとなるデータベースアプライアンスのIDを指定します。
IPアドレス以外のネットワーク関連の設定(接続するスイッチやネットワークマスク長、デフォルトルート)は指定しなかった場合はマスター側と同じ値が設定されます。
当然ですがマスター側とネットワーク的に疎通できるようなネットワーク構成となるようにしてください。
シンプル監視でのBasic認証
シンプル監視にてHTTP/HTTPSでの監視時にBasic認証が行えるようになりました。
以下のように利用します。
# HTTP監視の例 resource sakuracloud_simple_monitor "mymonitor" { target = "<監視対象のFQDN or IPアドレス>" health_check = { protocol = "http" delay_loop = 60 path = "/" status = "200" username = "foo" # Basic認証 ユーザー名 password = "bar" # Basic認証 パスワード } notify_email_enabled = true }
その他: Terraform公式サイトのスタイルを用いて英語版ドキュメントをプレビュー
Terraform for さくらのクラウドでは英語版ドキュメントを用意しています。
英語版ドキュメントについてはTerraform公式サイト(terraform.io)と同じスタイルで確認できる仕組みを用意しています。
gitとdockerが利用できればGitHubからソース一式を取得してmake serve-english-docs
コマンドを実行することでプレビューが行えます。
# GitHubからソース一式をクローン $ git clone https://github.com/sacloud/terraform-provider-sakuracloud.git $ cd terraform-provider-sakuracloud # プレビュー # make serve-english-docs
この後http://localhost:4567/docs/providers/sakuracloud
をブラウザなどで開くことでプレビューできます。
終わりに
ということで、今回は最近のTerraform for さくらのクラウドで追加/変更となった機能を紹介しました。
もしご要望や不具合の報告などありましたらぜひGitHubにておしらせください。
以上です。
Rio v0.0.3はKubernetes/Istioをどう利用しているの?
Rio v0.0.3がリリースされましたね。
以前書いたRioの記事ではスタンドアロン版を試しましたので、 今回は既存KubernetesクラスタとしてDocker for Macを利用してRioを試してみようと思います。
前回と重複する内容も含まれますが、今回はRioがKubernetes/Istioをどう利用しているのかといった点を中心にみていきたいと思います。
(前回の記事でIstio周りもうちょい詳しく知りたかったという声もあったような気がしますので)
前回の記事(スタンドアロン版のRio)
必要なもの
- Docker for Mac(Kubernetesが有効化されていること)
おそらくDocker for Windowsでも動くと思いますが手元に環境がないため今回は試してません。
また、大事な注意点として、Rio v0.0.3時点では簡単にアンインストールする手段は提供されていません!!
Docker for Macをファクトリーリセットするか地道に手で一つ一つ消していくくらいしかアンインストール方法がないため、お手元で試される方はご注意ください。
<宣伝>
クラウド上に使い捨てKubernetesクラスタを建てる場合はTerraform+RKEがおすすめです。
Rioのインストール(Mac)
まずは手元のMacにRioをインストールしておきます。
RioはGoで書かれたシングルバイナリですのでダウンロード&展開&PATHの通った場所に配置すればOKです。
#rio v0.0.3のダウンロード $ curl -L -o rio.tgz https://github.com/rancher/rio/releases/download/v0.0.3/rio-v0.0.3-darwin.tar.gz #展開 $ tar zxvf rio.tgz #PATHの通った場所に移動 $ sudo mv rio-v0.0.3-darwin/rio /usr/local/bin/ #確認 $ rio -v # "rio version v0.0.3" と表示されればOK
Rioサーバ側コンポーネントのデプロイ
Rioはスタンドアロン版の場合はrio server
コマンドを実行することでembedなk8s(k3sとか呼ばれてるやつ)を起動してそこに各種コンポーネントをデプロイします。
既存のk8sクラスタを利用する場合はkubectl
の向き先に対してデプロイを行います。今回はこちらを利用します。
kubectl
コマンドの接続先の確認
始める前にkubectl
の向き先がDocker for Macになっているか確認しておいてください。
$ kubectl get nodes NAME STATUS ROLES AGE VERSION docker-for-desktop Ready master 2d v1.10.3
NAMEの部分がdocker-for-desktop
になっていればOKですね。
現時点ではRioのデプロイを行うにはcluster-admin権限が必要です。
この辺は今後改善されるっぽいです。
Rioのコンポーネントのデプロイ
それではrio login
コマンドを実行してみましょう。
$ rio login [1] Connect to remote Rio server [2] Install Rio in existing Kubernetes Select Number [1]
こんな感じで選択肢が表示されます。今回は既存のk8sクラスタにデプロイしますので2
を選択してください。
その後以下のようなログが表示されるはずです。
INFO[0068] Installing Rio INFO[0071] Waiting to connect to Rio INFO[0111] Log in successful
Log in successful
と表示されるまで数分かかるかもしれません。気長に待ちましょう。
(手元のマシンでは1分ほどでした)
Rioを使ってみる
コンテナ起動 & 確認
まずは動かしてみます。
docker run
するような感覚でrio run
を実行することでコンテナの起動が可能です。
ここではDockerHubのnginxイメージを利用してfoobar
というサービスを起動します。
Rioでは「サービス」という概念があります。k8sやDockerでのserviceとは役割が異なり、単なる同じ役割を持つコンテナの集合ということです。 詳細はREADME.mdのServiceセクションを参照してください。
#DockerHubのnginxイメージを用いてコンテナ起動 $ rio run --name foobar nginx default-265db573:foobar # k8s上でのネームスペース:サービス名が表示される #起動しているか確認 $ rio ps NAME IMAGE CREATED SCALE STATE ENDPOINT DETAIL foobar nginx About a minute ago 1 active #foobarサービスのコンテナを確認 $ rio ps foobar NAME IMAGE CREATED NODE IP STATE DETAIL foobar/f6985f578-schpv nginx 2 minutes ago docker-for-desktop 10.1.3.58 running
起動したようですね。 dockerコマンドやkubectlコマンドなどと同じく、コンテナへのattachやexecも可能です。
# bashを実行(--interactive + --ttyオプションを指定 = dockerと同じ) $ rio exec -it foobar bash root@foobar-f6985f578-schpv:/# # 以下のようにサービス名/コンテナ名まで指定してもOK # rio exec -it foobar/f6985f578-schpv bash
次にk8sからはどう見えてるか確認してみます。
kubectlでKubernetes側から確認
# ネームスペースの確認 $ kubectl get namespaces NAME STATUS AGE default Active 2d default-265db573 Active 10m # rio runで作成されたネームスペース docker Active 2d istio-095b8502 Active 15m kube-public Active 2d kube-system Active 2d rio-defaults Active 15m rio-system Active 16m
Rioがいくつかnamespaceを作成してますね。rio run
実行後に表示されたdefault-265db573
というものも見えます。
実際のコンテナはこのネームスペースに配置されています。
それではこのネームスペースに何があるのか確認していきましょう。
実際に手元で試す際はdefault-265db573
の部分をそれぞれの手元で表示されたものに置き換えてください。
$ kubectl get all -n default-265db573 NAME READY STATUS RESTARTS AGE pod/foobar-f6985f578-schpv 2/2 Running 0 14m NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/foobar ClusterIP 10.97.38.245 <none> 80/TCP 14m NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE deployment.apps/foobar 1 1 1 1 14m NAME DESIRED CURRENT READY AGE replicaset.apps/foobar-f6985f578 1 1 1 14m
ServiceとDeploymentが作成され、そこからReplicaSetとPodも生えてますね。
それぞれ少し詳しくみておきます。
# Serviceの確認 $ kubectl get svc -o wide -n default-265db573 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR foobar ClusterIP 10.97.38.245 <none> 80/TCP 19m app=foobar,rio.cattle.io/namespace=default-265db573,rio.cattle.io/service=foobar,rio.cattle.io=true # Deploymentの確認 $ kubectl get deploy -o wide -n default-265db573 NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR foobar 1 1 1 1 20m foobar,istio-proxy nginx,docker.io/istio/proxy_debug:1.0.0 app=foobar,rio.cattle.io=true,rio.cattle.io/namespace=default-265db573,rio.cattle.io/revision=latest,rio.cattle.io/service=foobar # Podの確認 $ kubectl get pod -n default-265db573 NAME READY STATUS RESTARTS AGE foobar-f6985f578-schpv 2/2 Running 0 22m # Podの詳細 $ kubectl describe pod -n default-265db573 foobar-f6985f578-schpv Name: foobar-f6985f578-schpv Namespace: default-265db573 Node: docker-for-desktop/192.168.65.3 Labels: app=foobar pod-template-hash=925419134 rio.cattle.io=true rio.cattle.io/namespace=default-265db573 rio.cattle.io/revision=latest rio.cattle.io/service=foobar Status: Running Init Containers: istio-init: # ... enable-core-dump: # ... Containers: foobar: Image: nginx Port: <none> Host Port: <none> # ... istio-proxy: Image: docker.io/istio/proxy_debug:1.0.0 Mounts: /etc/certs/ from istio-certs (ro) /etc/istio/proxy from istio-envoy (rw) # ... Volumes: istio-envoy: Type: EmptyDir (a temporary directory that shares a pod s lifetime) Medium: Memory istio-certs: Type: Secret (a volume populated by a Secret) SecretName: istio.default Optional: true Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Created 23m kubelet, docker-for-desktop Created container #...
Istio+指定したnginxが起動してますね。
次にRioでサービス内のコンテナをスケールアウトさせてみましょう。
スケールアウトさせてみる
rio scale
コマンドでスケールさせることが出来ます。
$ rio scale foobar=3 default-265db573:foobar #確認 $ rio ps NAME IMAGE CREATED SCALE STATE ENDPOINT DETAIL foobar nginx 38 minutes ago 3 active #詳細確認 $ rio ps foobar NAME IMAGE CREATED NODE IP STATE DETAIL foobar/f6985f578-wbgwj nginx About a minute ago docker-for-desktop 10.1.3.60 running foobar/f6985f578-nrmhk nginx About a minute ago docker-for-desktop 10.1.3.59 running foobar/f6985f578-schpv nginx 39 minutes ago docker-for-desktop 10.1.3.58 running
Kubernetes側もみておきます。
#Deploymentの確認 $ kubectl get deploy -n default-265db573 NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE foobar 3 3 3 3 40m #Podの確認 $ kubectl get pod -n default-265db573 NAME READY STATUS RESTARTS AGE foobar-f6985f578-nrmhk 2/2 Running 0 2m foobar-f6985f578-schpv 2/2 Running 0 41m foobar-f6985f578-wbgwj 2/2 Running 0 2m
きちんとスケールアウトされてました。
サービスの削除
削除はrm
サブコマンドで行います。
#削除 $ rio rm foobar default-265db573:foobar #確認 $ rio ps NAME IMAGE CREATED SCALE STATE ENDPOINT DETAIL
外部からアクセス可能にしてみる(expose相当)
次に起動したサービスを外部からアクセス可能にしてみます。
外部からアクセス可能にするにはrio run
時に-p
(--publish
)オプションを指定します。
(rio edit
などでもOK)
# run時にオプションを指定して80番ポートを公開 $ rio run --name foobar -p 80/http nginx default-265db573:foobar # 確認 $ rio ps NAME IMAGE CREATED SCALE STATE ENDPOINT DETAIL foobar nginx 8 seconds ago 1 active http://foobar.default.127.0.0.1.nip.io
今度はENDPOINTというのが割り当てられましたね。 この辺は前回の記事で説明した通りノードのIPアドレスにを指すようなFQDNが割り振られます。
起動したらブラウザなどでENDPOINTのURLを開いてみましょう。
非常に簡単に公開できましたね。
Kubernetes側からのぞいてみる(外部からのアクセス編)
またKubernetes側を確認してみます。 まずはServiceです。
$ kubectl get svc -n default-265db573 -o wide NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR foobar ClusterIP 10.97.38.245 <none> 80/TCP 1h app=foobar,rio.cattle.io/namespace=default-265db573,rio.cattle.io/service=foobar,rio.cattle.io=true
前と変わってないですね。RioはIstioを使ってますのでそちらの方を確認なければいけませんね。 なおIstioについては以下を読んでおくと以降をスムーズに読めると思います。
# Istio Gatewayの確認 $ kubectl get gateways --all-namespaces NAMESPACE NAME AGE istio-095b8502 rio-gateway 1h # Gatewayの詳細確認 $ kubectl get gateways -n istio-095b8502 rio-gateway -o yaml #適当に省略してます apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: rio-gateway namespace: istio-095b8502 spec: selector: gateway: external servers: - hosts: - '*' port: number: 80 protocol: HTTP
ホスト名問わず80番ポートへのアクセスを扱うようになってますね。
セレクタに指定されているgateway: external
についても確認しておきます。
#全ネームスペースからgateway: externalというラベルを持つサービスを取得 $ kubectl get svc --all-namespaces -l gateway=external NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE istio-095b8502 rio-lb LoadBalancer 10.102.122.149 localhost 80:31752/TCP 1h
Type: LoadBalancer
なServiceが作成されてますね。これで外部からのアクセスを受け付けています。
(実はDocker for Macは18.03.0-ce-rc1-mac54
以降でType: LoadBalancer
が利用できるようになってたりします)
次にIstioのVirtualServiceについてみてみます。
$ kubectl get virtualservices -n default-265db573 NAME AGE foobar 52m #詳細確認 $ kubectl get virtualservices -n default-265db573 -o yaml foobar # 適当に省略してます apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: foobar namespace: default-265db573 spec: gateways: - mesh - rio-gateway.istio-095b8502.svc.cluster.local hosts: - foobar - foobar:80 - foobar.default.127.0.0.1.nip.io - foobar.default.127.0.0.1.nip.io:80 http: - match: - gateways: - mesh - rio-gateway.istio-095b8502.svc.cluster.local port: 80 route: - destination: host: foobar # foobar.default-265db573.svc.cluster.localと解釈される port: number: 80 subset: latest weight: 100
先ほど確認したGatewayrio-gateway
にバインドされていますね。
また、ホスト名としてfoobar
とfoobar.default.127.0.0.1.nip.io
が指定されています。
後者は先ほどrio ps
で確認したENDPOINTに表示されていたものですね。
destinationも指定されているようです。latest
という名前のsubset宛てですね。
IstioのDestinationRuleも確認してみます。
$ kubectl get destinationrules -n default-265db573 NAME AGE foobar 1h #詳細確認 $ kubectl get destinationrules -n default-265db573 -o yaml foobar #適当に省略してます apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: foobar namespace: default-265db573 spec: host: foobar subsets: - labels: rio.cattle.io/revision: latest name: latest
latest
という名前のsubsetが定義されています。
rio.cattle.io/revision: latest
というラベルを持つPod宛てということですね。
#宛先Podの確認 $ kubectl get pod -l rio.cattle.io/revision=latest -n default-265db573 NAME READY STATUS RESTARTS AGE foobar-6554b575b7-fz7fz 2/2 Running 0 1h
ということでrio run
の裏ではIstioを上手く使って頑張っているようです。
次はカナリアリリースを試してみます。
カナリアリリース
Rioはカナリアリリースをサポートしています。 先ほどはfoobarサービスとしてnginxイメージを利用しましたので、Apache(httpd)を使うように更新しカナリアリリースしてみます。
まずfoobarサービスに対するステージング環境をデプロイします。
#Apache(httpd)イメージを指定、foobar:v2とする $ rio stage --image=httpd foobar:v2 #確認 # rio ps NAME IMAGE CREATED SCALE STATE ENDPOINT DETAIL foobar nginx 40 minutes ago 1 active http://foobar.default.127.0.0.1.nip.io foobar:v2 httpd 40 minutes ago 1 active http://foobar-v2.default.127.0.0.1.nip.io
foobar:v2
がデプロイされました。ENDPOINTをブラウザで開いて確認しておきます。
昔懐かしい"It works!"が表示されていますね。
続いて早速カナリアリリースしてみます。
元のfoobar
サービスのENDPOINTに対してアクセスした際に30%の確率で更新版であるfoobar:v2
が表示されるようにしてみます。
$ rio weight foobar:v2=30%
foobarのエンドポイントhttp://foobar.default.127.0.0.1.nip.io
にアクセスし何度かリロードしてると表示が切り替わるのが確認できるはずです。
(キャッシュに注意してくださいね)
では裏ではどうなってるのか、Kubernetes側を確認してみましょう。
Kubernetes側からのぞいてみる(カナリアリリース編)
まずVirtualServicesを確認してみます。(ちなみにGatewayは変更なし)
$ kubectl get virtualservices -n default-265db573
NAME AGE
foobar 1h
foobar-v2 9m
予想通りfoobar-v2が増えてますね。
これは先ほどまでで確認したfoobarと同等ですので省略し、カナリアリリースしているfoobar
の方がどう変わったか詳しく見ておきます。
$ kubectl get virtualservices -n default-265db573 -o yaml foobar #適当に省略してます apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: foobar namespace: default-265db573 spec: gateways: - mesh - rio-gateway.istio-095b8502.svc.cluster.local hosts: - foobar - foobar:80 - foobar.default.127.0.0.1.nip.io - foobar.default.127.0.0.1.nip.io:80 http: - match: - gateways: - mesh - rio-gateway.istio-095b8502.svc.cluster.local port: 80 route: - destination: host: foobar port: number: 80 subset: latest weight: 70 - destination: host: foobar port: number: 80 subset: v2 weight: 30
spec.http.route
の子要素が追加されてますね。
指定通りv2
の方は30%、latest
は70%となっています。
念のためDestinationRulesの方も載せておきます。
$ kubectl get destinationrules -n default-265db573 -o yaml foobar #適当に省略してます apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: foobar namespace: default-265db573 spec: host: foobar subsets: - labels: rio.cattle.io/revision: latest name: latest - labels: rio.cattle.io/revision: v2 name: v2
こちらでもRioがIstioを上手く使って頑張っているようですね。
ステージング => 本番へ昇格(プロモート)させる
最後にfoobar:v2を昇格させfoobarの代わりにfoobar:v2を利用するようにしてみます。
$ rio promote foobar:v3 # 確認 $ rio ps NAME IMAGE CREATED SCALE STATE ENDPOINT DETAIL foobar httpd About an hour ago 1 active http://foobar.default.127.0.0.1.nip.io
イメージがhttpd
に変わってますね。
v2
というラベルも無くなっているのもポイントです。
#プロモートするとタグは無くなる $ kubectl get pod -l rio.cattle.io/revision=v2 -n default-265db573 No resources found. #その代わりにこれまでのv2にはlatestというタグが改めて付与される $ kubectl get pod -l rio.cattle.io/revision=latest -n default-265db573 NAME READY STATUS RESTARTS AGE foobar-69c4b7944c-2jcxt 2/2 Running 0 1m
まとめ
ということで今回はRioがKubernetes/Istioをどう扱っているかについてみてみました。
まだまだ不安定な部分も結構あったりしますが、今もGitHubに大きめのPRが上がってきてたりと開発は進んでる様子なので今後に期待しています。
以上です。
Kubernetes完全ガイド (impress top gear)
- 作者: 青山真也
- 出版社/メーカー: インプレス
- 発売日: 2018/09/21
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
- 作者: 山田明憲
- 出版社/メーカー: 技術評論社
- 発売日: 2018/08/25
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (1件) を見る
Terraform Provider実装 入門(4): スキーマ定義 後編
目次(未確定)
- 第1回: Terraform Custom Provider 基礎
- 第2回: リソース実装 基礎 -
schema.Resource
でのリソース実装の基礎 - 第3回: スキーマ定義 前編-
schema.Schema
でのスキーマ定義 - 第4回: スキーマ定義 後編-
schema.Schema
でのスキーマ定義 (当記事) - 第5回: リソース操作 -
schema.ResourceData
でのリソース操作 - 第6回: リソース定義 -
schema.Resource
の高度な機能(差分調整/マイグレーション/インポート) - 第7回: テスティングフレームワーク
前回はスキーマ定義として必須項目であるType
(データ型)について扱いました。
今回はschema.Schema
の他のフィールドについて見ていきます。
前回、*schema.Schema
に最低限指定しないといけないフィールドとして以下2つを挙げました。
Type
Optional
/Required
/Computed
を1つ以上
Type
は前回扱いましたので、今回はもう一方のOptional
/Required
/Computed
について扱います。
入出力動作(Required
/Optional
/Computed
)
項目の入力/出力動作を決めるフィールドです。 これらは1つ以上指定(trueに設定)する必要があります。
それぞれの意味は以下の通りです。
Optional : bool
: trueの場合、省略可能になるRequired : bool
: trueの場合、必須となるComputed : bool
: trueの場合、値は算出される(値が未指定の場合のみ)
これらを組み合わせて指定するのですが中には組み合わせ出来ないものもありますので、取りうるパターンは以下4つだけです。
入出力動作で指定可能な組み合わせ
Required: true
: 入力必須な項目Optional: true
: 省略可能な項目Optional:true かつ Computed: true
: 省略可能な項目(省略した場合はリソースが算出する)Computed: true
: 出力専用の項目(値はリソースが算出する)
このうち、Computed
は出力専用、それ以外は入力項目となります。
特殊な項目: 入力専用
また、少々特殊なものとして、Required
/Optional
を使いつつResourceData.Set()
を呼ばない事で入力専用の項目を作ることも可能です。
詳しくはサンプル実装を動かしながら見ていきます。
サンプル実装inout
リソースでの動作確認
これらの項目の動作確認のためにminimum_provider
でinout
というリソースを実装しています。
今回も手を動かしながら確認できるようにソースを以下に置いています。
前回ソースコードをクローンされた方はinout
ブランチをチェックアウトしビルドしておいてください。
# チェックアウト $ cd $GOPATH/github.com/yamamoto-febc/terraform-provider-minimum $ git fetch -a $ git checkout inout # ビルド $ dep ensure $ go build -o terraform-provider-minimum $ terraform init
inout
リソースでのスキーマ定義
inout
リソースはRequired
/Optional
/Computed
の組み合わせを確認するために以下のようにスキーマ定義しています。
func resourceMinimumInOut() *schema.Resource { return &schema.Resource{ // ... Schema: map[string]*schema.Schema{ "required": { Type: schema.TypeString, Required: true, }, "optional": { Type: schema.TypeString, Optional: true, }, "optional_computed": { Type: schema.TypeString, Optional: true, Computed: true, }, "computed": { Type: schema.TypeString, Computed: true, }, "input_only": { Type: schema.TypeString, Optional: true, }, }, } }
先ほどの指定可能な組み合わせ4つそれぞれに対応した項目があります。
また、入力専用の項目としてinput_only
という項目も定義しています。
このうち、computed
については現在の日時(RFC3339形式)が値として設定されます。
また、optional_computed
はtfファイルで値が指定されなかった場合に現在の日時を値として設定するようにしています。
実際に動きを見てみましょう。
Required
の動作確認
まずは以下のようにinout
リソースに対してどの項目も未指定の状態のtfファイルを作成してapply
してみます。
resource minimum_inout "inout" { }
apply
するとRequired: true
の項目が未指定である旨を表示してエラーとなってくれます。
$ terraform apply Error: minimum_inout.inout: "required": required field is not set
Required
の注意点
注意点として、Required
は値がtfファイル上で指定されていればOKと判定します。
例として、以下のようにスキーマ定義していた場合、
Schema: map[string]*schema.Schema{ "string": { Type: schema.TypeString, Required: true, }, "int": { Type: schema.TypeInt, Required: true, }, "bool": { Type: schema.TypeBool, Required: true, }, "list": { Type: schema.TypeList, Elem: &schema.Schema{Type: schema.TypeString}, Required: true, }, },
次のようなtfファイルであればapply
出来てしまいます。
resource dummy_required "example" { string = "" bool = false int = 0 list = [] }
Required: true
でも有効な値が指定されたとは限らない点にご注意ください。
この辺りは必要であれば後述するValidateFunc
フィールドを用いてバリデーションを行う事で対応可能です。
Computed
の動作確認
次にComputed
な項目について確認してみます。
tfファイルで必須項目であるrequired
項目に値を設定してみましょう。
resource minimum_inout "inout" { required = "required" }
apply
するとcomputed
とoptional_computed
に現在日時が値として設定されているはずです。
$ terraform apply # ... Apply complete! Resources: 1 added, 0 changed, 0 destroyed. # Stateを確認 $ terraform show minimum_inout.inout: id = 596549384 computed = 2018-09-17T20:49:49+09:00 #現在日時が設定されている optional = optional_computed = 2018-09-17T20:49:49+09:00 #現在日時が設定されている required = required
なお、Computed: true
のみの項目にtfファイル上で値を設定しようとすると当然ながらエラーになります。
# !正しくないtfファイルの例! resource minimum_inout "inout" { required = "required" // Computed: trueのみの項目には値の指定は出来ない computed = "computed" }
$ terraform apply Error: minimum_inout.inout: "computed": this field cannot be set
Optional
+Computed
の動作確認
次にtfファイル上でoptional_computed
に値を設定してみます。
resource minimum_inout "inout" { required = "required" optional_computed = "optional_computed" }
apply
すると今度はoptional_computed
に現在日時ではなくtfファイルで指定した値が設定されているはずです。
$ terraform apply # ... Apply complete! Resources: 0 added, 1 changed, 0 destroyed. # Stateを確認 $ terraform show minimum_inout.inout: id = 596549384 computed = 2018-09-17T20:54:06+09:00 optional = optional_computed = optional_computed #tfファイルで設定した値が反映された required = required
Optional
+Computed
の使い所
なぜこんな項目が指定するのか疑問に思う方もいらっしゃるかもしれません。
これは、指定しなければプラットフォーム側が自動で割り当ててくれる、といった項目のためのものです。
例えばAWSのEC2におけるAZ(アベイラビリティゾーン)などが該当します。
参考: AWS: EC2リソースでのスキーマ定義(AZ部分抜粋)
func resourceAwsInstance() *schema.Resource { return &schema.Resource{ // ... Schema: map[string]*schema.Schema{ // ... "availability_zone": { Type: schema.TypeString, Optional: true, Computed: true, ForceNew: true, }, // ... }, } }
入力専用の項目
スキーマ定義をしてResourceData.Set()
を呼ばない項目は入力専用の項目となります。
この場合、tfファイルからの入力でStateには保存されますが、ResourceData.Set()
が呼ばれないので実リソースの値が反映される事がないということです。
この場合でもtfファイルを変更すればTerraformが変更を検知してくれますので実リソースへの反映は可能です。
使いどころとしては実リソースから値が取得できないような項目、例えばパスワードといった項目があります。
入力専用の項目はソースコードやtfschemaといったツールでスキーマ定義を確認しただけではそうと判断できないので注意が必要です。
また、入力専用の項目は実リソースの値がStateに反映されないため、実リソースを(コントロールパネルなどを利用して)直接変更しても検知できない問題があります。
このため、実リソースの状態とtfファイルやterraform.tfstateの内容が食い違う可能性があります。
これを避けるにはTerraformで管理している実リソースについてはTerraform以外の手段での変更をなるべく避けるなど、運用面での工夫も必要となります。
これでschema.Schema
の最低限の設定項目を押さえました。
残りのフィールドについても見ていきましょう。
なお、ここからはサンプルを別ブランチに格納しています。
schema
ブランチをチェックアウトしてビルドしておいてください。
# チェックアウト $ cd $GOPATH/github.com/yamamoto-febc/terraform-provider-minimum $ git fetch -a $ git checkout schema # ビルド $ dep ensure $ go build -o terraform-provider-minimum $ terraform init
デフォルト値(Default
/DefaultFunc
)
その名の通り未指定の場合のデフォルト値を指定します。
Default
には値を直接指定し、DefaultFunc
にはデフォルト値を返すfuncを指定します。funcのシグニチャはfunc() (interface{}, error)
です。
なぜか両方指定することも可能ですが、両方指定した場合はDefault
が優先されます。
該当部分のソース: https://github.com/hashicorp/terraform/blob/v0.11.8/helper/schema/schema.go#L257-L273
それぞれを詳しく見ていきましょう。
Default
/DefaultFunc
で共通の部分
まず両者で共通しているのは以下の点です。
Computed: true
の場合は指定できないTypeList
またはTypeSet
の場合は指定できない
基本的にOptional: true
なプリミティブ型の項目に指定しますが、DefaultFunc
については例外的にRequired: true
でも指定できます。
DefaultFunc
のみRequired: true
でも設定できる
DefaultFunc
は値の決定時に指定のfuncを実行する事で動的に値を決定するものです。
これを利用して、「APIキーなどのtfファイルに直接記載して欲しく無いけど必要な情報」を環境変数から取得する、といった使い方ができます。
サンプルで動きを確認してみましょう。
今回はminimum
プロバイダーにschema
リソースというschema.Schema
の様々なフィールドを試すためのリソースを用意しました。
Required: true
かつDefaultFunc
を設定しているdefault_func_required
という項目を以下のように定義しています。
func resourceMinimumSchema() *schema.Resource { return &schema.Resource{ // ... Schema: map[string]*schema.Schema{ // ... "default_func_required": { Type: schema.TypeString, Required: true, DefaultFunc: schema.EnvDefaultFunc("MINIMUM_DEFAULT", nil), }, // ... }, } }
DefaultFunc
にschemaパッケージで提供されているヘルパー関数EnvDefaultFunc
というfuncを指定しています。
これは与えられた環境変数名(第1引数)の値を参照し、設定されていればそれを、なければ第2引数(ここではnil)の値を返してくれるものです。
default_func_required
はRequired: true
なので指定しないとterraform apply
時にエラーになります。
tfファイルまたは環境変数MINIMUM_DEFAULT
で値を指定するとterraform apply
出来るようになります。
DefaultFunc
で利用できるヘルパー関数
先ほども利用していましたが、schemaパッケージではDefaultFunc
で利用できるヘルパー関数を提供しています。
EnvDefaultFunc
: 前述の通りMultiEnvDefaultFunc
: シグニチャはfunc ([]string, interface{}) SchemaDefaultFunc
第1引数が配列になっており、環境変数名を複数指定可能。第2引数はEnvDefaultFunc
と同様
Default
/DefaultFunc
の定義を変更する際の注意点
Default
に指定する値、またはDefaultFunc
で返す値を変更するのはbreaking-changeになります。
例えばリソースが以下のように定義されているとします。
Schema: map[string]*schema.Schema{ // ... "value": { Type: schema.TypeString, Optional: true, Default: "before", }, // ... },
デフォルト値が設定されているため、tfファイルにvalue
項目は指定しなくてもよいですよね。
resource dummy_resource { name = "foobar" # value = "" # valueは指定せずデフォルトのまま }
この状態でプロバイダーのバージョンアップを行いデフォルト値を変更したとします。
Schema: map[string]*schema.Schema{ // ... "value": { Type: schema.TypeString, Optional: true, Default: "after", }, // ... },
すると、これまでこのリソースを利用していた場合はプロバイダーのバージョンアップをしただけで影響を受けてしまいます。
特に後述するForceNew: true
(変更時に削除/作成が行われる)な項目の場合だと影響が大きくなる可能性があります。
このため、出来るだけ変更しないで済むような値を設定する事が望ましいです。
とはいえどうしても変更を避けられない場合もあります。
そのような場合はRefresh
処理(第2回を参照)で呼ばれるRead
の中で適切にResourceData.Set()
する、
または次回以降に扱うスキーマのマイグレーション処理を行なって適切な値を設定するといった対応が必要となります。
値変更/保存時の挙動(DiffSuppressFunc
/ForceNew
/StateFunc
)
次に値の変更や保存時の挙動を指定するためのフィールドです。
差分の検出を抑制するDiffSuppressFunc
Terraformはplan
やapply
実行時に実リソースとtfファイルでの指定の差分を検出してくれます。
通常はデフォルトの差分検出を利用すれば良いですが、中には差分を無視したいケースもあります。
例えばtfファイルと実リソースで表記方法が違うだけで意味合いが同じといった場合などです。 具体的な用途としては
- 大文字/小文字の違いを無視する
- SSH公開鍵で末尾の改行有無を無視する
といったものがあります。
型はschema.SchemaDiffSuppressFunc
となっており、シグニチャはfunc(key, old, new string, d *ResourceData) bool
です。
第1〜3引数は項目のキー、変更前の値、変更後の値となっています。
第4引数はCRUD操作時に渡されるものと同じ*ResourceData
です。
同じとみなす場合はtrueを、差分ありとみなす場合はfalseを返すようにします。
例として、大文字/小文字の違いを無視したい場合は以下のようにします。
Schema: map[string]*schema.Schema{ // ... "diff_suppress_func": { Type: schema.TypeString, Optional: true, DiffSuppressFunc: func(key, old, new string, d *ResourceData) bool { return strings.ToLower(old) == strings.ToLower(new) // 小文字に変換して比較 }, }, // ... },
なお、DiffSuppressFunc
はスキーマ内の項目単位で制御しますが、リソース全体で制御するためのものとしてschema.Resource
にCustomizeDiff
というフィールドもあります。
こちらについては次回以降の記事で扱います。
変更されたらリソースを削除-作成するForceNew
ForceNew: true
にすると変更後のapply
実行時にリソースのUpdate
ではなく、Destroy
してCreate
するようになります。
これは作成時のみ指定が可能で以後変更できないような項目などで利用します。
また、第2回で触れた、Update
を省略する時の条件にも利用されます。
Update
を省略するにはComputed: true
でない項目全てにForceNew: true
をつけておく必要があります。
Update
を省略 = 更新できない = 更新時は削除->作成が必要なリソース、ということですね。
乱用すると思わぬところでリソースの削除が行われる原因となりえますので適度で適切な利用を心がけましょう。
Stateへの格納の際に呼ばれるフックStateFunc
Stateに値を格納する時にフックを通じて値の加工を行う事ができます。
例えば長い文字列をStateに格納する代わりにハッシュ値を保存するといった使い方です。
以下のように利用します。
Schema: map[string]*schema.Schema{ // ... "state_func": { Type: schema.TypeString, Optional: true, StateFunc: func(value interface{}) string { return hash(value.(string)) // 値のハッシュ値を返す }, }, // ... }, // ... func hash(v string) string { return fmt.Sprintf("%x", sha256.Sum256([]byte(v))) }
StateFunc
はResourceData.Set()
を呼んだ際には利用されないことに注意が必要です。
ResourceData.Set()
を呼ぶ際はStateFunc
と同様の処理を自分で行う必要があります。
d.Set("state_func", hash(value.StateFunc)) // StateFuncと同様の処理
結構面倒ですね。なのでStateFunc
は基本的にアップロードするファイルのコンテンツといった入力専用(ResourceData.Set()
をしない)項目に対して用いられる事が多いです。
本来パスワードなどの難読化といった用途でも利用できるはずなのですが、ざっとTerraform Providers配下を探してみても見当たりませんでした(よく探せばあるのかも?)。
パスワードなどのセンシティブなデータの扱いについては次のSensitive
も参照してください。
センシティブなデータをマスクするSensitive
Sensitive: true
にすると、ログや標準出力への出力時に値がマスクされるようになります。
マスクされている様子
$ terraform apply + minimum_schema.schema_example id: <computed> sensitive: <sensitive>
注意点として、Sensitive: true
にしてもStateに保存される際にはマスクされません。
例えばバックエンドがローカルの場合に作成されるterraform.tfstate
ファイルには生の値がそのまま保存されます。
また、Outputとして利用する場合にも同様にマスクされません。
(Output側で改めてSensitive指定すればマスクされます。参考:https://www.terraform.io/docs/configuration/outputs.html)
一応ソースコードのコメントにはFuture versions of Terraform may encrypt these values
とありますので、今後対応されるのかもしれません。
https://github.com/hashicorp/terraform/blob/v0.11.8/helper/schema/schema.go#L195-L199
バリデーション(ValidateFunc
/ConflictsWith
)
項目のバリデーションで利用するフィールドです。
入力値のバリデーションを行うValidateFunc
入力値に対するバリデーションを行うためのfuncを指定可能です。
型はschema.SchemaValidateFunc
で、シグニチャはfunc(value interface{},key string) (warns []string,errs []error)
です。
以下は入力された文字列の長さが1から5の間でなければエラーとする例です。
Schema: map[string]*schema.Schema{ // ... "validate_func": { Type: schema.TypeMap, Optional: true, // ValidateFunc: validation.IntBetween(1, 5)と同じ ValidateFunc: func(value interface{}, k string) (ws []string, es []error) { v , ok := value.(string) if !ok { es = append(es, fmt.Errorf("expected type of %s to be string", k)) return } if !(1 <= len(v) && len(v) <= 5) { es = append(es, fmt.Errorf("expected length of %s to be in the range (%d - %d), got %s", k, 1, 5, v)) } return }, }, // ... },
引数に項目の値とキーが渡されますのでそれらを利用してバリデーションを行います。 戻り値は警告とエラーを配列です。バリデーションの結果をそれぞれに格納してください。
組み込みのバリデーション関数
github.com/hashicorp/terraform/helper/validation
パッケージにはValidateFunc
で利用できる関数が用意されています。
なるべくこれらを利用し、不足する機能についてのみ自前実装するようにしましょう。
先ほどのvalidate_func
の例であればvalidation.IntBetween(1, 5)
のように利用します。
ValidateFunc
の注意点
現在はTypeList
とTypeSet
は非対応です。
https://github.com/hashicorp/terraform/blob/v0.11.8/helper/schema/schema.go#L192
これらのバリデーションを行いたい場合はElem
で複合型を指定してそちらにValidateFunc
を書く(もちろんプリミティブ型のみ)か、
Create
/Update
などの中で自分でバリデーションを実装するかしないといけません。
コメントには
ValidateFunc currently only works for primitive types
と書いてますが実はTypeMap
はバリデーションできたりします。
複数の項目の同時指定を防ぐConflictsWith
ConflictsWith
は同時に指定できない項目の名称を文字列の配列で指定します。
Schema: map[string]*schema.Schema{ // ... "conflicts_with1": { Type: schema.TypeString, Optional: true, ConflictsWith: []string{"conflicts_with2"}, }, "conflicts_with2": { Type: schema.TypeString, Optional: true, ConflictsWith: []string{"conflicts_with1"}, }, // ... },
両方を指定すると以下のようなエラーメッセージを出してくれます。
$ terraform validate Error: minimum_schema.schema_example: "conflicts_with1": conflicts with conflicts_with2 Error: minimum_schema.schema_example: "conflicts_with2": conflicts with conflicts_with1
警告/エラーメッセージ(Deprecated
/Removed
)
プロバイダーのバージョンアップなどでスキーマ定義が変更になり利用しなくなる/なった項目が発生した場合に利用します。
どちらも文字列を指定しておくと、その項目を利用している場合に警告/エラーを表示してくれます。
なお、Removed
の場合はterraform
コマンド自体を異常終了してくれます。
Schema: map[string]*schema.Schema{ // ... "deprecated": { Type: schema.TypeString, Optional: true, Deprecated: "deprecated", }, "removed": { Type: schema.TypeString, Optional: true, Removed: "removed", }, // ... },
これらを指定していると以下のような警告/エラーメッセージを出してくれます。
$ terraform validate Warning: minimum_schema.schema_example: "deprecated": [DEPRECATED] deprecated Error: minimum_schema.schema_example: "removed": [REMOVED] removed
なお、両者ともOptional: true
でデフォルト値なしにしておかないと常に警告/エラーが出ますのでご注意ください。
第4回 まとめ
第4回ではスキーマ定義に利用するschena.Schema
型のフィールドについて扱いました。
次回はスキーマを持つリソースの操作を行うためのschema.ResourceData
について見ていきます。
以上です。
Terraform: Up and Running: Writing Infrastructure as Code
- 作者: Yevgeniy Brikman
- 出版社/メーカー: O'Reilly Media
- 発売日: 2017/03/13
- メディア: Kindle版
- この商品を含むブログを見る
Infrastructure as Code ―クラウドにおけるサーバ管理の原則とプラクティス
- 作者: Kief Morris,宮下剛輔,長尾高弘
- 出版社/メーカー: オライリージャパン
- 発売日: 2017/03/18
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (2件) を見る
Terraform Provider実装 入門(3): スキーマ定義 前編
目次(未確定)
- 第1回: Terraform Custom Provider 基礎
- 第2回: リソース実装 基礎 -
schema.Resource
でのリソース実装の基礎 - 第3回: スキーマ定義 前編-
schema.Schema
でのスキーマ定義 (当記事) - 第4回: スキーマ定義 後編-
schema.Schema
でのスキーマ定義 - 第5回: リソース操作 -
schema.ResourceData
でのリソース操作 - 第6回: リソース定義 -
schema.Resource
の高度な機能(差分調整/マイグレーション/インポート) - 第7回: テスティングフレームワーク
前回はCustom Providerにリソースを追加し基本となるCRUD操作を実装してみました。
今回はリソースで扱う入出力項目であるスキーマの定義について詳しくみていきます。
なお、今回も手を動かしながら確認できるようにソースを以下に置いています。
https://github.com/yamamoto-febc/terraform-provider-minimum/tree/types
前回ソースコードをクローンされた方はtypes
ブランチをチェックアウトしビルドしておいてください。
#チェックアウト $ cd $GOPATH/github.com/yamamoto-febc/terraform-provider-minimum $ git fetch -a $ git checkout types #ビルド $ dep ensure $ go build -o terraform-provider-minimum main.go
schema.Schema
でのスキーマ定義
前回作成したbasic
リソースではスキーマを以下のように定義していました。
import ( // [...中略...] "github.com/hashicorp/terraform/helper/schema" ) func resourceMinimumBasic() *schema.Resource { return &schema.Resource{ // [...中略...] Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, }, } }
文字列型で必須なname
という項目一つだけの単純なものです。
項目を増やすにはSchema
に項目を追加していくだけでOKです。(もちろんCRUD操作の中でd.Set()
を適度に呼び出すなど適切に実装する必要があります)
各項目はSchema
に設定するmapの要素であるschema.Schema
のフィールドを適切に設定することでデータ型や制約、デフォルト値などを柔軟に制御可能です。
まずはschema.Schema
型について詳しくみていきます。
schema.Schema
型
schema.Schema
型はリソースが扱う入出力項目それぞれの振る舞いを定義するための構造体です。
- 公式ドキュメント: https://www.terraform.io/docs/extend/schemas/index.html
- ソース: https://github.com/hashicorp/terraform/blob/v0.11.8/helper/schema/schema.go#L34-L200
以下のようなフィールドが定義されています。
("フィールド名
(型) : 説明" で表記)
- データ型
Type
(schema.ValueType) : データ型
- 入出力動作の指定(1つ以上の指定必須)
Optional
(bool) : trueの場合、省略可能になるRequired
(bool) : trueの場合、必須となるComputed
(bool) : trueの場合、作成時に計算(算出)される(値が未指定の場合のみ)
- デフォルト値関連
Default
(interface{}) : デフォルト値(値で指定)DefaultFunc
(schema.SchemaDefaultFunc) : デフォルト値(funcで指定)
- 値変更/保存時の挙動関連
DiffSuppressFunc
(schema.SchemaDiffSuppressFunc) : 差分検出で使用するfuncForceNew
(bool) : trueの場合は変更時にUpdate
ではなくDestroy
->Create
が行われるStateFunc
(schema.SchemaStateFunc) :State
への格納の際に呼ばれるフック
- センシティブなデータの扱い
Sensitive
(bool) : trueの場合、この項目をログ/標準出力に出力する際にマスクされる
Type
がTypeList or TypeSetの場合に使用するフィールドElem
(interface{}) : ListまたはSetの各要素のデータ型MaxItems
(int) : ListまたはSetに格納できる最大数(境界含む/1以上の場合のみ有効)MinItems
(int) : ListまたはSetに格納できる最小数(境界含む/1以上の場合のみ有効)PromoteSingle
(bool) : trueの場合、単一の値として指定された場合に自動的にリストに変換する(プリミティブ型のみで有効)
Type
がTypeSetの場合に使用するフィールドSet
(schema.SchemaSetFunc) : 項目のハッシュ値算出で使用するfunc
- バリデーション関連
ConflictsWith
([]string) : 同時に指定できない項目の名称を指定ValidateFunc
(schena.SchemaValidateFunc) : 入力値のバリデーションで使用するfunc(プリミティブ型のみで有効)
- 警告/エラーメッセージ関連
Deprecated
(string) : 設定されている場合、この項目を利用すると警告メッセージを出すRemoved
(string) : 設定されている場合、この項目を利用するとエラーメッセージを出す(validate
やplan
、apply
を異常終了させる)
- v0.11時点では実装されていないフィールド
Description
(string)InputDefault
(string)ComputedWhen
([]string)
必須項目
これらのうち、最低限指定しないといけないのは以下2つです。
Type
Optional
/Required
/Computed
を1つ以上
まずはType
からみていきます。
データ型を決めるType
Type
はデータ型を表します。以下の値が指定可能です。
TypeBool
- boolTypeInt
- intTypeFloat
- float64TypeString
- stringTypeList
- []interface{}TypeMap
- map[string]interface{}TypeSet
- *schema.Set
右側はResourceData.Get()
を呼んだ時の戻り値の実際の型を表しています。
ResourceData.Get()
の戻り値はinterface{}
ですので、項目のデータ型に応じて適切にキャストする必要があります。
それぞれの特徴を順に見ていきます。
プリミティブ型(Bool/Int/Float/String)
それぞれのプリミティブ型を示します。特に説明不要ですね。
tfファイル上は値の書き方にバリエーションがある点には留意しておいてください。
(このへんはTerraformの実装というよりHCLの実装によります)
Terraform v0.12ではHCLの後継であるHCL2への切り替えが進められています。
以下にtfファイルの書き方例を記載しておきます。
TypeBool
resource minimum_bool "bool" { value = true // 文字列で指定してもOK #value = "true" #OK #value = "foobar" #これはNG // 数値もOK(true=1,false=0) #value = 0 # OK #value = 2 # これはNG #value = -1 # これはNG // 数値を文字列で指定してもOK #value = "0" # OK #value = "2" # これはNG #value = "-1" # これはNG }
TypeInt
resource minimum_int "int" { value = 1 // 8進数/16進数/指数もOK #value = 0777 # 8進数 #value = 0xFFFF # 16進数 #value = 1e10 # 指数表記 // 文字列で指定してもOK #value = "1" // 範囲(golangのintの範囲) #value = -9223372036854775808 #value = 9223372036854775807 #value = 1.1 # これはNG }
TypeString
resource minimum_string "string" { value = "foobar" // 数値で指定してもOK(文字列に変換される) #value = 0777 # 8進数 #value = 0xFFFF # 16進数 #value = 1e10 # 指数表記 }
List型(TypeList)
次にTypeList
を見ていきます。TypeList
はプリミティブ型、または複合型のリストを表します。
スキーマ定義は以下のようになります。
func resourceMinimumList() *schema.Resource { return &schema.Resource{ // ... Schema: map[string]*schema.Schema{ "value": { Type: schema.TypeList, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, // MaxItems: 1, // MinItems: 1, }, }, } }
TypeListにする場合、Elem
は必須です。Elem
でリストの各要素のデータ型を指定する必要があります。
リスト内の要素がプリミティブ型の場合は*schema.Schema
を、複合型の場合は*schema.Resource
を指定します。
多段ネストも可能です。
なお、後述するTypeMap
と違いElem
にschema.ValueType
を直接指定することはできません。
正しくないElem指定の例
// !正しくないElem指定の例! func resourceMinimumList() *schema.Resource { return &schema.Resource{ // ... Schema: map[string]*schema.Schema{ "value": { Type: schema.TypeList, Optional: true, Elem: schema.TypeString, // TypeListではValueTypeを直接指定できない }, }, } }
次にリスト内に複合型を利用する場合のスキーマ定義です。
リスト内に複合型を利用する例
func resourceMinimumNestedList() *schema.Resource { return &schema.Resource{ // ... Schema: map[string]*schema.Schema{ "value": { Type: schema.TypeList, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Optional: true, }, "value": { Type: schema.TypeString, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, }, }, }, }, }, } }
また、リストの要素数をMinItems
/MaxItems
で制限可能です。
いずれもデフォルトは0で、1以上の数値を指定すると有効になります。
(これらは後述するTypeSet
でも利用可能です)
List型を含むリソースを利用するtfファイルは以下のようになります。
# プリミティブ型のリスト resource minimum_list "list" { value = ["list0", "list1", "list2"] } # 複合型のリスト resource minimum_nested_list "nested-list" { value = [ { name = "name1" value = "value1" description = "description1" }, { name = "name2" value = "value2" description = "description2" // 定義していない項目はちゃんとエラーにしてくれる # foo = "bar" }, ] }
List型の項目に対しResourceData.Get()
すると[]interface{}
が返ってきます。
各要素がプリミティブ型の場合は単にキャスト(TypeString
ならstringへ)すればOKです。
複合型の場合はmap[string]interface{}
になります。
上記のminimum_nested_list
リソースの場合は以下のようにします。
func resourceMinimumNestedListRead(d *schema.ResourceData, meta interface{}) error { // ... values := d.Get("value").([]interface{}) // まず[]interface{}にキャスト for _ , v := range values { element := v.(map[string]interface{}) // 各要素はmap[string]interface{} name := element["name"].(string) value := element["value"].(string) desc := element["description"].(string) // ... } }
Map型(TypeMap)
続いてTypeMap
を見ていきます。TypeMap
はその名の通りキーと値をペアで持ちます。キーは文字列である必要があります。
スキーマ定義は以下のようになります。
func resourceMinimumMap() *schema.Resource { return &schema.Resource{ // ... Schema: map[string]*schema.Schema{ "value": { Type: schema.TypeMap, Optional: true, }, }, } }
リソースの実装時、Mapの項目に対しResourceData.Get()
するとmap[string]interface{}
が返ってきます。
また、Map型でも要素の型をElem
で指定できますが、少々クセがある点に注意が必要です。
Map型の注意点
現状はMap型の項目に対しElem
を指定した際は以下のような挙動となっています。
Elem
が未指定の場合は各要素をTypeString
とみなすElem
にschema.ValueType
(TypeIntとかTypeStringとか)が指定された場合- プリミティブ型であればそのまま使う
- 以外の場合は
TypeString
とみなす
Elem
に*schema.Schema
が指定された場合*schema.Schema
のType
がプリミティブ型であればそのまま使う- 以外の場合は
TypeString
とみなす
Elem
に*schema.Resource
が指定された場合TypeString
とみなす
要はMapの各要素はプリミティブ型でないとダメということです。
この挙動はリストのように複合型を利用したい場合に問題が発生します。
例えば以下のようにスキーマ定義します。
// !問題のあるスキーマ定義! func resourceMinimumInvalidMap() *schema.Resource { return &schema.Resource{ // ... Schema: map[string]*schema.Schema{ "value": { Type: schema.TypeMap, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "value1": { Type: schema.TypeString, Required: true, }, "value2": { Type: schema.TypeInt, Optional: true, }, "value3": { Type: schema.TypeBool, Optional: true, }, }, }, }, }, } }
Map型の各要素として以下のような複合型を指定しています。
value1
: 文字列型/必須value2
: Int型value3
: Bool型
このリソースを利用するtfファイルは以下の通りです。
resource minimum_invalid_map "invalid" { value = { value1 = "value1" value2 = 2 value3 = true } }
terraform validate
もterraform apply
も問題なく行えるはずです。
しかし、以下のようにした場合はどうでしょうか?
resource minimum_invalid_map "invalid" { value = { # value1 = "value1" # Required=trueの項目をコメントアウト value2 = "foo" # Int型の項目に文字列を指定 value3 = "bar" # Bool型の項目に文字列を指定 value4 = "not exists" # 定義していない項目を指定 } }
なんとterraform validate
もterraform apply
も問題なく行えてしまいました。
これはElem
にプリミティブ型以外を指定してしまったために、各要素がTypeString
とみなされてしまうからです。
このようにMap型でElem
を使う場合には直感的でない挙動となりますので、
もしtfファイル上で複合型を利用したい場合は後述のTypeSet
か、MaxItems=1
にしたTypeList
を使う方法を検討してください。
なお、この辺のTypeMapの挙動については対応が進められてはいるのですが現時点では止まっちゃってるっぽいです。
Set型(TypeSet
)
最後にTypeSet
をみていきます。TypeSet
は値のハッシュ機能付きのリストです。
スキーマ定義は以下のようになります。
func resourceMinimumSet() *schema.Resource { return &schema.Resource{ // ... Schema: map[string]*schema.Schema{ "value": { Type: schema.TypeSet, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Optional: true, }, "value": { Type: schema.TypeString, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, }, }, }, Set: func(v interface{}) int { var buf bytes.Buffer m := v.(map[string]interface{}) // nameとvalueの値からハッシュ値を生成 keys := []string{"name", "value"} for _, key := range keys { if v, ok := m[key]; ok { buf.WriteString(fmt.Sprintf("%s-", v.(string))) } } return hashcode.String(buf.String()) }, }, }, } }
TypeList
の時と同じくElem
が必須となっています。
違いはSet
の部分ですね。
これは値からハッシュ値を算出する部分となっています。
(なおSet
を利用せず、ResourceData.Set()
するときにschema.NewSet()
を利用する方法もあります。)
ポイントはハッシュ値が同一なTypeSet
の要素はまとめられるという性質を持っている点です。
tfファイルでの例を見てみましょう。
この例のminimum_set
リソースでは、各要素のname
とvalue
を元にハッシュ値を算出しています。
まずはハッシュ値が異なる要素の場合です。
resource minimum_set "set" { value = [ { name = "name1" value = "value1" description = "description1" }, { name = "name2" value = "value2" description = "description2" }, ] }
terraform apply
すると以下のようになります。
$ terraform apply An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: + minimum_set.set id: <computed> value.#: "2" value.1049404950.description: "description1" value.1049404950.name: "name1" value.1049404950.value: "value1" value.741132560.description: "description2" value.741132560.name: "name2" value.741132560.value: "value2" Plan: 1 to add, 0 to change, 0 to destroy.
minimum_set.set
のvalue
の要素として2つの要素が作成される様子が確認できますね。
次にtfファイルを修正しname
とvalue
を同一の値にしてハッシュ値が同じになるようにしてみます。
$ terraform apply An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: + minimum_set.set id: <computed> value.#: "1" value.1049404950.description: "description2" value.1049404950.name: "name1" value.1049404950.value: "value1" Plan: 1 to add, 0 to change, 0 to destroy.
今度はminimum_set.set
のvalue
の要素として1つの要素が作成される様子が確認できますね。
前述の通りハッシュ値が同じ要素はまとめられるという性質によりこうなっています。
一見するとめんどくさいだけなようにも思えますが、もちろん役に立つ場面が存在します。
例えばDNSのAレコードを扱う場合を考えてみます。
Aレコードは一つのホスト名に対し複数のIPアドレスを指定できますよね。
この場合単純にホスト名をキーにしたマップを利用するというわけにはいかないでしょう。
正しくない例: レコードをマップで表す場合
# !正しくない例! レコードをマップで表す場合 resource dummy_dns_zone "zone" { zone = "example.com" records = { "www" = "192.2.0.1", # mapなので重複したキーは指定できない "www" = "192.2.0.2", # } }
単純にリストにすればキーに当たるホスト名は重複させられますが、今度はホスト名/IPアドレスの組み合わせが重複する可能性があります。
正しくない(かもしれない)例: レコードをリストで表す場合
# !正しくない(かもしれない)例! レコードをリストで表す場合 resource dummy_dns_zone "zone" { zone = "example.com" # リストの場合は値の重複が起こり得る records = [ { name = "www", ip = "192.2.0.1", }, { name = "www", ip = "192.2.0.1", }, ] }
ResourceData.Get()
を行なった際に手動でバリデーションを行えば良いのですが、TypeSet
であればこの辺りが楽に行えるようになっています。
# レコードをセット(TypeSet)で表す場合 resource dummy_dns_zone "zone" { zone = "example.com" # TypeSetの場合はハッシュ生成に使う値が同じならまとめられる records = [ { name = "www", ip = "192.2.0.1", }, { name = "www", ip = "192.2.0.1", }, ] }
注意点として、TypeSet
を用いることで、tfファイルには2レコード書いたつもりなのに(書き間違えにより意図せず)1レコードしか作成されない!といったことが発生し得ます。
TypeList
を利用してバリデーションを実装することで重複チェックを行う方法もありますので、適度に使い分けましょう。
また、TypeSet
を利用した場合、リソースのテストが書きにくい(リソースIDがハッシュ値になるため)という問題もあります。
この辺りは次回以降の記事でテスティングフレームワークを扱う際に改めて触れます。
Type
は以上です。これらを組み合わせてリソースに対する入出力項目を定義していくことになります。
各データ型の特徴をしっかり押さえておきましょう。
第3回 まとめ
第3回ではスキーマ定義に利用するschena.Schema
型のフィールドのうち、データ型を表すType
について扱いました。
次回はschema.Schema
の他のフィールドについて見ていきます。
以上です。
Terraform: Up and Running: Writing Infrastructure as Code
- 作者: Yevgeniy Brikman
- 出版社/メーカー: O'Reilly Media
- 発売日: 2017/03/13
- メディア: Kindle版
- この商品を含むブログを見る
Infrastructure as Code ―クラウドにおけるサーバ管理の原則とプラクティス
- 作者: Kief Morris,宮下剛輔,長尾高弘
- 出版社/メーカー: オライリージャパン
- 発売日: 2017/03/18
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (2件) を見る
Terraform Provider実装 入門(2): リソース実装 基礎
目次(未確定)
- 第1回: Terraform Custom Provider 基礎
- 第2回: リソース実装 基礎 -
schema.Resource
でのリソース実装の基礎 (当記事) - 第3回: スキーマ定義 前編-
schema.Schema
でのスキーマ定義 - 第4回: スキーマ定義 後編-
schema.Schema
でのスキーマ定義 - 第5回: リソース操作 -
schema.ResourceData
でのリソース操作 - 第6回: リソース定義 -
schema.Resource
の高度な機能(差分調整/マイグレーション/インポート) - 第7回: テスティングフレームワーク
前回はCustom Providerとして最低限の実装を行いTerraformから利用できるようにするところまででした。
今回は引き続きTerraformでのCustom Providerの実装の基礎的な部分について扱います。
前回作成したterraform-provider-minimum
にリソース操作のための実装を追加していきます。
プロバイダーで扱うリソースの実装
それでは早速minimum
プロバイダーで扱うリソースを実装してみます。
minimum
プロバイダーの仕様
Terraformプロバイダーの多くは対象プラットフォーム(例:AWS/Azure/GCP)が提供するAPIを用いてプラットフォーム上のリソースに対するCRUD操作を行うという形で実装されますが、
minimum
プロバイダーはサンプル実装ということで簡易的にローカルマシン上にファイルを作成するように実装します。
ここでは例としてminimum
プロバイダーに対しbasic
というリソースを実装してみます。
以下のようにname
という属性一つだけをもつシンプルなリソースです。
resource minimum_basic "example" { name = "foobar" }
terraform apply
を実行するとカレントディレクトリに以下のようなJSONファイルが作成されます。
{ "Name": "foobar" }
それでは早速リソースの実装を行なっていきます。
なお、basic
リソースを実装したソース一式は以下に置いています。
前回ソースコードをクローンされた方はbasic
ブランチをチェックアウトしておいてください。
$ cd $GOPATH/github.com/yamamoto-febc/terraform-provider-minimum $ git checkout basic
basic
リソースの実装
リソース実装の手順は以下の通りです。
- (1)
*schema.Resource
を返すfuncを作成 - (2)
Provider()
に(1)で実装したfuncを追記
順に見ていきます。
(1) *schema.Resource
を返すfuncを作成
schema.Resource
とは、リソースに対するCRUDを行うfuncや入出力項目のスキーマ定義を持つ構造体です。
ソース: https://github.com/hashicorp/terraform/blob/v0.11.8/helper/schema/resource.go#L23-L137
リソースとしての最低限の実装は以下の通りです。
package minimum import "github.com/hashicorp/terraform/helper/schema" func resourceMinimumBasic() *schema.Resource { return &schema.Resource{ // CRUD操作用のfuncをそれぞれ指定 Create: resourceMinimumBasicCreate, Read: resourceMinimumBasicRead, Update: resourceMinimumBasicUpdate, // 省略可 Delete: resourceMinimumBasicDelete, Schema: map[string]*schema.Schema{ // ここに入出力項目を定義 }, } } func resourceMinimumBasicCreate(d *schema.ResourceData, meta interface{}) error { return nil } func resourceMinimumBasicRead(d *schema.ResourceData, meta interface{}) error { return nil } func resourceMinimumBasicUpdate(d *schema.ResourceData, meta interface{}) error { return nil } func resourceMinimumBasicDelete(d *schema.ResourceData, meta interface{}) error { return nil }
schema.Resource
はいくつかのフィールドを持っていますが、リソースとして動作させるには最低限以下の項目をセットする必要があります。
Create
: リソース作成用のfuncRead
: リソース参照用のfuncUpdate
: リソース更新用のfunc(特殊なリソースでは省略可)Delete
: リソース削除用のfuncSchema
: 入出力項目の定義
CRUD用のfuncはfunc (*schema.ResourceData, interface{}) error
というシグニチャを持ちます。
このうちUpdate
は省略可能ですが、省略するにはいくつか条件がありますので次回以降の記事で改めて解説します。
それぞれのfuncの書き方にはお作法がありますので後ほどCRUDそれぞれについて見ていきます。
Schema
にはこのリソースで扱う入出力項目をmap[string]*schema.Schema
として記述します。
mapのキーが項目名となり、項目のデータ型や制約などを*schema.Schema
として記述します。
Schema
の定義
今回のbasic
リソースではname
という文字列型の項目を一つだけ定義しておきます。
func resourceMinimumBasic() *schema.Resource { return &schema.Resource{ // ... Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, }, } }
*schema.Schema
には他にもいくつかのフィールドがありますが、それらは次回以降に扱います。
Create
の実装
続いてCreate
用のfuncを実装します。
Create
では以下の処理を行う必要があります。
- tfファイルからの入力を取得
- 各プラットフォームのAPIなどを用いて実リソースの作成
*schema.ResourceData
のSetId()
を呼び出してリソースIDを設定- 必要に応じて
*schema.ResourceData
のSet()
を呼び出してリソースの値を設定
basic
リソースでは以下のように実装します。
var client = state.NewDriver("basic") func resourceMinimumBasicCreate(d *schema.ResourceData, meta interface{}) error { // tfファイルからの入力を取得 name := d.Get("name").(string) // プラットフォームのAPIなどを用いて実リソースの作成 // (ここではローカルマシン上にJSONファイルを保存) value := &state.BasicValue{ Name: name, } id, err := client.Create(value) // 実リソース作成 - リソースのIDを戻り値として受け取る if err != nil { return err } // リソースIDを設定 d.SetId(id) // リソースの値を設定 return resourceMinimumBasicRead(d, meta) }
第1引数のd
は*schema.ResourceData
型となっています。
これはリソースに対する入出力値を扱う構造体で、tfファイルからの入力値の取得やTerraformが管理するリソースの値=State
へのアクセスなどを担当します。
TerraformでのState
については以下を参照してください。
第2引数のmeta
はAPIキーといったプロバイダー固有の設定を各リソースに受け渡すためのものです。
通常APIクライアントなどが設定されています。
今回は利用していませんが、次回の記事でこの引数を利用するようにリファクタしてみます。
tfファイルからの入力値の取得
まずはd.Get("項目名")
でtfファイルからの入力値を取得しています。戻り値はinterface{}
になっていますので、適切な型にキャストします。
プラットフォームのAPIを利用して実リソースの作成
入力値が出揃ったらプラットフォームのAPIを利用して実リソースの作成を行います。
この際にリソースごとに一意なIDを発行する必要があります。
今回はサンプルとしてプラットフォームのAPIを呼び出す代わりにローカルマシン上にJSONファイルとして保存しておくようにしています。
実際の処理はstate
パッケージに実装していますので興味のある方はソースを眺めてみてください。
(値をinterface{}
型として受け取り、JSONにして保存、IDはランダムな数値を返す実装です)
リソースIDの設定
実リソースを作成しIDが発行されたらd.SetId()
を呼び出してリソースIDの設定を行います。
Terraformはこのfuncを抜けた後にd.SetId()
を通じてリソースIDが設定されていか判定し、設定されている場合はリソースの持つ値をState
に保存します。
(特にバックエンドの設定をしていない場合はカレントディレクトリにterraform.tfstate
というファイルが作成されます)
リソースの値の設定
最後にd.Set()
を呼び出してリソースの値をキーごとに設定していきます。
設定した値は最終的にState
に保存されます。
(なお、d.SetId()
とd.Set()
の順番はどちらが先でもOKです)
ここで直接d.Set()
を呼んでも良いのですが、後ほどRead
を実行するときにもd.Set()
を実行することになりますので、実装の重複を避けるためにRead
用のfuncを直接呼び出すようにしています。
Read
の実装
続いてRead
を実装していきます。
Read
では以下の処理を行う必要があります。
*schema.ResourceData
のId()
を呼び出してリソースのIDを取得- 取得したIDを用いて各プラットフォームのAPIなどを呼び出し実リソースのデータを参照
*schema.ResourceData
のSet()
を呼び出してリソースの値を設定
一言で言うと、リソースのIDを用いて実リソースの最新リソースを参照し、リソースの値を設定
を行います。
basic
リソースでは以下のように実装します。
func resourceMinimumBasicRead(d *schema.ResourceData, meta interface{}) error { // リソースのIDを取得 id := d.Id() // IDを用いてプラットフォームのAPIを呼び出し、実リソースを参照する data, err := client.Read(id) if err != nil { // 実リソースが見つからなかったら if state.IsStateNotFoundError(err) { // *Point* IDを空にしてnilを返す d.SetId("") return nil } return err } value := &state.BasicValue{} if err := json.Unmarshal(data, &value); err != nil { return err } // 実リソースの最新の情報をリソースデータに設定 d.Set("name", value.Name) // nolint return nil }
実装時の注意点: 実リソースが存在しなかった場合の対応
ポイントは実リソースが見つからなかった場合の処理です。
IDを元にプラットフォーム上を検索してみたけど見つからなかったと言う場合には
d.SetId(""")
を呼びリソースIDをクリアした上でnilを返します。
こうすることでTerraformとしてはリソースは存在しないものとして認識し、再度作成が必要といった判断を行ってくれます。
ここでは、IDによる検索で予期せぬエラーが発生した場合と実リソースが見つからなかった場合を戻り値などから区別できることが必要です。
実リソースが存在しない場合にもエラーを返してしまった場合、terraform plan
やterraform destroy
の実行ができないという状況になる可能性もあります。
というのも、Terraformはplan
やapply
、destroy
の実行の前にState
を最新の状態にするためにRefresh
という処理を行います。
Refresh
は単に各リソースのRead
を呼び出すのですが、Read
がエラーを返すとそこで処理が中断されてしまうからです。
実リソースが存在しない場合はTerraformではもはや手の打ちようがないですので、d.SetId("")
を呼び出すことでState
からリソースの値をクリアします。
ごく稀にこの辺の処理を正しく行えていないリソースがあり、applyもdestroyもできないという状況になることがあります。
そのような場合の応急処置としてterraform.tfstate
ファイルから該当リソース部分を手作業で削除という泥臭い解決方法もあります。
IDを元に最新の実リソースを参照できるようになった後はd.Set()
を呼び出してリソースの値を設定していきます。
Update
の実装
続いてUpdate
の実装を行います。
Update
では以下の処理を行う必要があります。
*schema.ResourceData
のId()
を呼び出してリソースのIDを取得- 各プラットフォームのAPIなどを呼び出しIDに対応する実リソースの値を更新
*schema.ResourceData
のSet()
を呼び出してリソースの値を設定
Create
と同じくd.Get()
などでtfファイルからの入力値を取得して実リソースの値を更新していきます。
*schema.ResourceData
には以下のようないくつかのヘルパー関数がありますので、変更のあった項目だけ更新するといったことも可能です。
*schema.ResourceData
の変更検知用関数
d.HasChange("<項目名>")
: 対象項目が変更されているかを判定(戻り値: bool)d.GetChange("<項目名>")
: 対象項目の変更前/変更後の値を取得(戻り値: interface{}, interface{})
basic
リソースでは以下のように実装します。
func resourceMinimumBasicUpdate(d *schema.ResourceData, meta interface{}) error { id := d.Id() // IDを元に実リソースを検索 data, err := client.Read(id) if err != nil { // 実リソースが見つからなかったら if state.IsStateNotFoundError(err) { d.SetId("") return nil } return err } value := &state.BasicValue{} if err := json.Unmarshal(data, &value); err != nil { return err } // set updated values if d.HasChange("name") { value.Name = d.Get("name").(string) } // save values if err := client.Update(id, value); err != nil { return err } return resourceMinimumBasicRead(d, meta) }
ここでもRead
と同じくIDを用いて実リソースを検索し、見つからなかったらd.SetId("")
を呼び出した上でnilを返しています。
あとはd.HasChange()
で変更があった項目をセットした上でプラットフォームのAPIを呼び出し実リソースを更新します。
Create
と同じくRead
用のfuncを直接呼び出すことでd.Set()
を行っています。
Delete
の実装
続いてDelete
の実装を行います。
Delete
では以下の処理を行う必要があります。
*schema.ResourceData
のId()
を呼び出してリソースのIDを取得- 各プラットフォームのAPIなどを呼び出しIDに対応する実リソースの値を削除
Create
やUpdate
と比較するとシンプルな実装になることが多いですね。
basic
リソースでは以下のように実装します。
func resourceMinimumBasicDelete(d *schema.ResourceData, _ interface{}) error { id := d.Id() if err := client.Delete(id); err != nil { if !state.IsStateNotFoundError(err) { return err } } d.SetId("") return nil }
IDを用いてプラットフォームのAPIで実リソースの削除を行うだけですね。
なお、Delete
ではエラーを返さなければd.SetId("")
があとで呼ばれるようになっていますのでここでのd.SetId("")
は省略可能だったりします。
(公式ガイドにもその旨が書いてあります。明確にするためにあえて書いているとのことです。)
参考: https://github.com/hashicorp/terraform/blob/v0.11.8/helper/schema/resource.go#L197-L203
以上でリソースの基本的な実装ができました。
余談: Exists
は実装すべき?
schema.Resource
のソースを読むと、Exists
というfuncがあり、実装を推奨(recommended)すると言うコメントが書かれています。
参考: https://github.com/hashicorp/terraform/blob/v0.11.8/helper/schema/resource.go#L59-L86
// The functions below are the CRUD operations for this resource. // // [...中略...] // // Exists is a function that is called to check if a resource still // exists. If this returns false, then this will affect the diff // accordingly. If this function isn't set, it will not be called. It // is highly recommended to set it. The *ResourceData passed to Exists // should _not_ be modified. Create CreateFunc Read ReadFunc Update UpdateFunc Delete DeleteFunc Exists ExistsFunc
これはRead
で行っていた、IDによる実リソースの存在確認を実装するためのもので、セットされているときだけ呼ばれるようになっています。
推奨という割には実装していないリソースもチラホラと散見されます。(例: AWSプロバイダーのEC2リソース aws_instance
など)
実際にはRead
が正しく実装されていればExists
は実装しなくても構いません。
Read
との違いは呼ばれるタイミングです。
Exists
とRead
の間にはスキーマのマイグレーション処理(次回以降に扱う予定)が行われています。
参考: https://github.com/hashicorp/terraform/blob/v0.11.8/helper/schema/resource.go#L319-L352
このあたりの処理が重くなるようであればExists
を実装した方が良いですが、最初はRead
の実装のみで十分でしょう。
(2) Provider()
に(1)で実装したfuncを追記
リソースの実装ができましたので、Provider()
にリソースを登録しておきます。
minimum/provider.go
を以下のようにします。
package minimum import ( "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" ) // Provider returns a terraform.ResourceProvider. func Provider() terraform.ResourceProvider { return &schema.Provider{ Schema: map[string]*schema.Schema{}, ResourcesMap: map[string]*schema.Resource{ "minimum_basic": resourceMinimumBasic(), // この行を追記 }, } }
これでbasic
リソースの実装ができました。
動作確認
実際にビルドして試してみましょう。
tfファイルはbasic
ブランチの直下にexample.tf
がというファイルを置いていますのでそれを利用します。
# ビルド $ go build -o terraform-provider-minimum main.go # init & apply $ terraform init $ terraform apply An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: + minimum_basic.example id: <computed> name: "foobar" Plan: 1 to add, 0 to change, 0 to destroy. 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を入力 minimum_basic.example: Creating... name: "" => "foobar" minimum_basic.example: Creation complete after 0s (ID: 2012225891) Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
apply
が成功するとカレントディレクトリにbasic-nnnnnnnn.json
(nはランダムな数字)というファイルが作成されているはずです。
tfファイルやJSONファイルを書き換えてapply
を実行したりdestroy
を実行したりと色々試してみてください。
basic
リソースのソース全体
まとめとしてbasic
リソースのソース全体を載せておきます。
package minimum import ( "encoding/json" "github.com/hashicorp/terraform/helper/schema" "github.com/yamamoto-febc/terraform-provider-minimum/state" ) var client = state.NewDriver("basic") func resourceMinimumBasic() *schema.Resource { return &schema.Resource{ Create: resourceMinimumBasicCreate, Read: resourceMinimumBasicRead, Update: resourceMinimumBasicUpdate, Delete: resourceMinimumBasicDelete, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, }, } } func resourceMinimumBasicCreate(d *schema.ResourceData, meta interface{}) error { name := d.Get("name").(string) value := &state.BasicValue{ Name: name, } id, err := client.Create(value) if err != nil { return err } d.SetId(id) return resourceMinimumBasicRead(d, meta) } func resourceMinimumBasicRead(d *schema.ResourceData, _ interface{}) error { id := d.Id() data, err := client.Read(id) if err != nil { if state.IsStateNotFoundError(err) { d.SetId("") return nil } return err } value := &state.BasicValue{} if err := json.Unmarshal(data, &value); err != nil { return err } d.Set("name", value.Name) // nolint return nil } func resourceMinimumBasicUpdate(d *schema.ResourceData, meta interface{}) error { id := d.Id() // read current state data, err := client.Read(id) if err != nil { if state.IsStateNotFoundError(err) { d.SetId("") return nil } return err } value := &state.BasicValue{} if err := json.Unmarshal(data, &value); err != nil { return err } // set updated values if d.HasChange("name") { value.Name = d.Get("name").(string) } // save values if err := client.Update(id, value); err != nil { return err } return resourceMinimumBasicRead(d, meta) } func resourceMinimumBasicDelete(d *schema.ResourceData, _ interface{}) error { id := d.Id() if err := client.Delete(id); err != nil { if !state.IsStateNotFoundError(err) { return err } } d.SetId("") return nil }
第2回 まとめ
第2回では単純なリソースの実装を行ってみました。 操作したいリソースにAPIさえ提供されていれば意外と簡単に実装できますね。
次回はスキーマ定義(schema.Schema
)についてもう少し深掘りしてみます。
以上です。
Terraform: Up and Running: Writing Infrastructure as Code
- 作者: Yevgeniy Brikman
- 出版社/メーカー: O'Reilly Media
- 発売日: 2017/03/13
- メディア: Kindle版
- この商品を含むブログを見る
Infrastructure as Code ―クラウドにおけるサーバ管理の原則とプラクティス
- 作者: Kief Morris,宮下剛輔,長尾高弘
- 出版社/メーカー: オライリージャパン
- 発売日: 2017/03/18
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (2件) を見る
Terraform Provider実装 入門(1): Custom Providerの基礎
今回はTerraformから提供されているprovider framework
を利用した独自のプロバイダーの実装について扱います。
カスタムプロバイダーについての基本的な知識〜実装上の注意点などをサンプル実装を通じて見ていきます。
注:この記事はTerraform v0.11に対応しています。
=== UPDATE: 2022/05
この記事執筆当時と筆者の状況が変わり、この一連の記事は完結する見込みがほぼ無くなっています。
また、Terraform側も進化しており、Terraform SDKが生まれ、SDK v2に変わり、現在ではTerraform Plugin Frameworkというものが生まれています。
この一連の記事の内容はSDK v2でも若干の調整(エラーハンドリングなど)だけで通用しますが、Plugin Frameworkについては全く新しい作りとなっていること、新規にプロバイダーを作成する場合はPlugin Frameworkの利用を検討するように公式ドキュメントに書かれていますので、これらの点に留意しつつお読みください。
=== UPDATE ここまで
はじめに
この記事は主にカスタムプロバイダーの実装をする方やAWS/GCP/Azureなどの既存のプロバイダーで発生した問題の解決のためにソースを読む/修正するといった方向けです。
このため、Terraform自体についての説明はかなり省いています。
とはいえ必要に応じて触れますので最低限以下の2点を押さえておけばOKです。
- Terraformの基本的なコマンド(
init
/apply
/destroy
など)の利用方法について - tfファイルの基本的な書き方について
Terraformを利用したことがある方であれば問題ないでしょう。
また、この記事は以下のような内容には触れず、より実装寄りの内容を中心とします。
- どんな場合にカスタムプロバイダーが必要なのか
- リソース提供側はどのようなAPIがあると望ましいのか
ということで早速本題に入っていきます。
目次(未確定)
1ポストでは収まらない量になりそうなので複数回に分けて投稿します。
今の所は以下のような感じになる予定です。(確定したら目次を更新します)
- 第1回: Terraform Custom Provider 基礎 (当記事)
- 第2回: リソース実装 基礎 -
schema.Resource
でのリソース実装の基礎 - 第3回: スキーマ定義 前編-
schema.Schema
でのスキーマ定義 - 第4回: スキーマ定義 後編-
schema.Schema
でのスキーマ定義 - 第5回: リソース操作 -
schema.ResourceData
でのリソース操作 - 第6回: リソース定義 -
schema.Resource
の高度な機能(差分調整/マイグレーション/インポート) - 第7回: テスティングフレームワーク
Terraform Custom Providerの基本
まずは前提知識としてTerraformでのプロバイダーの扱いについて押さえておきます。
前提知識: Terraformでのプロバイダーの扱い
Terraformはコアな処理を担当するTerraform本体とAWS/GCP/Azureといった各プラットフォームに依存した処理を担当するプロバイダーとで実行ファイルが分離されています。
Terraform本体は必要に応じてプロバイダーの実行ファイルを見つけだしてRPCを行います。
Terraform v0.11の時点ではRPCに
net/rpc
が使われています。 v0.12以降ではgRPCに切り替える方向に進んでいます。 参考: v0.12でのprotoファイル
プロバイダーの実行ファイルの探し方についてはこの記事の本筋から外れるため省略します。 細かい例外はありますが、ひとまず以下のルールを覚えておけばOKです。
- HashiCorp社が配布しているプロバイダーについては
terraform init
時に自動でインストールされる - サードパーティにより配布されているプロバイダーについては以下のディレクトリから検索される
参考: https://www.terraform.io/docs/extend/how-terraform-works.html#discovery
いくつか例を通じて動きを見ておきます。
HashiCorp社により配布されているプロバイダーのインストール
この例はHashiCorp社により配布されているArukasプロバイダーをインストールする例です。
tfファイルを用意しterraform init
を実行することでカレントディレクトリの.terraform/
配下にプラグインがインストールされます。
# tfファイルを作成 $ echo "provider arukas{}" > test.tf # プロバイダーをインストール $ terraform init # 確認 $ tree -a . . ├── .terraform │ └── plugins │ └── darwin_amd64 │ ├── lock.json │ └── terraform-provider-arukas_v1.0.0_x4 └── test.tf
なお、今回はtfファイルにprovider
ブロックを記載していますが、provider
ブロックは省略可能です。(プロバイダーの実装によります)
以下のようにいきなりリソース定義のみを書く形でもterraform init
実行時にtfファイルが解析され必要なプロバイダーの検出が行われます。
resource "arukas_container" "foobar" { name = "example" image = "nginx:latest" ports = { protocol = "tcp" number = "80" } }
サードパーティにより配布されているプロバイダーのインストール
次にサードパーティにより配布されているプロバイダーをインストールする例です。
Terraform v0.11時点ではサードパーティプロバイダーを自動でインストールする仕組みはまだありません。
なので、自分でプロバイダーの実行ファイルをダウンロードし、前述の検索対象ディレクトリに格納しておく必要があります。
以下はさくらのクラウド向けプロバイダーをダウンロードして利用する例です。
ここではプロバイダーの実行ファイルは~/.terraform.d/plugins
配下に格納します。
# プロバイダーの実行ファイルをダウンロード $ curl -sL -o provider.zip https://github.com/sacloud/terraform-provider-sakuracloud/releases/download/v1.6.1/terraform-provider-sakuracloud_1.6.1_darwin-amd64.zip # 展開して配置 $ unzip provider.zip ; rm provider.zip $ mv terraform-provider-sakuracloud_v1.6.1_x4 ~/.terraform.d/plugins/ # tfファイルを作成 $ echo "provider sakuracloud{}" > test.tf # プロバイダーをインストール $ terraform init # 確認 $ tree -a . . ├── .terraform │ └── plugins │ └── darwin_amd64 │ └── lock.json └── test.tf
先ほどと違い、.terraform
ディレクトリ配下にプラグインの実行ファイルが格納されないことに注意してください。
当記事ではサンプルのカスタムプロバイダーを作成し実際に動かして動作を確認していきます。
その際にこちらの方法を用いてカスタムプロバイダーをインストールし利用するようにします。
Custom Providerの実装
それでは本題のカスタムプロバイダーの実装について見ていきます。
最初に、公式ドキュメントのガイドの中にWriting Custom Providersというドキュメントが 用意されていますので目を通しておきます。
このドキュメントによると、カスタムプロバイダーを作成するには最低限以下が必要ということです。
- (1)
terraform.ResourceProvider
を返すfunc - (2)
plugin.Serve()
に(1)のfuncを指定して呼び出すエントリーポイント - (3)
terraform-provider-<プロバイダー名>
という名前でビルド
それぞれを詳しく見ていきます。実際に手を動かしながら確認出来るようにソース一式を以下に準備しました。
サンプルとしてterraform-provider-minimum
というカスタムプロバイダーを作成してみます。
以降はこのサンプルを元に各項目を見ていきます。
カスタムプロバイダーのサンプルのソースコードを取得
まずはサンプルをローカルマシンにクローンし、strucrure
ブランチをチェックアウトしてください。
# *GOPATHを設定していない場合は$HOME/goに読み替えてください* # ソース一式を取得 & structureブランチのチェックアウト $ go get github.com/yamamoto-febc/terraform-provider-minimum $ cd $GOPATH/src/github.com/yamamoto-febc/terraform-provider-minimum $ git checkout structure
以下のようなファイルが含まれているはずです。
$ tree . . ├── Gopkg.lock ├── Gopkg.toml ├── main.go └── minimum └── provider.go
それでは順番に見ていきます。
(1) terraform.ResourceProvider
を返すfunc
まずはminimum/provider.go
を見てみます。
package minimum import ( "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" ) // Provider returns a terraform.ResourceProvider. func Provider() terraform.ResourceProvider { return &schema.Provider{ ResourcesMap: map[string]*schema.Resource{ // ここにリソースの定義を書いていく }, } }
ここではterraform.ResourceProvider
を返すfuncProvider()
を定義しています。terraform.ResourceProvider
はinterfaceです。
https://github.com/hashicorp/terraform/blob/v0.11.8/terraform/resource_provider.go#L19
このインターフェースを実装することでTerraform本体とのやり取りが可能になります。
実際にはこのインターフェースを実装したstructschema.Provider
が用意されていますので、これを利用します。
https://github.com/hashicorp/terraform/blob/v0.11.8/helper/schema/provider.go#L25
現在はリソースの定義は空になっています。次回記事でここに定義を追記していきます。
(2) plugin.Serve()
に(1)のfuncを指定して呼び出すエントリーポイント
次にエントリーポイントであるmain.go
を見てみます。
package main import ( "github.com/hashicorp/terraform/plugin" "github.com/yamamoto-febc/terraform-provider-minimum/minimum" ) func main() { plugin.Serve(&plugin.ServeOpts{ ProviderFunc: minimum.Provider, }) }
plugin.Serve
に引数として(1)で定義したProvider()
を指定しています。
plugin.Serve()
を実行することで、Terraform本体とRPCでやり取りするための諸々を行ってくれます。
これによりカスタムプロバイダーの作成者はRPCについて意識することなくそれぞれが担当するリソースの操作に集中できます。
(3) terraform-provider-<プロバイダー名>
という名前でビルド
あとはgo build
コマンドでビルドするだけです。Terraformは前提知識の項で触れたようにカスタムプロバイダーを検出してくれる仕組みがありますが、
そのためにはカスタムプロバイダーの実行ファイルを特定のディレクトリに配置することに加え、
実行ファイルの名前をterraform-provider-<プロバイダー名>
という形式にしておく必要があります。
このため、ビルド時に-o
オプションで出力されるファイル名を指定する必要があります。
また、ビルドのために依存ライブラリを用意しておく必要があります。
今回はdep
で依存関係を管理するようにしておきましたので、dep ensure
を実行してからビルドするようにしてください。
# depがない場合は以下でインストール # go get -u github.com/golang/dep/cmd/dep $ dep ensure $ go build -o terraform-provider-minimum main.go
これでカレントディレクトリにterraform-provider-minimum
という実行ファイルが作成されているはずです。
余談1: カスタムプロバイダーでの依存ライブラリの管理方法
Terraform本体はgovendorを利用して依存ライブラリを管理していますが、 本体とカスタムプロバイダーは実行ファイルが分かれているためビルドさえできれば無理にgovendorを利用する必要はありません。
terraform-providers配下のプロバイダーにおいても統一されてない状況で、
AWS/GCP/AzureRMといったプロバイダーはgovendorを利用していますが、
Herokuプロバイダーなどではdep
が使われています。
動作確認
ビルドしたカスタムプロバイダーが動作するか確認します。
実際にtfファイルを作成しterraform init
を実行してみましょう。
terraform version
でminimumプロバイダーが表示されていればOKです。
# tfファイルを作成 $ echo "provider minimum{}" > test.tf # プロバイダーをインストール $ terraform init # 確認 $ terraform version Terraform v0.11.8 + provider.minimum (unversioned)
余談2: プロバイダーのバージョンについて
terraform version
を実行するとminimumプロバイダーのバージョンがunversioned
になっているのに気づかれたかもしれません。
実はプロバイダーの実装としてはバージョン情報を持っておらず、プロバイダーのファイル名からバージョン情報を取得しています。
先ほどArukasプロバイダーを自動でインストールする例が出てきましたが、そこではプロバイダーのファイル名は以下のようになっていました。
terraform-provider-arukas_v1.0.0_x4
プロバイダーのファイル名は正式には以下の形式を持ちます。
terraform-<プラグインのタイプ>-<名称>_v<プラグインのバージョン>_x<プラグインプロトコルのバージョン>
名称
プラグインの名称を指定します。
名称以降は省略可能です。(今回は省略されているからunversioned
になってます)プラグインのバージョン
プラグインのバージョンにはMAJOR.MINOR.PATCH
の形式でバージョンを指定します。
(参考: Versioning and Changelog)プラグインプロトコルのバージョン
プラグインプロトコルのバージョンはv0.11時点では4
しかありません。
今後プロバイダーが満たすべきインターフェースが変更された場合にはバージョンも変更されるはずです。
現状はx4
決め打ちでも良いですし、以下から取得することも可能です。
https://github.com/hashicorp/terraform/blob/v0.11.8/plugin/serve.go#L22
リリース時のバージョニング処理
HashiCorp社が配布しているプロバイダーの場合はリリース時にCIサーバによってバージョンが決定されファイル名が付与されます。
(CIにはTeamCityが利用されており、CI/CDパイプラインの中でCHANGELOG.mdの解析をしてリリースするバージョンを決定しています)
サードパーティのプロバイダーについてはこの辺の処理を自前で行う必要があります。
参考までに、さくらのクラウド向けプロバイダーではこの辺の処理を行なっていますので興味のある方はMakefileあたりから眺めてみてください。
余談3: ソースコードのレイアウト/パッケージ構成について
公式ガイドのWriting Custom Providersのexample
プロバイダーとminimum
プロバイダーとで
ソースコードのレイアウトが違うことにお気付きの方もいらっしゃるかと思いますが、これはterraform-providersでのレイアウトに合わせているからです。
公式ガイドでのサンプルexample
プロバイダーのソースコードレイアウト
. ├── main.go └── provider.go
minimum
プロバイダーのソースコードレイアウト
.
├── main.go
└── minimum # プロバイダー固有コードを格納するパッケージ(リソースを追加する際もここに追加する)
└── provider.go
公開予定の無い自作プロバイダーであればどちらを選んでも良いですが、特に理由がなければ後者のように別途パッケージを設けて
その中にプロバイダー固有のコードを置き、ルートディレクトリにはmain.go
だけおく形にしておくのが良さそうです。
ここまででカスタムプロバイダーとしての最低限の体裁が整いました。
しかし、リソースの実装が空ですので現時点では特に何もできない状態です。
実装は次回行います。
第1回 まとめ
今回はカスタムプロバイダーの基礎知識をおさえ、カスタムプロバイダーとしての最小限のコードを書いた上でビルドしてTerraformから利用してみました。
今回はリソースの実装が空のためtfファイルを書く機会もなかったですが、次回の記事でリソース実装を追加していくことでよりプロバイダーらしくしていきます。
以上です。