CI/CD とサイトの公開¶
ひとことで
このサイトは push がそのまま公開です。だからチェックは公開より前に置きます。ローカル PC で一度チェックし、ビルドの中でもう一度チェックし、どこかで失敗したらアップロードしません。 認証情報は持たずに済むなら持たない(OCI ではサービス自身の ID を使う)、どうしても必要なものはボールトに入れて権限を最小にする。Terraform が管理するのは「サイトを置く場所」だけで、サイトの中身はスクリプトで同期します。
このページは Phase 7 と Phase 10 の実験をまとめたものです。AWS 側(S3 / CloudFront)の設計は AWS との対比、権限設計の詳細は セキュリティ を参照してください。
1. まず構成要素を知る¶
| このリポジトリで使うもの | AWS / GitHub で近いもの | ひとこと |
|---|---|---|
| MkDocs(Material テーマ) | — | Markdown から静的な HTML サイトを生成するツール。このサイトもこれで生成しています |
| OCI DevOps のコード・リポジトリ(Code Repository) | CodeCommit / GitHub のプライベート・リポジトリ | マネージドな Git リポジトリ。このリポジトリ全体をここに push します |
| ビルド・パイプライン(Build Pipeline)+ マネージド・ビルド(Managed Build) | CodeBuild / GitHub Actions | build_spec.yaml の手順どおりにビルドを実行。runner(ビルドを実行する一時的なマシン)は OCI が用意し、ビルド中の使用量で課金されます |
| トリガー(Trigger) | Webhook / Actions の on: push |
「main ブランチに push があったらビルド・パイプラインを起動」 |
build_spec.yaml |
buildspec.yml / workflow の YAML |
ビルド手順の定義。リポジトリ直下に置きます |
| リソース・プリンシパル(Resource Principal) | CodeBuild の IAM ロール | サービス自身の ID。ビルドはこれで OCI の API を呼び出すので、API キーは不要です |
| 動的グループ(Dynamic Group) | (IAM ロールの信頼関係) | 「どのリソースを 1 つのグループとみなすか」のルール。このグループにポリシーで権限を与えます |
| オブジェクト・ストレージ(Object Storage)のバケット | S3 バケット | サイトのファイルを置く場所。このリポジトリの OCI 版はバケットを公開して、そのままサイトとして使っています |
| ボールト(Vault)のシークレット(Secret) | Secrets Manager | パスワードやキーを保管する場所。このリポジトリでは AWS のアクセス・キーを入れています |
| 認証トークン(Auth Token) | Personal Access Token | git push のときにパスワードの代わりに使うトークン(HTTPS) |
2. 仕組み:push のあと何が起きるか¶
flowchart TB
pc["① ローカル PC<br/>push 前チェック<br/>→ 人が確認 → git push"] -->|"HTTPS + 認証トークン"| repo["② コード・リポジトリ"]
repo -->|"トリガー:main への push"| s1
subgraph build["③ ビルド・パイプライン(マネージド・ビルドの runner)"]
direction TB
s1["チェック<br/>Terraform<br/>日本語版コピー<br/>ツールのテスト"] --> s2["ビルド<br/>mkdocs build --strict<br/>検索インデックス<br/>のチェック"]
s2 --> s5["OCI にアップロード"] --> s6["AWS にアップロード"]
end
s5 -->|"リソース・プリンシパル"| oci["オブジェクト・ストレージ<br/>OCI 版"]
s6 -->|"ボールト内の<br/>アクセス・キー"| aws["S3 + CloudFront<br/>AWS 版"]
3 つの原則:
- 品質ゲートはアップロードの前:Terraform のチェック、日本語版のコピー、strict ビルド、検索インデックスのどれかが失敗すると、後ろのアップロードは実行されません → 壊れたサイトは公開されません
- AWS へのアップロードは最後:AWS のステップが失敗しても、OCI 版はすでに更新済みで影響を受けません
- コンテンツは Terraform に入れない:サイトのファイルはビルドのたびに変わるため、Terraform に入れると plan に毎回差分が出ます。Terraform が作るのはバケット、パイプライン、権限だけで、ファイルはスクリプトで同期します
3. サイト自体の構成(MkDocs + 多言語)¶
| やり方 | 理由 |
|---|---|
xxx.md = 中国語、xxx.ja.md = 日本語(mkdocs-static-i18n プラグイン。日本語訳のないページは中国語で表示) |
1 つのリポジトリから 2 言語のサイトを出す |
ページ内のコードは --8<-- "07-site/main.tf" でリポジトリのファイルを直接埋め込む |
コードは 1 か所だけなので、サイトと実際のコードが食い違わない |
日本語ページは i18n/ja/ のコピーを埋め込む:翻訳するのはコメントだけで、コードは完全に同じ。tools/check_ja_code.py でチェック |
日本語の読者は日本語のコメントで読める。コピーと元ファイルのコードが食い違えばビルドが失敗する |
use_directory_urls: false(topics/oke/index.html ではなく topics/oke.html を生成) |
オブジェクト・ストレージはディレクトリにアクセスされても index.html を返さない |
検索プラグインに lang: [en, zh, ja] を明示し、i18n プラグインは reconfigure_search: false |
i18n プラグインが検索を再設定すると一部ページのメイン・レコードが消え、日本語ページの章の検索結果が undefined リンクになる(第 11 節) |
4. OCI DevOps の権限:リソース・プリンシパル¶
# 07-site/devops.tf(抜粋):tf-lab 内のビルド・パイプラインはすべてこのグループ
resource "oci_identity_dynamic_group" "site_build" {
matching_rule = "ALL {resource.type = 'devopsbuildpipeline', resource.compartment.id = '${var.compartment_ocid}'}"
}
# このグループに与える権限(ポリシーは tf-lab コンパートメントに付ける)
Allow dynamic-group tflab-site-build to manage objects in compartment tf-lab where target.bucket.name = 'tflab-site'
Allow dynamic-group tflab-site-build to read devops-repository in compartment tf-lab
Allow dynamic-group tflab-site-build to use ons-topics in compartment tf-lab
Allow dynamic-group tflab-site-build to read secret-bundles in compartment tf-lab where target.secret.id = '<その 1 つの Secret>'
- 保管すべき鍵がない:ビルド自体が ID を持っています。GitHub Actions を使うなら、OCI の API キーを GitHub の Secrets に入れる必要があります
- できることは、サイト用バケット内のオブジェクトの読み書き・削除、コード・リポジトリの読み取り、ビルド通知の送信、AWS キーが入ったシークレット 1 つの読み取りだけです。state のバケットやバケット自体には触れません
- スクリプト側で認証方法を切り替えます:ローカル PC では
~/.oci/config、ビルドの中では--auth resource_principal
repos は DevOps のコード・リポジトリではない
最初は read repos と書いていて、ビルドが Unable to fetch build_spec file ... RelatedResourceNotAuthorizedOrNotFound で失敗しました。
repos はコンテナ・レジストリ(OCIR)のリソース・タイプで、DevOps のコード・リポジトリは devops-repository です。
切り分け方:一時的に manage devops-family in compartment tf-lab を与える → 成功 → 動的グループとポリシーの書き方は正しく、問題はリソース・タイプだとわかる。確認後は最小権限に戻しました。
5. build_spec.yaml のステップ設計¶
| ステップ | 内容 | 失敗したとき |
|---|---|---|
| Setup Python | 3.11 以上の Python を探す(なければインストール)→ venv → バージョン固定の依存をインストール | 公開しない |
| Terraform checks | fmt -check → 各ルート・モジュールで init -backend=false + validate → tflint |
公開しない |
| Check site tools and ja code copies | ツールの単体テスト、日本語版コードのコピーの一致 | 公開しない |
| Build site | mkdocs build --strict(警告があれば失敗)→ 検索インデックスのチェック |
公開しない |
| Upload to Object Storage | OCI のバケットに同期 | — |
| Upload to AWS | S3 に同期 + CloudFront のキャッシュを削除 | OCI 版は更新済みで影響なし |
設計のポイント(build_spec.yaml の抜粋):
env:
variables:
# runner イメージの pip のグローバル設定に Oracle 内部のプロキシが書かれている(runner 内では名前解決できない)
# → pip に設定ファイルを一切読ませず、PyPI に直接つなぐ
PIP_CONFIG_FILE: "/dev/null"
steps:
- type: Command
name: Terraform checks
command: |
set -euo pipefail
TF_VERSION=1.16.6 # runner に terraform がない → ローカル PC と同じ固定バージョンをダウンロード
# ...
terraform -chdir="${d}" init -backend=false -input=false -no-color >/dev/null # state にはつながない。認証も不要
terraform -chdir="${d}" validate -no-color
- ステップごとに別の shell:venv は
activateに頼らず、絶対パス(/tmp/venv/bin/python)で呼び出します set -euo pipefail:どのコマンドが失敗してもすぐ止まります。変数が渡っていないとき(set -u)もそのままエラーになり、黙ってスキップされません- バージョンはすべて固定(Terraform、tflint、Python パッケージ):ローカル PC と同じにして、今日通ったチェックが明日も通るようにします
- サイトのページに
build_spec.yamlを埋め込むので、AWS のバケット名、ディストリビューション ID、シークレットの OCID は中に書かず、ビルド・パイプラインのパラメータにしています(Terraform で定義し、ビルド時に環境変数になる)
6. オブジェクト・ストレージを静的サイトとして使う¶
| 制約 | 対策 |
|---|---|
ディレクトリにアクセスしても index.html を返さない |
use_directory_urls: false |
| Content-Type はアップロード時に指定する(指定しないと HTML がダウンロード扱いになる) | アップロード・スクリプトで拡張子ごとに固定(Windows の mimetypes はレジストリを読むので、マシンによって結果が変わる) |
| 存在しないページはオブジェクト・ストレージ自身の JSON が返る | 対策なし(AWS 版では MkDocs の 404 ページを表示できる) |
| URL に必ずネームスペース(namespace)が含まれる | 公開サイトである以上は隠せない。state のバケットは NoPublicAccess なので、ネームスペースがわかっても読めない |
- バケットの
access_type = "ObjectReadWithoutList":匿名でファイルは読めますが、ファイルの一覧は取れません(ObjectReadだとファイル名の一覧まで公開されます) - アップロード・スクリプト
tools/upload_site.py(AWS 版のupload_site_s3.pyも同じルール):- MD5 で変わったファイルだけを送る → API リクエストを節約(Always Free は月 5 万回)。内容が変わっていなければ
uploaded 0 / unchanged 72 - バケットにあって
site/にないファイルは削除する(ページ名を変えても古いページが残らない) - Cache-Control:HTML は
no-cache(公開後すぐに反映)、それ以外は 1 時間キャッシュ
- MD5 で変わったファイルだけを送る → API リクエストを節約(Always Free は月 5 万回)。内容が変わっていなければ
- OCI CLI や
aws s3 syncを使わない理由:ローカル PC に OCI CLI をインストールできない(Windows のパス長の上限)。aws s3 syncは Content-Type を推測で決め、「変わったかどうか」をサイズと時刻で判断する(ビルドのたびに時刻が変わる → 毎回全部送り直し)
7. クラウドをまたぐ公開:OCI のビルドから AWS にアップロード¶
OCI のビルド runner は AWS のリソースではないので、AWS の IAM ロールは使えず、IAM ユーザーのアクセス・キー(長期間有効)を発行するしかありません。そのぶん別の手段で補います:
| 対策 | やり方 |
|---|---|
| 権限は最小 | サイト用バケットのオブジェクトの一覧・書き込み・削除と、このディストリビューションのキャッシュ削除だけ。オブジェクトの読み取り(GetObject)すらない |
| キーは Terraform を通さない | aws_iam_access_key の secret は state に平文で残る → スクリプトでキーを作り、そのまま OCI ボールトのシークレットに書き込む |
| キーを環境変数に入れない | アップロード・スクリプトがリソース・プリンシパルでシークレットを読む。キーはプロセスのメモリにあるだけで、ログには出ない |
| 定期的にローテーション | tools/rotate_aws_deploy_key.py:新しいキーを作る → シークレットに書き込む → 成功してから古いキーを削除 |
シークレットは Terraform では「入れ物」だけを作ります:
# 07-site/aws.tf(抜粋)
resource "oci_vault_secret" "aws_deployer" {
secret_content {
content_type = "BASE64"
content = base64encode(jsonencode({ placeholder = true })) # プレースホルダー
}
lifecycle {
ignore_changes = [secret_content] # 本当の値はスクリプトが書き込む。この行がないと次の apply でプレースホルダーに戻される
}
}
IAM ユーザーの権限設計とキーのローテーションは セキュリティ、S3 / CloudFront 側は AWS との対比 を参照してください。
8. push の前後の流れ¶
| タイミング | やること | ツール |
|---|---|---|
| push 前 | 日本語版コピーの一致、terraform fmt、strict ビルド、検索インデックス、機密情報のスキャン(公開サイト + push するファイル)、表の中のコードの縦棒、日本語ページの不足 |
tools/prepublish_check.py → NG 0 のときだけ先へ進む |
| push 前 | 人が確認:push する commit、チェック結果、更新されるページ | — |
| push | git push oci main |
— |
| push 後 | ビルドの終了を待つ。成功したら変更したページを URL に変換し、1 つずつ 200 を確認 | tools/wait_build.py --since <push 前の commit> |
- 機密情報のスキャンで探す値(ネームスペース、OCID、ユーザー名など)は実行時にローカル PC の OCI 設定と SDK から読み出し、リポジトリには書きません
- ビルドの中でも同じチェックの一部(日本語版コピー、strict ビルド、検索インデックス)を実行します:ローカル PC で見逃しても、ビルドで止められます
- これらのチェックはどれも「以前見逃したもの」から生まれました:
build_spec.yamlを変えて日本語版のコピーの同期を忘れた、表の中にバックスラッシュが入った、ページに本物の IP やメール・アドレスを書きかけた、など
9. なぜこの組み合わせなのか¶
| 決定 | 選んだもの | 選ばなかったもの | 理由 |
|---|---|---|---|
| サイトの置き場所 | OCI オブジェクト・ストレージ(+ AWS 版) | GitHub Pages | GitHub Pages の無料プランはリポジトリの公開が必要 → リポジトリ全体と Git の履歴は公開したくない |
| CI/CD | OCI DevOps | GitHub Actions | 利用中のネットワークでブラウザから GitHub がブロックされている。コマンドラインからはつながるが、回避はしない(ブロックされているのは、ネットワークの管理者が許可していない可能性があるため) |
| ビルドの認証情報 | リソース・プリンシパル | API キーを CI の Secrets に入れる | 保管すべき鍵がない |
| git push の認証 | HTTPS + 認証トークン(コンソールで手動作成) | SSH / Terraform で認証トークンを作成 | 利用中のネットワークで 22 番ポートがブロックされている。Terraform で作るとトークンの原文が state に入る |
| サイトのコンテンツ | スクリプトで同期(MD5 比較) | Terraform でファイルごとに管理 | 中身は毎回変わるので、plan に毎回差分が出る |
| クラウドをまたぐ認証情報 | アクセス・キーを OCI ボールトに入れ、スクリプトでローテーション | キーをビルドの環境変数に入れる / Terraform でキーを作成 | ログ / state / ローカル PC のディスクに現れない |
| チェックの場所 | ローカル PC(push 前)+ ビルドの中 | ビルドの中だけ | push = 公開。ビルドで失敗した時点で、機密情報はもうリポジトリに入っている |
10. 検証¶
| 確かめたいこと | やり方 | 結果 |
|---|---|---|
| サイトに匿名でアクセスできる | curl で匿名アクセス | 200 text/html; charset=utf-8 |
| ファイルの一覧は公開されない | curl でバケットの一覧 URL にアクセス | 404(ObjectReadWithoutList が効いている) |
| 前のステップが失敗したら公開されない | Phase 7 の構築中に実際に起きた失敗(build_spec を読めない、pip がつながらない) | ビルド FAILED、アップロードのステップは実行されず(チェック系のステップ自体が公開を止めたケースはまだ実際には起きていない) |
| main だけが公開をトリガーする | テスト・ブランチで手動ビルド(SDK の create_build_run)を実行してパイプラインのパラメータを確認 |
テスト・ブランチへの push ではトリガーされず、サイトは更新されない |
| AWS のキーは必要なことしかできない | deployer のキーで実際に呼び出す | サイト用バケットへの書き込み、キャッシュ削除は成功。オブジェクトの読み取り、state バケットへのアクセス、ポリシーの変更はすべて AccessDenied |
| 公開後のページが正常 | wait_build.py で変更したページを 1 つずつ確認 |
200 |
| 中身が変わっていなければ送り直さない | アップロード・スクリプトを 2 回続けて実行 | 2 回目は uploaded 0 |
11. ハマりどころ¶
| 現象 | 原因 | 対策 |
|---|---|---|
Unable to fetch build_spec file ... RelatedResourceNotAuthorizedOrNotFound |
ポリシーに repos(OCIR のリソース・タイプ)と書いていた |
read devops-repository |
runner 内で pip → ProxyError ... Name or service not known |
runner イメージの pip のグローバル設定に Oracle 内部のプロキシが書かれている | PIP_CONFIG_FILE=/dev/null。推測で直さず、先に診断出力(pip config list など)を入れてから修正 |
| 最初の push が拒否される(non-fast-forward) | リポジトリ作成時に空の "Initial Commit" が自動で作られる | git push --force(その空の commit だけを上書き) |
default_branch が毎回 plan で差分になる |
API 側では refs/heads/main として保存される |
コードに refs/heads/main と書く |
git push が止まる:auto-detection of host provider took too long |
Git Credential Manager がサービスの種類を判定する処理がタイムアウト | git config credential.<URL>.provider generic |
| コンソールでビルドのログが見えない | ログ(service = devops)を作っていない | Terraform でロググループ + ログを作成。ビルド終了から約 1 分後に検索できる |
| runner に terraform / tflint / unzip がない | イメージに入っていない | 公式から固定バージョンをダウンロードし、Python の zipfile で解凍して chmod +x |
build_spec.yaml を変えたら、CI の日本語版コピーのチェックが失敗 |
i18n/ja/build_spec.yaml を同期していない |
2 つのファイルを一緒に変更。push 前に prepublish_check.py を実行 |
日本語ページの章の検索結果が undefined リンク |
i18n プラグインが検索を再設定するとき、一部ページのメイン・レコードが消える | reconfigure_search: false + check_search_index.py でチェック |
チェック・ツールが Windows で UnicodeEncodeError |
ターミナルが cp932 で、中国語を出力できない | ツールの冒頭で sys.stdout.reconfigure(encoding="utf-8")。直したら Windows で実際に一度実行する |
12. 確認問題¶
1. なぜ push の前にチェックするのですか。ビルドの中のチェックだけでは足りませんか?
このリポジトリは push がそのまま公開です。しかも機密情報は push した時点でリポジトリの Git 履歴に入ってしまうので、ビルドで見つけても手遅れです。ビルドの中のチェックは 2 番目の防御線です。
2. GitHub Actions から OCI に公開する場合と OCI DevOps の場合、認証情報の面で何が違いますか?
GitHub Actions は OCI のリソースではないので、OCI の API キーを GitHub の Secrets に保管する必要があります。OCI DevOps のビルド・パイプラインは自分自身の ID(リソース・プリンシパル)を持っていて、動的グループ + ポリシーで権限を与えるので、保管すべき鍵がありません。
3. なぜサイトのファイルを Terraform で管理しないのですか?
サイトのファイルはビルドのたびに変わるので、Terraform に入れると plan に毎回差分が出て、state も大きくなります。Terraform はバケット、パイプライン、権限といったインフラだけを管理し、中身はスクリプトで同期します。
4. AWS へのアップロードにはなぜアクセス・キーしか使えないのですか。リスクはどう下げますか?
OCI のビルド runner は AWS のリソースではないので、IAM ロールを使えません。補う手段は、権限を最小にする(オブジェクトを読めない、ほかのバケットに触れない)、キーを Terraform に通さない(state に入れない)、OCI ボールトに入れてビルドだけが読めるようにする、環境変数に入れない、定期的にローテーションする、です。
5. AWS のバケット名をなぜ build_spec.yaml に書かないのですか?
サイトのページに build_spec.yaml を埋め込むので、書くと公開されてしまいます。ビルド・パイプラインのパラメータにして、ビルド時に環境変数として渡します。
6. ポリシーに read repos と書いたらビルドが build_spec.yaml を読めません。ポリシーの書き方の誤りか、リソース・タイプの誤りか、どう見分けますか?
一時的に大きめの権限(manage devops-family in compartment tf-lab)を与えます。それで成功すれば、動的グループとポリシーの構造は正しく、問題はリソース・タイプ(repos は OCIR のもの)だとわかります。確認したら最小権限に戻します。
7. シークレットの中身を Terraform ではプレースホルダーにして、さらに ignore_changes を付けるのはなぜですか?
本当の値を Terraform に通すと state に入ってしまうからです。本当の値はスクリプトが書き込みますが、ignore_changes がないと、次の apply でプレースホルダーに戻されてしまいます。
8. アップロード・スクリプトはなぜ毎回全部アップロードせず、MD5 で比較するのですか?
Always Free のオブジェクト・ストレージは API リクエストが月 5 万回までです。MD5 で変わったファイルだけを送れば、中身が変わっていないときはアップロードが 1 回も発生しません。
コード全文¶
build_spec.yaml
# OCI DevOps のビルド・パイプライン(07-site/devops.tf)が実行する内容
# main に push される → ここのステップがマネージド・ビルドのランナー(OL8)で順番に実行される
# ステップごとに別々のシェル → venv は activate に頼らず、絶対パスで呼び出す
version: 0.1
component: build
timeoutInSeconds: 900
shell: bash
env:
variables:
NO_MKDOCS_2_WARNING: "1" # Material のメンテナンス終了の通知を表示しない
PIP_DISABLE_PIP_VERSION_CHECK: "1"
# ランナー・イメージの pip のグローバル設定に Oracle 社内のプロキシ(www-proxy-hqdc.us.oracle.com、名前解決できない)が書かれている
# → pip にどの設定ファイルも読ませず、PyPI に直接つなぐ(pip の仕様:PIP_CONFIG_FILE=os.devnull なら設定を読み込まない)
PIP_CONFIG_FILE: "/dev/null"
steps:
- type: Command
name: Setup Python
timeoutInSeconds: 600
command: |
set -euo pipefail
cd "${OCI_PRIMARY_SOURCE_DIR}"
# mkdocs-material 9.8 は Python 3.11+ が必要。OL8 の標準版が条件を満たさない場合はインストール
PY=""
for p in python3.12 python3.11 python3; do
if command -v "$p" >/dev/null && "$p" -c 'import sys; sys.exit(sys.version_info < (3, 11))'; then
PY="$p"; break
fi
done
if [ -z "$PY" ]; then
dnf install -y -q python3.11
PY=python3.11
fi
"$PY" --version
"$PY" -m venv /tmp/venv
/tmp/venv/bin/pip install -r requirements.txt oci==2.189.0 boto3==1.35.49
# CI:Terraform のチェック(通らなければサイトは公開しない)
# ランナーには terraform / tflint がない → 公式サイトから固定バージョンをダウンロード(ローカル PC と同じ)
# validate に認証は不要:init -backend=false(state のバケットにはつながない)。provider はダウンロードするだけで設定はしない
- type: Command
name: Terraform checks
timeoutInSeconds: 600
command: |
set -euo pipefail
cd "${OCI_PRIMARY_SOURCE_DIR}"
TF_VERSION=1.16.6
TFLINT_VERSION=0.64.0
mkdir -p /tmp/bin
curl -sSfL -o /tmp/terraform.zip "https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip"
curl -sSfL -o /tmp/tflint.zip "https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/tflint_linux_amd64.zip"
# unzip があるとは限らない → Python の zipfile で展開(展開後は実行権限が消えるので chmod が必要)
/tmp/venv/bin/python -m zipfile -e /tmp/terraform.zip /tmp/bin
/tmp/venv/bin/python -m zipfile -e /tmp/tflint.zip /tmp/bin
chmod +x /tmp/bin/terraform /tmp/bin/tflint
export PATH="/tmp/bin:${PATH}"
terraform version
tflint --version
terraform fmt -check -recursive -diff .
# ルート・モジュール(05-resource-manager/stack は除外:モジュールは zip 作成時に入れるため)
export TF_PLUGIN_CACHE_DIR=/tmp/tf-plugin-cache # oci provider のダウンロードは 1 回だけ
mkdir -p "${TF_PLUGIN_CACHE_DIR}"
for d in 01-network 02-compute 03-modules/bootstrap 03-modules/env 04-3tier/env 06-oke 07-site 08-guard 09-secops 10-aws-site/bootstrap 10-aws-site/site; do
echo "== validate ${d}"
terraform -chdir="${d}" init -backend=false -input=false -no-color >/dev/null
terraform -chdir="${d}" validate -no-color
done
# tflint:ルート・モジュール + モジュール(i18n/ja のコピーは完全なモジュールではない → チェックしない)
for d in 01-network 02-compute 03-modules/bootstrap 03-modules/env 03-modules/modules/compute 03-modules/modules/network \
04-3tier/env 04-3tier/modules/compute 04-3tier/modules/lb 04-3tier/modules/network 06-oke 07-site 08-guard 09-secops 10-aws-site/bootstrap 10-aws-site/site; do
echo "== tflint ${d}"
tflint --chdir="${d}" --config="${PWD}/04-3tier/.tflint.hcl"
done
- type: Command
name: Check site tools and ja code copies
command: |
set -euo pipefail
cd "${OCI_PRIMARY_SOURCE_DIR}"
/tmp/venv/bin/python -m unittest discover -s tests
/tmp/venv/bin/python tools/check_ja_code.py
- type: Command
name: Build site
command: |
set -euo pipefail
cd "${OCI_PRIMARY_SOURCE_DIR}"
/tmp/venv/bin/mkdocs build --strict
/tmp/venv/bin/python tools/check_search_index.py
- type: Command
name: Upload to Object Storage
command: |
set -euo pipefail
cd "${OCI_PRIMARY_SOURCE_DIR}"
/tmp/venv/bin/python tools/upload_site.py --auth resource_principal
# Phase 10-4:AWS 版(S3 + CloudFront)にもアップロードする
# AWS_* は build pipeline のパラメータ(07-site/devops.tf)→ このファイルには書かない(サイトのページに埋め込まれるため)
# Access Key は OCI Vault の Secret にあり、スクリプトが Resource Principal で読む(環境変数にもログにも出ない)
# set -u:パラメータが渡らなかった場合はここでエラーにする(黙ってスキップしない)
- type: Command
name: Upload to AWS
command: |
set -euo pipefail
cd "${OCI_PRIMARY_SOURCE_DIR}"
/tmp/venv/bin/python tools/upload_site_s3.py \
--bucket "${AWS_SITE_BUCKET}" \
--distribution-id "${AWS_DISTRIBUTION_ID}" \
--creds-secret-id "${AWS_CREDS_SECRET_ID}"
07-site/devops.tf(DevOps 一式 + 動的グループ / ポリシー)
# ------------------------------------------------------------
# Phase 7-2:OCI DevOps による自動公開(push → ビルド → アップロード)
# ローカル PC で git push(HTTPS + 認証トークン)→ コード・リポジトリ(Code Repository)
# → トリガー(main)→ ビルド・パイプライン(build_spec.yaml:チェック → mkdocs build → アップロード)
# 認証:リソース・プリンシパル(Resource Principal、ビルド・パイプライン自身の ID)→ API キーは発行しない
# GitHub を使わない理由:利用中のネットワークでブラウザから GitHub がブロックされているため
# ------------------------------------------------------------
# tf-lab の親 = テナンシ(動的グループはテナンシ直下にしか作れない)
data "oci_identity_compartment" "lab" {
id = var.compartment_ocid
}
# DevOps プロジェクトには通知トピックの指定が必須(ビルドの成功 / 失敗などのイベントが届く。サブスクライブしなくてもよい)
resource "oci_ons_notification_topic" "devops" {
compartment_id = var.compartment_ocid
name = "${var.project_name}-site-devops"
description = "DevOps project events for ${var.project_name}-site"
freeform_tags = local.common_tags
}
resource "oci_devops_project" "site" {
compartment_id = var.compartment_ocid
name = "${var.project_name}-site"
description = "Build and publish the MkDocs study site"
notification_config {
topic_id = oci_ons_notification_topic.devops.id
}
freeform_tags = local.common_tags
}
# ビルドのログ:ログを作らないとコンソールでビルドの出力が見られない
resource "oci_logging_log_group" "devops" {
compartment_id = var.compartment_ocid
display_name = "${var.project_name}-site-devops"
freeform_tags = local.common_tags
}
resource "oci_logging_log" "devops" {
log_group_id = oci_logging_log_group.devops.id
display_name = "${var.project_name}-site-devops"
log_type = "SERVICE"
is_enabled = true
retention_duration = 30 # 日(最短 30)
configuration {
source {
category = "all"
resource = oci_devops_project.site.id
service = "devops"
source_type = "OCISERVICE"
}
}
freeform_tags = local.common_tags
}
# プライベートな Git リポジトリ(このリポジトリ全体をここに push する)
resource "oci_devops_repository" "site" {
project_id = oci_devops_project.site.id
name = "oci-terraform"
description = "OCI x Terraform study repo (code + MkDocs site)"
repository_type = "HOSTED"
default_branch = "refs/heads/main" # "main" と書くと API が refs/heads/main として保存する → plan のたびに差分が出る
freeform_tags = local.common_tags
}
resource "oci_devops_build_pipeline" "site" {
project_id = oci_devops_project.site.id
display_name = "${var.project_name}-site-build"
description = "check -> mkdocs build -> upload to ${oci_objectstorage_bucket.site.name} and AWS S3"
# Phase 10-4:build_spec.yaml の「Upload to AWS」で使う(ビルド時に環境変数として渡される)
build_pipeline_parameters {
items {
name = "AWS_SITE_BUCKET"
default_value = var.aws_site_bucket
}
items {
name = "AWS_DISTRIBUTION_ID"
default_value = var.aws_distribution_id
}
items {
name = "AWS_CREDS_SECRET_ID"
default_value = oci_vault_secret.aws_deployer.id
}
}
freeform_tags = local.common_tags
}
# マネージド・ビルド:OCI が用意するランナー(ビルド中の OCPU / メモリで課金)がリポジトリ直下の build_spec.yaml を実行する
resource "oci_devops_build_pipeline_stage" "build" {
build_pipeline_id = oci_devops_build_pipeline.site.id
build_pipeline_stage_type = "BUILD"
display_name = "build-and-upload"
# description を明示:書かないと API 上は空文字列になり、ステージ更新時に 400 Invalid description(2026-11-06)
description = "Build the MkDocs site and upload it to OCI Object Storage and AWS S3"
image = "OL8_X86_64_STANDARD_10"
build_spec_file = "build_spec.yaml"
primary_build_source = "src"
stage_execution_timeout_in_seconds = 900 # 固まったら 15 分で強制終了(課金が続くのを防ぐ)
# ランナーのシェイプ:CUSTOM 2 OCPU / 8GB を明示(= DEFAULT で実際に割り当てられた VM.Standard.E5.Flex 2 / 8、費用は同じ)
# 2026-11-06 DEFAULT で GET_BUILD_RUNNER が失敗し続けた(Unable to get Build Runner)→ CUSTOM でランナーを確保できるか試す
# CUSTOM では ocpus / memory_in_gbs が必須(SDK の CustomBuildRunnerShapeConfig)
build_runner_shape_config {
build_runner_type = "CUSTOM"
ocpus = 2
memory_in_gbs = 8
}
build_pipeline_stage_predecessor_collection {
items {
id = oci_devops_build_pipeline.site.id # 最初のステージの前段 = パイプライン自体
}
}
build_source_collection {
items {
name = "src"
connection_type = "DEVOPS_CODE_REPOSITORY"
repository_id = oci_devops_repository.site.id
repository_url = oci_devops_repository.site.http_url
branch = "main"
}
}
freeform_tags = local.common_tags
}
# main に push されたらビルドを開始(ほかのブランチへの push では起動しない)
resource "oci_devops_trigger" "push_main" {
project_id = oci_devops_project.site.id
display_name = "push-main"
trigger_source = "DEVOPS_CODE_REPOSITORY"
repository_id = oci_devops_repository.site.id
actions {
type = "TRIGGER_BUILD_PIPELINE"
build_pipeline_id = oci_devops_build_pipeline.site.id
filter {
trigger_source = "DEVOPS_CODE_REPOSITORY"
events = ["PUSH"]
include {
head_ref = "main"
}
}
}
freeform_tags = local.common_tags
}
# ---------- 権限:リソース・プリンシパル ----------
# tf-lab 内のビルド・パイプラインはすべてこの動的グループのメンバーになる
resource "oci_identity_dynamic_group" "site_build" {
compartment_id = data.oci_identity_compartment.lab.compartment_id
name = "${var.project_name}-site-build"
description = "DevOps build pipelines in ${data.oci_identity_compartment.lab.name}"
matching_rule = "ALL {resource.type = 'devopsbuildpipeline', resource.compartment.id = '${var.compartment_ocid}'}"
freeform_tags = local.common_tags
}
# tf-lab にアタッチ:tf-lab(とその配下)の権限しか付与できない
# manage objects + where でバケット名を指定 → tflab-site 内のオブジェクトしか操作できない(tflab-tfstate には触れず、バケット自体も削除できない)
# read devops-repository → ビルド時に build_spec.yaml を読み、コードを clone する
# ハマりどころ:repos はコンテナ・レジストリ(OCIR)のリソース・タイプで、DevOps のコード・リポジトリではない
# read / use / manage repos はどれも RelatedResourceNotAuthorizedOrNotFound(Unable to fetch build_spec file)
# use ons-topics → プロジェクトのトピックにビルドのイベントを送る
# read secret-bundles + where secret.id → AWS Access Key のシークレット 1 つの中身しか読めない(Phase 10-4)
resource "oci_identity_policy" "site_build" {
compartment_id = var.compartment_ocid
name = "${var.project_name}-site-build"
description = "Permissions for ${oci_identity_dynamic_group.site_build.name}"
statements = [
"Allow dynamic-group ${oci_identity_dynamic_group.site_build.name} to manage objects in compartment ${data.oci_identity_compartment.lab.name} where target.bucket.name = '${oci_objectstorage_bucket.site.name}'",
"Allow dynamic-group ${oci_identity_dynamic_group.site_build.name} to read devops-repository in compartment ${data.oci_identity_compartment.lab.name}",
"Allow dynamic-group ${oci_identity_dynamic_group.site_build.name} to use ons-topics in compartment ${data.oci_identity_compartment.lab.name}",
"Allow dynamic-group ${oci_identity_dynamic_group.site_build.name} to read secret-bundles in compartment ${data.oci_identity_compartment.lab.name} where target.secret.id = '${oci_vault_secret.aws_deployer.id}'",
]
freeform_tags = local.common_tags
}
07-site/main.tf(サイト用バケット)
# ------------------------------------------------------------
# Phase 7-1:オブジェクト・ストレージ(Object Storage)で学習ノートのサイト(MkDocs の site/)を公開
# Terraform が管理するのはバケットだけ(インフラ)
# サイトのファイル(コンテンツ)はビルドのたびに変わる → tools/upload_site.py でアップロードし、state には入れない(7-2 で OCI DevOps に任せる)
# ------------------------------------------------------------
locals {
common_tags = {
project = "tf-lab"
managed_by = "terraform"
}
}
# オブジェクト・ストレージのネームスペース(URL で使う)
data "oci_objectstorage_namespace" "ns" {
compartment_id = var.compartment_ocid
}
resource "oci_objectstorage_bucket" "site" {
#checkov:skip=CKV_OCI_10:Public static website; objects are readable but listing is disabled
#checkov:skip=CKV_OCI_8:Content is rebuilt from Git on every deploy; old versions are not needed
#checkov:skip=CKV_OCI_9:Public content; Oracle-managed key is sufficient (CMK needs Vault)
#checkov:skip=CKV_OCI_7:No event consumers; enable when an event rule is added
compartment_id = var.compartment_ocid
namespace = data.oci_objectstorage_namespace.ns.namespace
name = "${var.project_name}-site"
# ObjectRead :ファイルを読めるうえ、バケット内の全ファイル名も一覧できる
# ObjectReadWithoutList :ファイルは読めるが、一覧はできない ← サイトには読み取りだけで十分
# NoPublicAccess :公開を停止(var.site_public = false。08-guard の Function もこれに変更する)
access_type = var.site_public ? "ObjectReadWithoutList" : "NoPublicAccess"
storage_tier = "Standard"
versioning = "Disabled" # コンテンツはいつでも Git から再ビルドできるので、古いバージョンは不要
freeform_tags = local.common_tags
}
07-site/aws.tf(AWS キーを入れるボールト / シークレット)
# ------------------------------------------------------------
# Phase 10-4:build 後に AWS にもアップロード(10-aws-site/site の S3 + CloudFront)
# AWS の Access Key(IAM ユーザー tflab-site-deployer)はここの Vault Secret に入れる
# build runner は Resource Principal でこの Secret を読む → コード、build_spec、build ログのどこにも Key はない
#
# Vault(DEFAULT、無料)
# └─ マスター暗号化キー(SOFTWARE、無料)
# └─ Secret:{"access_key_id": ..., "secret_access_key": ...}(JSON)
#
# Secret の中身は Terraform を通さない:
# Terraform は Secret(中身はプレースホルダー)を作成するだけ + ignore_changes
# 本当の値は tools/rotate_aws_deploy_key.py が新しいバージョンとして書き込む(そうしないと次の apply で本当の値がプレースホルダーに戻される)
#
# ⚠ 削除:destroy 後は「削除保留中」になり(7〜30 日)、その間は同名の Secret を再作成できない
# ------------------------------------------------------------
resource "oci_kms_vault" "site" {
compartment_id = var.compartment_ocid
display_name = "${var.project_name}-site-vault"
vault_type = "DEFAULT"
freeform_tags = local.common_tags
}
resource "oci_kms_key" "site" {
compartment_id = var.compartment_ocid
display_name = "${var.project_name}-site-key"
management_endpoint = oci_kms_vault.site.management_endpoint
protection_mode = "SOFTWARE"
key_shape {
algorithm = "AES"
length = 32
}
freeform_tags = local.common_tags
}
resource "oci_vault_secret" "aws_deployer" {
compartment_id = var.compartment_ocid
vault_id = oci_kms_vault.site.id
key_id = oci_kms_key.site.id
secret_name = "${var.project_name}-aws-site-deployer"
description = "AWS access key of IAM user tflab-site-deployer (written by tools/rotate_aws_deploy_key.py)"
secret_content {
content_type = "BASE64"
content = base64encode(jsonencode({ placeholder = true }))
}
lifecycle {
ignore_changes = [secret_content]
}
freeform_tags = local.common_tags
}