Phase 7:サイトの公開 + CI/CD(OCI DevOps)¶
目標¶
- この学習ノートのサイト(MkDocs)をオブジェクト・ストレージ(Object Storage)で公開する
- push したら自動でチェック → ビルド → 公開する(CI/CD)
- API キーを発行しない:ビルドはリソース・プリンシパル(Resource Principal)(サービス自身の ID)を使う
方針¶
| 決定 | 理由 |
|---|---|
| GitHub Pages を使わない | 無料プランではリポジトリを public にする必要がある → リポジトリ全体と Git の履歴は公開したくない。サイトに埋め込んだコードが見えるのは許容 |
| オブジェクト・ストレージで公開 | Always Free の範囲内で、Terraform の練習も続けられる |
| CI/CD は OCI DevOps | 当初はプライベートな GitHub リポジトリ + GitHub Actions の予定だったが、利用中のネットワークでブラウザから GitHub がブロックされていた。コマンドラインからはつながるが、回避はしない(ブロックされているのは、ネットワークの管理者が許可していない可能性があるため) |
| Terraform が管理するのはバケットだけ | サイトのファイルはビルドのたびに変わるため、Terraform に入れると plan に毎回差分が出る → コンテンツはスクリプトでアップロードし、state には入れない |
構成¶
flowchart LR
pc["ローカル PC<br/>git push"] -->|HTTPS + 認証トークン| repo["コード・リポジトリ<br/>(プライベート)"]
repo -->|"トリガー<br/>PUSH main"| bp["ビルド・パイプライン<br/>(マネージド・ビルド)"]
bp -->|"リソース・プリンシパル"| bucket["バケット tflab-site<br/>(公開)"]
bucket --> user["ブラウザ"]
07-site/
├── main.tf バケット(ObjectReadWithoutList)
├── devops.tf DevOps 一式 + 動的グループ / ポリシー
└── outputs.tf サイトの URL、リポジトリの URL
build_spec.yaml ビルドの手順(リポジトリ直下)
tools/upload_site.py site/ → バケットへの同期(Python SDK)
ビルドの手順(build_spec.yaml):
| ステップ | 内容 | 失敗したとき |
|---|---|---|
| Setup Python | venv + pip install -r requirements.txt oci |
|
| Terraform checks | fmt -check -recursive → 各ルート・モジュールで validate → tflint |
公開しない |
| Check ja code copies | check_ja_code.py(日本語版のコードのコピーが元ファイルと一致しているか) |
公開しない |
| Build site | mkdocs build --strict(警告があれば失敗) |
公開しない |
| Upload | upload_site.py --auth resource_principal |
オブジェクト・ストレージを静的サイトとして使う¶
| 制約 | 対策 |
|---|---|
ディレクトリにアクセスしても index.html を返さない |
mkdocs.yml で use_directory_urls: false → phases/06-oke/ が phases/06-oke.html になる |
| Content-Type はアップロード時に指定する(指定しないと HTML がダウンロード扱いになる) | アップロード・スクリプトで拡張子ごとに固定(Windows の mimetypes はレジストリを読むため、マシンによって結果が変わる) |
404 はオブジェクト・ストレージの JSON で、MkDocs の 404.html は表示されない |
対策なし |
| URL に必ずネームスペース(namespace)が含まれる | サイトを公開する以上は隠せない。state のバケットは NoPublicAccess なので、ネームスペースがわかっても読めない |
access_type = "ObjectReadWithoutList":匿名でファイルは読めるが、一覧はできない(ObjectReadだとファイル名の一覧まで公開される)- アップロード・スクリプト
tools/upload_site.py:- MD5 で変更されたファイルだけを送る(オブジェクト・ストレージが返す md5 は base64)→ API リクエストを節約(Always Free は月 5 万回)
site/にないオブジェクトは削除する(ページ名を変えても古いページが残らない)- Cache-Control:HTML は
no-cache(公開後すぐに反映)、それ以外は 1 時間キャッシュ
- curl で匿名アクセスして確認:ページは
200 text/html; charset=utf-8、一覧は404
権限:リソース・プリンシパル¶
動的グループ:ALL {resource.type = 'devopsbuildpipeline', resource.compartment.id = '<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
- 「tf-lab 内のビルド・パイプライン」自体に権限がある → 保管すべき鍵がない(GitHub Actions なら API キーを Secrets に入れる必要がある)
- できるのは
tflab-site内のオブジェクトの読み書き・削除、コード・リポジトリの読み取り、ビルド通知の送信だけです。state のバケット、バケット自体、ほかのリソースには触れません - スクリプトでは
oci.auth.signers.get_resource_principals_signer()を使います(ローカル PC では~/.oci/config。--authで切り替え)
git push の認証¶
利用中のネットワークでポート 22(SSH)がブロックされている → HTTPS しか使えません。
| 項目 | 内容 |
|---|---|
| ユーザー名 | <テナンシ名>/<ユーザー名>(Default ドメインならドメイン名は不要) |
| パスワード | 認証トークン(Auth Token)(コンソールのパスワードではない) |
| 保存 | 初回の push のあと、Git Credential Manager が Windows の資格情報マネージャーに保存する |
認証トークンは Terraform で作らない
oci_identity_auth_token でも作成できますが、トークンの値がそのまま state に入ります。API キーと同じく、パスワード類は Terraform を通さず、コンソールで手動作成します(マイ・プロファイル → トークンおよびキー)。
1 ユーザーにつき最大 2 つで、値は作成時に 1 回だけ表示されます。
手順と観察¶
| # | 起動 | 結果 | 内容 |
|---|---|---|---|
| 1 | push | FAILED(20 秒) | build_spec.yaml を読めない(ポリシーは read repos) |
| 2 | 手動 | FAILED | use repos → 同じ |
| 3 | 手動 | FAILED(ランナー 2 分) | 一時的に manage devops-family → build_spec は読めた。pip が PyPI につながらない |
| 4 | push | FAILED | 診断出力を追加 → 原因はランナー・イメージの pip の設定 |
| 5 | push | SUCCEEDED(約 2.5 分) | PIP_CONFIG_FILE=/dev/null → アップロード成功 |
| 6 | 手動 | FAILED | manage repos → 同じ → リソース・タイプが間違っていた |
| 7 | 手動 | SUCCEEDED | read devops-repository |
| 8 | push | SUCCEEDED | 最終的なポリシーで push → 自動ビルド |
| 9 | push | SUCCEEDED(約 4 分) | Terraform のチェックを追加(fmt / 7 つのルート・モジュールの validate / 12 ディレクトリの tflint) |
- ランナーが起動する前に失敗したもの(1、2、6)は課金されません
- 2 回目以降の MD5 同期:コンテンツが変わっていなければ
uploaded 0 / unchanged 72 - ビルドのログは ログ(Log)(service = devops)がないとコンソールやログ検索(Logging Search)で見られません。ビルド終了から約 1 分後に検索できるようになります
- ランナーには Python 3.11、OCI CLI 3.58 があり、terraform / tflint はない(CI で公式サイトから固定バージョンをダウンロード)
ハマりどころ¶
repos は DevOps のコード・リポジトリではない
read repos / use repos / manage repos はどれも Unable to fetch build_spec file ... RelatedResourceNotAuthorizedOrNotFound になりました。
repos はコンテナ・レジストリ(OCIR)のリソース・タイプです。DevOps のコード・リポジトリは devops-repository です。
- 調べ方:一時的に
manage devops-family in compartment tf-labを付与 → 成功 → 動的グループ / ポリシー自体は正しい → 問題はリソース・タイプ - エラー・メッセージの例は
manage devops-family in tenancyですが、範囲が広すぎるのでそのままは使いませんでした
ランナーで pip が PyPI につながらない:ProxyError ... Name or service not known
プロキシの環境変数はないのに、ランナー・イメージの pip のグローバル設定に Oracle 社内のプロキシが書かれており(www-proxy-hqdc.us.oracle.com)、ランナー内では名前解決できません。
→ PIP_CONFIG_FILE=/dev/null(pip がどの設定ファイルも読み込まなくなる)。
- 推測で直さず、まず診断出力(
pip config list、getent hosts pypi.orgなど)を追加してから修正しました
最初の push が拒否される(non-fast-forward)
OCI がリポジトリ作成時に空の「Initial Commit」(ファイルなし)を自動で作るため、ローカルの履歴と共通の祖先がありません。
→ git push -u --force oci main(その空のコミットだけを上書き)。
default_branch = 'main' だと plan のたびに差分が出る
API は refs/heads/main として保存します。コードにも refs/heads/main と書きます。
Git Credential Manager が固まる:auto-detection of host provider took too long
GCM が「GitHub なのか Azure なのか」を判定する処理がタイムアウトしています。
→ git config credential.https://devops.scmservice.ap-osaka-1.oci.oraclecloud.com.provider generic
ローカル PC にはやはり OCI CLI をインストールできない(公式インストール・スクリプトでもだめ)
Phase 5b と同じく Windows のパス長の上限が原因です。アップロードは Python SDK のスクリプトに切り替えました。 教訓:始める前に自分のノートを見返すこと。
コード全体¶
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/devops.tf
# ------------------------------------------------------------
# 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"
image = "OL8_X86_64_STANDARD_10"
build_spec_file = "build_spec.yaml"
primary_build_source = "src"
stage_execution_timeout_in_seconds = 900 # 固まったら 15 分で強制終了(課金が続くのを防ぐ)
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
}
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}"
# check_ja_code.py には 3.9+ が必要。OL8 標準の python3 は 3.6 の可能性がある → 見つからなければインストール
PY=""
for p in python3.12 python3.11 python3.9 python3; do
if command -v "$p" >/dev/null && "$p" -c 'import sys; sys.exit(sys.version_info < (3, 9))'; 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 ja code copies
command: |
set -euo pipefail
cd "${OCI_PRIMARY_SOURCE_DIR}"
/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
- 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}"