Oracle Resource Manager(ORM)でInfrastructure as a Codeする

諸事情でやったので記録を残しておく。自分用メモ。

# どこから始めればいいのか

結構これが難しい。[公式ドキュメント](https://docs.oracle.com/ja-jp/iaas/Content/ResourceManager/Concepts/resourcemanager.htm)はいつもの感じで宇宙語で書かれておりわけがわからん。
ざっくり理解するには個人的には[Architect ProfessionalのeLearning](https://mylearn.oracle.com/ou/course/oracle-cloud-infrastructure-architect-professional/122659)の第5章 "Deliver Infrastructure as a Code" が一番わかりやすいと思った。　

サンプルとして参照できるのは以下のものがある。あんまりこれ一発ドカン、というものがない。
- [Terraform Examples for Oracle Cloud - GitHub](https://github.com/oracle/terraform-examples) Oracle公式のサンプル。なんだけど割と内容が微妙。たぶんdeprecated。使えるものがあればパクる、という感じ
- [Terraform Provider for Oracle Cloud Infrastructure](https://github.com/oracle/terraform-provider-oci/tree/master/examples) こちらのほうが現行メンテナンスされてるものだと思う。最初のリポジトリに比べてサンプルのバリエーションも段違いに多い。多いんだけど、実際色々調べ始めるとあまり役に立たないこともままあり。こちらも同様に使えるものがあればパクる、あればね・・・という感じ
- [Qiitaの各エントリ](https://www.google.com/search?q=site:qiita.com+oraclecloud+terraform) 割と慣れてきたら個々困ったときに検索して探る感じ。最初から見ると内容が様々過ぎて時間を浪費するかも

慣れれば割と気にしなくなるんだけど。

# 作るときの環境

いきなりtfファイル作って延々とtry&errorでもいいのかもしれないけど、ローカルでざっくり作っておくことは目標が見えるのでいいのかもしれない。私は以下のような環境でやった。

- ローカル
    - VirtualBox: 手元で動く構成をざっくり作る用
- OCI
    - 開発用コンパートメント
        - 構成ぶっこ抜き用コンパートメント
        - デプロイテスト用コンパートメント
    - 本番用コンパートメント: ここは環境によって様々だと思うので割愛

[構成ぶっこ抜きについては後述](#サンプル3---実践的な構成)

# サンプル

ここからはサンプル見ながら説明させてほしい。

サンプルは以下からダウンロードできる

https://gitlab.com/mkiuchi/ocistack_sample

## サンプル1 - VCNとサブネット

サンプル: `01-vcn-only`

ORMのhello world的なテンプレート。1つのVCNと2つのサブネット、インターネットゲートウェイを作っている

2つ要素を入れている

- **変数の扱い**: 変数は `variables.tf` もしくは `vars.tf` で宣言し、デフォルト値を入れておく。この例では変数名 `target_region` にデプロイ先のリージョンを指定している。デフォルト値は東京リージョン。 参照するときは `"${var.変数名}"` で参照できる（が、場所によって指定の仕方が違うのでサンプル参照）
- **UI上での変数指定**: UIでユーザに選択させたい場合は、`schema.yaml` を作ればできる。OCI側で作ってる規定のリスト（例えばリージョンの一覧など）を指定することもできる。(参照: [スキーマ・ドキュメントを使用したコンソール・ページの拡張 - サポートされているタイプ(動的事前移入および制御)](https://docs.oracle.com/ja-jp/iaas/Content/ResourceManager/Concepts/terraformconfigresourcemanager_topic-schema.htm))

## サンプル2 - サンプル1+インスタンス2個

サンプル: `02-instances`

でインスタンスを作る

考慮すべきなのは以下のもの

- **ソースイメージの選択**: [ソースイメージの選択](#工夫1-ソースイメージの選択)を参照
- **ローカルファイルの流し込み**: [ローカルファイルの流し込み](#工夫2-ローカルファイルの流し込み)を参照

### 工夫

#### 工夫1. ソースイメージの選択

`source_id` にブートイメージのOCIDをベタ書きしてる例が多いがOracle配布のイメージは環境によって同じイメージでもOCIDが異なる可能性があるようで、`oci_core_images` を使ってフィルタしてあげるのがお作法くさい。[フィルタに複数のイメージが該当する場合はタイムスタンプが新しいもの順にソートされたリストが戻るっぽいので](https://github.com/oracle/terraform-provider-oci/issues/2102#issuecomment-2226448186)、配列の先頭のイメージOCIDを `source_id` に食わせてあげればいい。こんな感じ。

```hcl
data "oci_core_images" "supported_platform_config_shape_images" {
  compartment_id   = "${var.compartment_ocid}"
  shape            = "VM.Standard.E4.Flex"
  operating_system = "Oracle Linux"
  operating_system_version = "8"
}

resource "oci_core_instance" "publicinstance" {
    (略)
    source_details {
        source_type = "image"
        source_id = data.oci_core_images.supported_platform_config_shape_images.images[0]["id"]
    }
    (略)
}
```

逆に言うと上記の方法ではデプロイした日によって使われるイメージが変わる可能性があるので、それが嫌な場合は従来どおりイメージのOCIDをベタ書きするしかない。

また[ `instance_source_image_filter_details` は今の所バグ持ちらしい](https://github.com/oracle/terraform-provider-oci/issues/2102)ので使わないほうがいいっぽい。私の環境でも2024/7/28現在うまく動かなかった。

#### 工夫2. ローカルファイルの流し込み

##### 1. 合計32KBまでのファイルの流し込み

[`oci_core_instance`](https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/core_instance) の `user_data` は32KBまでの情報を格納することができるので、ここを使ってローカルファイルを流し込むことができる。設定ファイルなどはこれで対応することができる。サンプルでは `02-instances` の `publicinstance(ファイル 02-instamces/compute.tf, 02-instamces/cloudinit.tf, 02-instamces/publicinstance_cloudinit.template.yaml)` でこれをやっている。

上限32KBの根拠はここ(https://github.com/oracle/terraform-provider-oci/issues/635) 。実際32KB以上のデータを与えると、OCI Stack上のTerraform applyは失敗する。

##### 2. 合計32KB以上のファイルの流し込み

Pythonのwheelファイルなど合計サイズが32KB以上になる場合には以下の3つの方法がある。もしかしたらそれ以外にもあるかもしれない。

1. オブジェクトストレージ経由でインスタンスにファイルを送り込む
2. コードリポジトリやArtifact Registry経由でインスタンスにファイルを送り込む
3. [Terraformのprovider](https://developer.hashicorp.com/terraform/language/resources/provisioners/connection)を使う

もしTerraformの実行前に資材を事前展開することができるのであれば、"1. オブジェクトストレージ経由でインスタンスにファイルを送り込む", "2. コードリポジトリやArtifact Registry経由でインスタンスにファイルを送り込む"はほぼ同等の比較的軽い実装になると思うので、可能であればこれが1番目の選択肢になると思う。

資材の事前展開ができない場合 Terraform+cloud-init でなんとかせざるを得ないわけだけど、上記のどの方法を選んでも十分にめんどくさい。今回は "1. オブジェクトストレージ経由でインスタンスにファイルを送り込む" を選択した。理由は:

- "2. コードリポジトリやArtifact Registry経由でインスタンスにファイルを送り込む" を選択しなかった理由
    - リポジトリにterraformで自動的にリポジトリやファイルを送り込むという業務が現実的ではないと思えたから
- "3. terraformのprovider機能を使う" を選択しなかった理由
    - ssh暗号化キーを都度生成する必要がある
    - OCI管理のTerraformインスタンスとの接続性を確保する必要があるため、汎用性がないように思われた

"3. terraformのprovider機能を使う" は一見Terraformだけで完結して外部ストレージが必要ないので魅力的に見えるけど、実際にはsshキーを内部生成するか、外部から秘密鍵を与えなければいけなかったり（公開鍵ならともかく秘密鍵はやりたくない人多いと思う）、内部生成した場合は鍵を実質使い捨てにする必要があったり制約がある。裏技的な使い方だとは思う。努力に見合ったメリットがあまり得られないかなと感じた。なにか特殊な事情で 1, 2 が使えない場合の最後の手段なのかなと思う。HashiCorp自体も [provisoner はラストリゾート(他の方法が使えない場合の最後の手段)ですと言っている](https://developer.hashicorp.com/terraform/language/resources/provisioners/syntax#provisioners-are-a-last-resort)。

**(合計32KB以上のファイルの流し込み) 方策1. オブジェクトストレージ経由でインスタンスにファイルを送り込む**

今回実装しているのはこの方法。具体的には以下のような感じでやる。

- `02-instances/objectstorage.tf` Terraformでオブジェクトストレージバケットをつくり、ファイルをput
- インスタンス内のOCIコマンドでオブジェクトを取得。認証は動的グループのアクセスポリシーを作って(`02-instances/identity.tf`)対応している

メリットは Terraform 実行前の作業があまりいらないという点。空のコンパートメントを作っておけばズコーンと入れることができる。お手軽といえばお手軽なのかもしれない。

デメリットは以下の通り

- 資材のバージョン管理をStack内でやらなければいけないので、パッケージを集中管理してAuditしたりセキュリティクリアランスしたりといった、たぶん大きな企業とかで求められるかもしれないような要件を満たすには適していない
- Terraform内で扱うファイルは UTF-8 テキストファイルでなければいけないという制約があるため、バイナリファイルを渡したい場合は事前に base64 encode しておく必要がある。めんどくさい

サンプル内では以下のファイルでそのあたりをやっている。

- `02-instances/objectstrage.tf` - オブジェクトストレージバケットの作成、ファイルをput

    ここでオブジェクトストレージを作成して、ファイルを置いている。

- `02-instances/identity.tf` - 動的グループを作成してオブジェクトストレージへのアクセスポリシーを定義

    動的グループはテナンシー(rootコンパートメント)直下に作る必要があるっぽい。GUIで作ると気づかないんだけどそういうことらしい。

- `02-instances/privateinstance_cloudinit.template.yaml`, `02-instances/cloudinit.tf` - インスタンス内のOCIコマンドでオブジェクトを取得

    ここはcloud-initでゴリっと書く感じ。力技的な。

ハマりポイントとしては以下の点がある:

- Terraformの制約上、バイナリファイルを `oci_objectstorage_object` でオブジェクトストレージに入れることができない。したがってTerraform内に含める資材がバイナリファイルの場合、事前にLinuxの `base64` コマンドなどでBase64 encodeしておいて、それを転送しなければいけない。
    - terraform の `file()` 関数は[与えられたファイルをUTF-8テキストとして解釈する](https://developer.hashicorp.com/terraform/language/functions/file#:~:text=this%20function%20will%20interpret%20the%20file%20contents%20as%20UTF%2D8%20encoded%20text)ため、バイナリファイルを与えると正しく読み込まれない（エラーも出ない）
    - 同様に `filebase64()` 関数も使用できない。`filebase64()` は `file()` をベースに拡張されているのでバイナリファイルを正しく読むことができない制約は `filebase64()` でも同様になる。つまりTerraform側のファイルをバイナリファイルのまま置いて、`content = filebase64(<source_file_path>)` でオブジェクトストレージに転送すると、`terraform apply` は成功するものの正しく転送されず、転送先のファイルの中身は壊れている
    - インスタンスからは `oci` コマンドを `--auth instance_principal` をつけて、インスタンスプリンシパルとしてオブジェクトストレージからファイルを取得し、インスタンス内部の `base64` コマンドでバイナリファイルを復元している 
- cloud_initの中で実行されるociコマンド(`oci os object get`コマンド)はなぜかときどき失敗して0バイトのファイルを作ることがある。なぜかインスタンスにログインしてやり直したり、リトライすると正常に取得できる。サンプル内では取得するスクリプト内で正しく取得できていなかったらリトライするようにしている。

**(合計32KB以上のファイルの流し込み) 方策2. コードリポジトリやArtifact Registry経由でインスタンスにファイルを送り込む**

この方法は実装していない。

たぶん会社とかでガバナンス利かせる場合はこの方法がいいかも。Terraform実行前に資材を送り込んでいるのであれば、`git clone`などでリポジトリから取得できるのでとてもかんたん。

**(合計32KB以上のファイルの流し込み) 方策3. TerraformのProvisionerを使ってファイルを送り込む**

この方法は実装していない。

この方法ではTerraformが実行されるインスタンス(OCI側で自動的に実行される、ユーザが操作できないインスタンス)から、作成されたインスタンスに対してscpを実行してファイルを送り込む。

メリット

- オブジェクトストレージやリポジトリなどのインスタンスの外側のストレージが必要ない
- 事前の資材展開が必要ない
- (検証してないけど)[多段sshで踏み台サーバ経由で流し込む](https://developer.hashicorp.com/terraform/language/resources/provisioners/connection#connecting-through-a-bastion-host-with-ssh)、とかもいけるらしい

デメリット

- インスタンスに対してscpをする必要があるため、外部から公開鍵+秘密鍵を与えるか、Terraform内部で生成する必要がある
    - (多分)OCIのセキュリティポリシーの制約で、Terraform内部で生成した秘密鍵はTerraform実行ログに出力できない。具体的にはTerraform内部で生成した鍵情報には [sensitive フラグをつけること](https://developer.hashicorp.com/terraform/language/values/outputs#sensitive-suppressing-values-in-cli-output)を強制され(つけないとエラーになる)、sensitive フラグをつけることでoutputの出力ではマスクされる、という挙動になる
- OCI側が管理するインスタンスからVPC内にアクセスさせるため、外部アクセスができる必要がある。つまりインターネットゲートウェイや、必要であればセキュリティ設定などが要る。サービスゲートウェイ経由でも出来るのかもしれないけど未確認

このへん(https://github.com/fwiw6430/tutorial_cn/blob/master/instance.tf) とか見ると使い方がわかる気がする。

## サンプル3 - 実践的な構成

サンプル: `03-captured` および `04-rewrited`

上の２つは手間の割には出来上がる構成はとてもシンプル。悪く言うとシンプルすぎて実戦で使えるような構成ではない。ある日構成図を見せられてこれTerraform化しといてねよろしくと言われても、上記の方法では実装方法を調べる手間が膨大になってしまい途方に暮れることになってしまう。

で、そんな場合にコンパートメント内に作り込んだ構成をスキャンしてTerraformの雛形を作ってくれる機能がある。これが上で述べたぶっこ抜き機能。

[公式ドキュメントはこれ(https://docs.oracle.com/iaas/Content/ResourceManager/Tasks/create-stack-compartment.htm#top)。](https://docs.oracle.com/iaas/Content/ResourceManager/Tasks/create-stack-compartment.htm#top)

### 実際に構成を作って、ぶっこ抜いてみる

以下のような構成をGUIで作成して、作成後 OCI Stack から構成をぶっこ抜いてみる。

![ocistack.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52237/36a7f79a-a9f8-d47f-ea40-83653554ac0a.png)

ぶっこ抜いた構成はサンプルの `03-captured` に入っている。ただしssh keyとOCIDは架空のものに変更している。それ以外は抜いたままの状態。

見てもらうと気づくと思うが、結構冗長な記述になっているし欠落もある。例えば

- 自動的に割り当てられるプライベートIPが保存されるようになっている
- 必要のないインスタンスプラグインが列挙されている
- インスタンスのOS以降の構成はまったくの白紙。起動イメージは決め打ち(つまり構成を見てもインスタンスが何のOSで上がってくるかわからない)
- リージョン、AD(Availability Domain), FD(Fault Domain)、コンパートメントはOCID決め打ち
- 使われないバックアップポリシー、Cloud Guardレシピが定義されている
- schema.yamlがなく、ユーザからの入力はない。つまりコンパートメントやリージョンの指定ができない

など。つまり抜き取った構成は直さないと再利用可能なテンプレートにはなりませんよ、ということがわかる。

### 地道に書き直す

じゃあどうするかというと、私の場合は愚直に抜いた構成を上から下まで見直して、一つづつ直していく方法をした。

直すといっても、雛形の構成はあるので何を調べてばいいかは最適化することが出来る。例えばロードバランサーの場合：

- 構成ファイルは `load_balancer.tf`
- 使用しているコンポーネントは `oci_load_balancer_load_balancer`, `oci_load_balancer_backend_set`, `oci_load_balancer_backend`, `oci_load_balancer_listener` の4つ
- 調べるべきページは:
    - `oci_load_balancer_load_balancer` - https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/load_balancer_load_balancer
    - `oci_load_balancer_backend_set` - https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/load_balancer_backend_set
    - `oci_load_balancer_backend` - https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/load_balancer_backend
    - `oci_load_balancer_listener` - https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/load_balancer_listener
    - 各ページには必須(Required), 任意(Optional)の記載があるので、何を記載して何を省略すればよいか理解できる

という感じ。必死に調べたけどその情報は結局使いませんでした、的な時間の浪費を抑えることができる。

`03-captured` をもとに書き直したものを `04-rewrited` に入れている。ファイル構成はできるだけ抜いたファイル構成を踏襲した。[同じファイルを横に並べて見る](https://atmarkit.itmedia.co.jp/ait/articles/1806/08/news028.html)とどのように直しているかがわかりやすいと思う。

### 動作確認

実際に`04-rewrited`をデプロイして、ロードバランサのヘルスチェックがOKになるまで待つ(バックエンド(=プライベートインスタンス)の準備に5~10分程度時間がかかる)。パブリックインスタンスにログインして、`curl --XGET http://<ロードバランサのIP>/ping` と実行して、`"Pong"` という戻り値があればOK

# バグっぽい挙動

以下の挙動はバグっぽい

- `/home/opc` のオーナーが `root:root` になる。回避策はcloud_initの中でchownする
- `instance_source_image_filter_details` で指定したフィルタが効かない([参照](https://github.com/oracle/terraform-provider-oci/issues/2102)。回避策は `oci_core_images` を使う(上述))

# 参考にしたもの

- [Quickops: creating ssh key and inserting inside OCI instance using terraform - https://dev.to/aernesto24/quickops-creating-ssh-key-and-inserting-inside-oci-instance-creation-using-terraform-4ijd](https://dev.to/aernesto24/quickops-creating-ssh-key-and-inserting-inside-oci-instance-creation-using-terraform-4ijd)

# さいごに

~~別に手順書でよくね？~~ がんばって・・・