さくらのクラウドのTerraformプロバイダーv3を試してみた(v2からの変更点とハマりどころ)

さくらのクラウド , セキュリティ

こんにちは、ネットアシスト運用チームのhaokiです。

今回は、さくらのクラウド向けのTerraformプロバイダーv3を実際に触ってみました。

普通に構築してみた、で終わらせるつもりだったのですが、検証の途中で「Plan: 0 to add, 1 to change, 0 to destroy」という一見安全な表示のまま、サーバが約2分間停止するという挙動に遭遇しました。planの出力からはそれが一切読み取れません。

セットアップでハマったポイントとあわせて、実測結果を紹介します。

検証環境

項目バージョン
Terraformv1.15.8
プロバイダーsacloud/sakura v3.12.8
操作端末WSL2
ゾーンis1b(石狩第1)
サーバ1core / 1GB / SSD 20GB / Ubuntu

Terraformプロバイダーv3とv2は別物です

まず押さえておきたいのが、さくらのクラウド向けTerraformプロバイダーには現在2つのバージョンが存在するという点です。

  • v2sacloud/sakuracloud / 2026年12月末でメンテナンス終了予定
  • v3sacloud/sakura / Terraform Plugin Framework ベースの新実装

両者に互換性はありません。ただしTerraform Registry上では別プロバイダーとして公開されているので、v2で管理中の構成が勝手にv3へ更新されることはなく、新規に使いたいリソースだけv3を使うといった段階的な移行も可能です。

これから始めるなら v3 が推奨されています。ここで一つ問題があって、ネット上の日本語記事はほとんどがv2ベースです。参考にしてそのまま書くと、まず間違いなくエラーになります。

プロバイダー設定

terraform {
  required_providers {
    sakura = {
      source  = "sacloud/sakura"
      version = "~> 3.0"
    }
  }
}

provider "sakura" {
  zone = "is1b"
}

APIキーは環境変数で渡します。tfファイルへの直書きは、GitHubなどへの誤公開リスクがあるため非推奨とされています。

export SAKURACLOUD_ACCESS_TOKEN="アクセストークン"
export SAKURACLOUD_ACCESS_TOKEN_SECRET="アクセストークンシークレット"
export SAKURACLOUD_ZONE="is1b"

ちなみに筆者は、これらを ~/.sakura-env にまとめて chmod 600 し、必要なときだけ source する形にしています。.bashrc に書きっぱなしにするより漏洩経路が減るのでおすすめです。

$ terraform init
Initializing provider plugins...
- Finding sacloud/sakura versions matching "~> 3.0"...
- Installing sacloud/sakura v3.12.8...
- Installed sacloud/sakura v3.12.8 (self-signed, key ID 8D264CA32277CF9A)

Terraform has been successfully initialized!

ハマりどころ1:ブロックが属性になっている

v2の記事を参考に、サーバリソースをこう書きました。

resource "sakura_server" "web01" {
  name   = "web01"
  core   = 1
  memory = 1

  network_interface {          # ← v2の書き方
    upstream = "shared"
  }
}

結果はエラーです。

│ Error: Unsupported block type
│
│   36:   network_interface {
│
│ Blocks of type "network_interface" are not expected here.
│ Did you mean to define argument "network_interface"?
│ If so, use the equals sign to assign it a value.

v3はTerraform Plugin Frameworkで実装されているため、v2でブロック { } だったものが属性 = { } に変わっています。プレフィックスを sakuracloud_ から sakura_ に置換するだけでは動きません。

解決策:スキーマを直接見る

v3はまだ情報が少ないので、ドキュメントを探し回るよりプロバイダー自身が持つスキーマを見るのが早いです。

$ terraform providers schema -json > schema.json

$ jq '.provider_schemas["registry.terraform.io/sacloud/sakura"]
      .resource_schemas.sakura_server.block.attributes.network_interface' schema.json
{
  "nested_type": {
    "attributes": { ... },
    "nesting_mode": "list"
  },
  "optional": true
}

"nesting_mode": "list" とあるので、正解はリスト形式でした。一方 disk_edit_parameter は単一オブジェクトなので書き方が違います。同じ「属性化」でも形が2種類あるので、ここは推測せず確認したほうが早いです。

resource "sakura_server" "web01" {
  name   = "web01"
  core   = 1
  memory = 1
  disks  = [sakura_disk.web01.id]

  network_interface = [{        # リスト
    upstream = "shared"
  }]

  disk_edit_parameter = {       # 単一オブジェクト
    hostname        = "web01"
    ssh_key_ids     = [sakura_ssh_key.lab.id]
    disable_pw_auth = true
  }
}

このスキーマダンプ、型・必須/任意の別・説明文まで、実際にインストールしたバージョンの正確な情報が出てきます。password の説明に「12〜128文字」という制限まで書かれていて、これだけで実装できるレベルでした。新しいプロバイダーを触るときの定番手段として覚えておくと便利です。

おまけ:リソース名も変わっている

プレフィックスの変更だけでなく、リソース名自体がリネームされているものもあります。

v2v3
sakuracloud_load_balancersakura_dsr_lb
sakuracloud_proxylbsakura_enhanced_lb
sakuracloud_vpc_routersakura_vpn_router

利用可能なリソース一覧も、同じくスキーマから取れます。

$ jq '.provider_schemas["registry.terraform.io/sacloud/sakura"].resource_schemas | keys' schema.json

ハマりどころ2:パスワードがtfstateに平文で残る

書き方を修正して、無事に apply が通りました。所要時間はディスク作成が2分15秒、サーバ作成が1分42秒で、合計約4分です。SSHでログインできることも確認できました。

ただ、apply中にこんな警告が出ていました。

│ Warning: Available Write-Only Attribute Alternative
│
│   46:     password        = var.server_password
│
│ This attribute has a WriteOnly version disk_edit_parameter.password_wo
│ available. Use the WriteOnly version of the attribute when possible.

気になったのでstateファイルを確認してみます。

$ grep -o '"password"[^,]*' terraform.tfstate
"password": "**************"

$ ls -l terraform.tfstate
-rw-r--r-- 1 haoki haoki 6180 Aug 15 01:59 terraform.tfstate

サーバのOSログインパスワードが平文で保存されていました。しかもパーミッションは644なので、同じマシンの他ユーザーからでも cat するだけで読めてしまいます。

なお、ここで言うパスワードはAPIキーとは別物です。APIキーは環境変数で渡しているだけなのでstateには入りません。問題になっているのは、disk_edit_parameter で設定した「作成したサーバのOSログインパスワード」のほうです。

password_wo とは

Terraform 1.11で導入されたwrite-only属性です。値をプロバイダーに渡すだけで、stateファイルにもplanファイルにも記録しません。暗号化して隠すのではなく、そもそも書き込まないという考え方ですね。

ただ、記録しないということはTerraform側が現在の値を知らないということでもあります。そこで相棒になるのが password_wo_version です。

  disk_edit_parameter = {
    hostname            = "web01"
    password_wo         = var.server_password   # stateに残らない
    password_wo_version = 1                     # stateに残る
    ssh_key_ids         = [sakura_ssh_key.lab.id]
    disable_pw_auth     = true
  }

パスワードを変更するときは、値と一緒にversionの数字を1つ上げます。Terraformは「versionが変わった」という事実だけを見て「新しい値を送り直そう」と判断する仕組みです。中身を知らないまま更新できる、というわけです。

書き換えて適用したあと、あらためて確認します。

$ grep -o '"password[^,]*' terraform.tfstate
"password": null
"password_wo": null
"password_wo_version": 1

平文が消えました。

「緊急時にパスワードが分からなくなるのでは?」

これは筆者も最初に思ったので補足しておきます。

password_wo が消すのはtfstate上の記録だけで、サーバ本体にはパスワードが設定されたままです。SSHが繋がらなくなったときにコントロールパネルのコンソールから入る、といった用途はこれまで通り使えます。

保存場所passwordpassword_wo
サーバ本体(OS内)設定される設定される(変わらず)
tfstate平文で残る残らない
手元の管理場所あるある

そもそもtfstateは構成管理ファイルであって、パスワード台帳ではありません。値の正本はパスワードマネージャなどに置いて、Terraformには渡すだけ。緊急時はそちらから取得する、というのが本来の姿かと思います。

逆に言えば、パスワードの管理場所を用意しないまま _wo に移行すると本当に分からなくなりますので、そこはセットで考えてください。

ハマりどころ3:「0 destroyed」なのにサーバが止まる

さて、ここからが本題です。

先ほどの passwordpassword_wo の書き換えですが、applyの前にplanを確認していました。

Terraform will perform the following actions:

  # sakura_server.web01 will be updated in-place
  ~ resource "sakura_server" "web01" {
      ~ disk_edit_parameter = {
          - password            = (sensitive value) -> null
          + password_wo_version = 1
        }
        ...
    }

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

update in-place0 to destroy。リソースは作り直されず、その場で更新されるだけ。安全そうに見えますよね。

実行してみます。

sakura_server.web01: Modifying... [id=113801823663]
sakura_server.web01: Still modifying... [id=113801823663, 00m10s elapsed]
...
sakura_server.web01: Modifications complete after 1m49s [id=113801823663]

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

1分49秒。ちょっと長いなと思いつつSSHで接続すると、こうなりました。

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

SSHホスト鍵が変わっています。これは怪しい。

journalctlで裏を取る

サーバにログインして起動履歴を確認しました。

$ journalctl --list-boots
IDX BOOT ID                          FIRST ENTRY                 LAST ENTRY
 -1 cfca805cfc1a4827bdc4467a796677c1 Sat 2026-08-15 02:08:49 JST Sat 2026-08-15 02:14:05 JST
  0 1a7f1e3cac1b4ec4818e3107b57806ff Sat 2026-08-15 02:15:56 JST Sat 2026-08-15 02:18:52 JST

$ uptime
 02:17:07 up 1 min,  0 users,  load average: 0.59, 0.29, 0.11
  • 02:14:05 — 停止
  • 02:15:56 — 起動
  • ダウンタイム 1分51秒

サーバIDは 113801823663 のまま変わっていません。Terraform的には「同じリソースを更新しただけ」ですが、実際には内部でサーバ停止 → ディスク再編集 → 起動という処理が走っていたわけです。

disk_edit_parameter はディスク作成時の初期設定を注入する仕組みなので、後から変更するとディスクの再編集が必要になり、そのためにサーバを止める必要がある。理屈としては納得できます。問題は、それがplanの出力から一切読み取れないことです。

再現性を確認する

1回目がたまたまだった可能性もあるので、今度は password_wo_version1 から 2 に変更してみました。差分はたった1行です。

      ~ disk_edit_parameter = {
          ~ password_wo_version = 1 -> 2
        }

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

apply中に別ターミナルからSSHを試みると、こうなります。

ssh: connect to host 153.127.196.80 port 22: Connection refused

そして起動履歴です。

$ journalctl --list-boots
IDX BOOT ID                          FIRST ENTRY                 LAST ENTRY
 -2 cfca805cfc1a4827bdc4467a796677c1 Sat 2026-08-15 02:08:49 JST Sat 2026-08-15 02:14:05 JST
 -1 1a7f1e3cac1b4ec4818e3107b57806ff Sat 2026-08-15 02:15:56 JST Sat 2026-08-15 02:22:42 JST
  0 fb71d84c7aae45e6a38e6c45e46e04bb Sat 2026-08-15 02:24:02 JST Sat 2026-08-15 02:24:17 JST

02:22:42に停止、02:24:02に起動。ダウンタイムは1分20秒でした。

差分1行のin-place updateでも、しっかりサーバは止まります。再現性ありです。

よかった点

指摘ばかりになってしまったので、公平に書いておきます。v3プロバイダーは以下の点で好印象でした。

  • 余計な差分が出ない:何も変更せずに plan を実行すると、きちんと No changes. が返ります
  • 警告が親切password_wo の存在をちゃんと教えてくれました。無視した筆者が悪いです
  • スキーマが整備されている:説明文に文字数制限まで書かれていて、providers schema だけで実装できます
  • 再現性:destroy → apply で約4分。同じ構成が確実に戻ってきます

一点だけ気になったのは、plan出力で hostnamemac_address など実際には変わらない値まで (known after apply) と表示されることです。ノイズが多く、本当に変わる箇所が埋もれてしまう印象でした。

まとめ

  • さくらのクラウド向けTerraformプロバイダーはv3が推奨。ただしv2とは互換性がなく、既存の日本語記事はほぼv2ベースなのでそのままでは動きません
  • v3ではブロック { } が属性 = { } に変わっており、リストか単一オブジェクトかの違いもあります
  • disk_edit_parameter.password はtfstateに平文で残ります。password_wo を使えば記録されません
  • 0 destroyed は無停止を意味しません。in-place updateでも1〜2分のダウンタイムが発生しました

今回の検証でハマりやすいポイントをまとめると:

  • v2の記事をコピペしても動かない。書き方が変わっているので terraform providers schema -json で確認するのが確実
  • sakuracloud_load_balancersakura_dsr_lb のように、リソース名自体がリネームされているものがある
  • tfstateはパーミッション644で作られる。ローカルstateのまま放置しない。.gitignore への追加も必須
  • password_wo への移行作業自体がダウンタイムを伴う。「コード整理のついでに」やると事故ります
  • パスワード変更(versionのインクリメント)でも毎回停止する。運用設計に織り込んでおく
  • 停止を伴う変更かどうかはplanから読めない。本番反映の前に検証環境で同じ変更をapplyし、journalctl --list-boots で確認するのが確実です

「Terraformで管理しているから安全」ではなく、「planを読めるようになって初めて安全」ということを、身をもって確認した検証でした。

次回は、コントロールパネルから手動で設定変更を加えた場合に、v3プロバイダーがどこまでその差分(ドリフト)を検知できるのかを検証してみたいと思います。

後片付けも忘れずに。検証用のサーバとディスクは停止していても課金対象なので、終わったら terraform destroy で削除しておきましょう。

$ terraform destroy

さくらのクラウドを使ったサーバーの構築・監視・運用体制の整備についてご相談がある方は、ぜひネットアシストにご連絡ください。

それではまた!

参考リンク

この記事を書いた人

haoki

ネットアシスト運用チームのhaokiです。

【取得資格】

AWS Certified Solutions Architect - Associate

AWS Certified CloudOps Engineer - Associate

LPIC level2

Cisco Certified Network Associate Routing and Switching

さくらのクラウド検定