はじめに

記事「OCI初心者向け:Fn Project CLIでOCI Functionsにデプロイしてみた」では、ローカル PC の Fn Project CLI を使って OCI Functions に Python 関数をデプロイしました。

その記事の冒頭で、OCI Functions のデプロイ方法は大きく 3 つあると紹介しました。

  • Fn Project CLI を使う(前回記事で紹介)
    • AWS Lambda デプロイ時に使う AWS SAM CLI と近い機能を備えているのが Fn Project CLI です。ローカルでの動作確認や、ローカルからのデプロイを行えます。
  • OCI DevOps を使う
    • AWS Lambda を AWS CodePipeline、AWS CodeBuild でデプロイする流れを OCI Functions で行いたい場合は、OCI DevOps で行えます。ソースコード・リポジトリ(GitHub や OCI Code Repositories など)と連携し、CI/CD パイプラインを実現したい場合に使用できます。
  • Terraform を使う
    • OCI Functions はコンテナベースで動作します。事前にコードをDockerビルドし、OCI Container Registry (OCIR) にプッシュした後に、Terraform で、OCI Functions の定義を作成できます。

本記事では、このうち 「OCI DevOps を使う」方法を扱います。OCI 側のリソースを可能な限り Terraform で構築し、GitHub との接続などの人手作業を最小限にすることをゴールにします。前回はコンソール上手作業でネットワーク(VCN)まわりを構築しましたが、今回は Terraform で作り直します。

今回のゴール

GitHub リポジトリに置いた Python 関数コードを git push すると、
OCI DevOps の ビルド・パイプラインが自動でコンテナ・イメージをビルドし、
OCIR(OCI Container Registry)へ push、
続けて デプロイ・パイプラインが OCI Functions を新しいイメージへ更新する。

この一連の CI/CD を、OCI 側は Terraform で構築します。

AWS で例えると

役割 AWS OCI(本記事)
ソース GitHub / CodeCommit GitHub
ビルド CodeBuild OCI DevOps(ビルド)
コンテナ・レジストリ ECR OCIR
パイプライン制御 CodePipeline OCI DevOps(ビルド+デプロイ・パイプライン)
実行環境 Lambda OCI Functions
IaC CloudFormation / Terraform Terraform

アーキテクチャ全体像

Terraform で作成する OCI リソースは次のとおりです。プレフィクスは handson-functions-devops- としています。

分類 リソース 名前(プレフィクス handson-functions-devops-
ネットワーク VCN handson-functions-devops-vcn
サブネット(パブリック) handson-functions-devops-subnet1
インターネット・ゲートウェイ handson-functions-devops-igw
ルート表 handson-functions-devops-rt
レジストリ OCIR リポジトリ handson-functions-devops-repository/hello
Functions アプリケーション handson-functions-devops-app
関数 handson-functions-devops-hello
Vault Vault / キー / シークレット handson-functions-devops-vault ほか
IAM 動的グループ / ポリシー handson-functions-devops-dg / -policy
DevOps プロジェクト handson-functions-devops-project
GitHub 接続 handson-functions-devops-github-connection
ビルド・パイプライン handson-functions-devops-build-pipeline
デプロイ・パイプライン handson-functions-devops-deploy-pipeline
トリガー handson-functions-devops-github-trigger
通知 Notification トピック handson-functions-devops-topic
ロギング ロググループ / ログ handson-functions-devops-log-group / -devops-log

やること / 前提

前提

前回記事の前提に加えて、いくつか増えます。増えた項目が太字です。

  • OCI アカウント作成済み(前回同様、個人アカウントを想定し Administrator 権限で実施。本番ではドメイン/グループ/ポリシーで権限を絞ることを推奨)
  • ローカル PC に Docker インストール済み
  • OCI CLI をセットアップ済み
  • Terraform インストール済み
  • GitHub アカウントとリポジトリ
  • GitHub の Personal Access Token(PAT)(OCI DevOps が GitHub に接続するために使用)

💡 前回記事では「OCI CLI をインストールして、ローカル PC に OCI の config(AWS で言うプロファイル)を作成」しました。本記事の Terraform も、その config(API キー)を使って OCI へ接続します。

作業の流れ

  1. GitHub リポジトリの準備(Python 関数コード・Dockerfilebuild_spec.yaml を push)
  2. GitHub PAT の発行
  3. Terraform 変数の設定terraform.tfvars
  4. terraform apply で OCI リソースを一括作成
  5. GitHub Webhook の登録(GitHub 側手作業。詳細は後述)
  6. git push して自動ビルド〜デプロイを確認

OCI 側の作成は基本的に手順 4 の terraform apply に集約されます。前回コンソールで作成した VCN・サブネット・IGW・OCIR・Functions アプリなども、すべて Terraform 定義に含めます。

1. GitHub リポジトリの準備

リポジトリのルートには次のファイルを置きます。

.
├── func.py            # Python 関数本体(前回の hello-python 相当)
├── func.yaml          # Fn の関数定義(ローカル実行・確認用)
├── requirements.txt   # Python 依存
├── Dockerfile         # OCI DevOps のマネージド・ビルドが docker build で使用
├── build_spec.yaml    # OCI DevOps ビルド・パイプラインのビルド仕様
└── terraform/         # OCI リソースの Terraform 定義

func.py は前回の hello-python と同じく、{"message": "Hello World"} を返すサンプルです。

import io
import json
import logging

from fdk import response


def handler(ctx, data: io.BytesIO = None):
    name = "World"
    try:
        body = json.loads(data.getvalue())
        name = body.get("name", "World")
    except (Exception, ValueError) as ex:
        logging.getLogger().info("error parsing json payload: " + str(ex))

    logging.getLogger().info("Inside Python Hello World function")
    return response.Response(
        ctx,
        response_data=json.dumps({"message": "Hello {0}".format(name)}),
        headers={"Content-Type": "application/json"},
    )

Dockerfile を用意する理由

前回は fn deploy が内部で Docker イメージをビルドしてくれていました。OCI DevOps のマネージド・ビルドでは、ビルド・ランナー上でコンテナ・イメージのビルドを実行するため、Dockerfile を自前で用意します。中身は fn init --runtime python が生成するものと同等のマルチステージ構成です。

FROM fnproject/python:3.11-dev AS build-stage
WORKDIR /function
ADD requirements.txt /function/
RUN pip3 install --target /python/ --no-cache --no-cache-dir -r requirements.txt \
    && rm -fr ~/.cache/pip /tmp* requirements.txt func.yaml Dockerfile .venv
ADD . /function/
RUN rm -fr /function/.pip_cache

FROM fnproject/python:3.11
WORKDIR /function
COPY --from=build-stage /python /python
COPY --from=build-stage /function /function
RUN chmod -R o+r /function
ENV PYTHONPATH=/function:/python
ENTRYPOINT ["/python/bin/fdk", "/function/func.py", "handler"]

build_spec.yaml

OCI DevOps のビルド・パイプライン(マネージド・ビルド・ステージ)が読み込むビルド仕様です。リポジトリのルートに置くと、ビルド・ステージのデフォルトパス(build_spec.yaml)として自動的に参照されます。

version: 0.1
component: build
timeoutInSeconds: 6000
shell: bash
env:
  exportedVariables:
    # ビルド実行ごとに一意なイメージタグ。デプロイ・パイプラインへ引き継ぐ。
    - BUILDRUN_HASH
steps:
  - type: Command
    name: "イメージタグ (BUILDRUN_HASH) を採番する"
    command: |
      export BUILDRUN_HASH=$(echo ${OCI_BUILD_RUN_ID} | rev | cut -c 1-7)
      echo "BUILDRUN_HASH: ${BUILDRUN_HASH}"
  - type: Command
    name: "関数コンテナ・イメージをビルドする"
    command: |
      cd ${OCI_PRIMARY_SOURCE_DIR}
      # OL8 ビルド・ランナーのコンテナ・エンジンは podman(OL7 は docker)。
      if command -v docker >/dev/null 2>&1; then CE=docker; else CE=podman; fi
      echo "container engine: $CE"
      $CE build -t fn-image:${BUILDRUN_HASH} -f Dockerfile .
outputArtifacts:
  - name: fn_image
    type: DOCKER_IMAGE
    location: fn-image:${BUILDRUN_HASH}

ポイントは 2 つです。

  • BUILDRUN_HASH:OCI DevOps の定義済み変数 OCI_BUILD_RUN_ID から一意なタグを生成し、exportedVariables でデプロイ・パイプライン側へ引き継ぎます。「毎回異なるタグを付けて push し、そのタグで関数を更新する」ための仕掛けです。
  • outputArtifacts[].namefn_image:後続の Deliver Artifact ステージが、この名前でビルド成果物(Docker イメージ)を OCIR へ紐付けます。

⚠️ コンテナ・エンジンは docker とは限りません。 OCI DevOps のビルド・ランナーは OL7 がサポート終了し、現在は Oracle Linux 8(OL8) を指定します。OL8 ランナーではコンテナ・エンジンが Podman に置き換わっているため、docker build を直書きすると失敗する可能性があります。上記のようにエンジンを自動判定しておくと、どちらのランナーでも動きます。

2. GitHub PAT の発行

OCI DevOps が GitHub リポジトリを読み取るために、GitHub の Personal Access Token(PAT) を使います。

  • GitHub → Settings → Developer settings → Personal access tokens で発行します。
  • スコープは、プライベートリポジトリなら repoと、admin:repo_hook を付与します。
  • 発行した PAT は、後述の Terraform 変数として渡し、OCI Vault のシークレットに安全に格納します。

3.〜4. Terraform で OCI リソースを一括作成

ここが本記事の主役です。terraform/ ディレクトリに OCI リソースをすべて定義し、terraform apply で作成します。

ファイル構成

リソースの種類ごとにファイルを分けています。

terraform/
├── versions.tf / provider.tf     # プロバイダのバージョンと認証設定
├── variables.tf / locals.tf      # 入力変数と、そこから導出する値
├── network.tf                    # VCN / サブネット / IGW / ルート表
├── registry.tf                   # OCIR リポジトリ
├── vault.tf                      # Vault / キー / GitHub PAT シークレット
├── iam.tf                        # 動的グループ / ポリシー
├── functions.tf                  # Functions アプリ / 関数 / seed イメージ push
├── devops.tf                     # DevOps プロジェクト / 接続 / ビルド / デプロイ / トリガー
├── logging.tf                    # DevOps ログ(これが無いとビルドが走りません)
├── outputs.tf                    # 関数 OCID や Webhook URL などの出力
└── terraform.tfvars.example      # ← これをコピーして terraform.tfvars を作る

変数の設定(terraform.tfvars

環境ごとに変わる値は、すべて変数として外に出しています。terraform.tfvars.example をコピーして terraform.tfvars を作り、自分の値を書き込みます。

cd terraform
cp terraform.tfvars.example terraform.tfvars

terraform.tfvars には GitHub PAT と OCI 認証トークンという 2 つの秘密情報が入ります。

# ---- OCI 認証・共通 ----
tenancy_ocid       = "ocid1.tenancy.oc1..xxxxxxxx"
compartment_ocid   = "ocid1.compartment.oc1..xxxxxxxx"
region             = "ap-tokyo-1"
oci_config_profile = "DEFAULT"
# prefix           = "handson-functions-devops-"   # 変更する場合のみ

# ---- ロギング ----
# log_retention_duration = 30                       # DevOps ログの保持日数(変更する場合のみ)

# ---- OCIR(seed イメージ push 用)----
ocir_region_key      = "nrt"                        # 東京=nrt / 大阪=kix
ocir_username        = "taro.oracle@example.com"    # フェデレーションユーザーは oracleidentitycloudservice/<user>
ocir_auth_token      = "xxxxxxxxxxxxxxxxxxxx"       # OCI 認証トークン(GitHub PAT とは別物)
auto_push_seed_image = true

# ---- GitHub ----
github_pat            = "ghp_xxxxxxxxxxxxxxxxxxxx"
github_repository_url = "https://github.com/<owner>/oci-functions-devops-handson.git"
github_branch         = "develop"

各変数の意味と取得方法は次のとおりです。

変数 必須 説明・取得方法
tenancy_ocid テナンシの OCID。コンソール右上のプロフィール → テナンシ、または oci iam compartment list の親。動的グループはテナンシ直下に作るため必要です。
compartment_ocid リソースを作るコンパートメントの OCID。個人アカウントでルート直下に作るならテナンシ OCID と同じ値で構いません。
region リージョン識別子。デフォルトは ap-tokyo-1
oci_config_profile ~/.oci/config のプロファイル名。前回記事で作った config をそのまま使います。デフォルトは DEFAULT
prefix 作成するリソース名のプレフィクス。デフォルトは handson-functions-devops-。前回の handson-functions- と衝突させないための値です。
log_retention_duration DevOps ログの保持日数。デフォルト 30 日。
ocir_region_key OCIR のリージョンキー。東京は nrt、大阪は kix
ocir_username seed push 時 OCIR への docker login に使うユーザー名の「ネームスペース/」以降。通常ユーザーはメールアドレス、IDCS フェデレーションユーザーは oracleidentitycloudservice/
ocir_auth_token seed push 時 OCI の認証トークン。コンソールのユーザー詳細 → 認証トークンで発行します。GitHub PAT とは別物なので注意。
auto_push_seed_image true なら terraform apply の中で seed イメージを build & push します。ローカルに docker が無い場合は false にして手動 push に切り替えます。
github_pat 2. で発行した GitHub PAT。Vault シークレットに格納されます。プライベートリポジトリなら repo スコープが必要です。
github_repository_url 接続する GitHub リポジトリの URL(.git 付き)。
github_branch ビルドのトリガーにするブランチ。デフォルトは develop。ビルド・ソースとトリガー・フィルタの両方で使われるので、実際に push するブランチと一致させてください

ネットワーク(VCN)を Terraform で作成する

前回記事では、コンソールで次のように手作業で作成していました。

  • VCN を作成します。最低限、名前:handson-functions-vcn、IPv4 CIDR ブロック:10.1.0.0/16、DNS 解決:OFF を設定
  • サブネットを作成します。名前:handson-functions-subnet1、IPv4 CIDR ブロック:10.1.0.0/24、パブリックサブネットを選択
  • インターネットゲートウェイの作成と、ルート・ルールを設定します。名前:handson-functions-igw
  • ターゲットタイプ:インターネット・ゲートウェイ、宛先 CIDR ブロック:0.0.0.0/0

今回はこれを VCN から丸ごと Terraform 化します。CIDR などの値は前回に合わせつつ、名前のプレフィクスだけ handson-functions-devops- にします。

resource "oci_core_vcn" "vcn" {
  compartment_id = var.compartment_ocid
  cidr_blocks    = ["10.1.0.0/16"]
  display_name   = "${var.prefix}vcn"
}

resource "oci_core_internet_gateway" "igw" {
  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.vcn.id
  display_name   = "${var.prefix}igw"
  enabled        = true
}

resource "oci_core_route_table" "rt" {
  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.vcn.id
  display_name   = "${var.prefix}rt"
  route_rules {
    destination       = "0.0.0.0/0"
    network_entity_id = oci_core_internet_gateway.igw.id
  }
}

resource "oci_core_subnet" "subnet1" {
  compartment_id    = var.compartment_ocid
  vcn_id            = oci_core_vcn.vcn.id
  cidr_block        = "10.1.0.0/24"
  display_name      = "${var.prefix}subnet1"
  route_table_id    = oci_core_route_table.rt.id
  security_list_ids = [oci_core_vcn.vcn.default_security_list_id]
}

⚠️ 前回記事の最後で触れられていた、こんなハマりどころがありました。

※ インターネットゲートウェイの作成とルート・ルールの設定を忘れているとここで、下記エラーとなります。
Error invoking function. status: 502 message: Failed to pull function image

OCI Functions はイメージを OCIR から pull するため、egress 経路(IGW とルート)が必須です。Terraform 化するとこの設定漏れを防げるのが利点です。

OCIR・Functions アプリ・関数

OCIR リポジトリ、Functions アプリ、関数を作成します。関数はデプロイ・パイプラインが更新しますが、関数リソース自体は事前に存在させる必要があるため、初回だけ「seed(種)イメージ」を指定して作成します。

resource "oci_artifacts_container_repository" "fn_repo" {
  compartment_id = var.compartment_ocid
  display_name   = "${var.prefix}repository/hello"
  is_public      = false
}

resource "oci_functions_application" "app" {
  compartment_id = var.compartment_ocid
  display_name   = "${var.prefix}app"
  subnet_ids     = [oci_core_subnet.subnet1.id]
}

resource "oci_functions_function" "hello" {
  application_id = oci_functions_application.app.id
  display_name   = "${var.prefix}hello"
  memory_in_mbs  = 256
  image          = "${local.ocir_image_base}:seed"

  # CI/CD(デプロイ・パイプライン)が image を更新するため、
  # Terraform 側では image の変更を無視してドリフトを防ぐ。
  lifecycle {
    ignore_changes = [image, image_digest]
  }

  depends_on = [null_resource.seed_image]
}

📝 seed イメージについて:OCI Functions は「イメージ URI を持つ関数」として作成されるため、初回作成時点で OCIR に最低 1 つイメージが必要です。しかし作りたての OCIR は空です。そこで terraform apply の中で、null_resource + local-exec を使って seed イメージを 1 回だけ build & push します(詳細は README を参照)。以降の更新はすべて DevOps のデプロイ・パイプラインが ${BUILDRUN_HASH} タグで行います。

Vault に GitHub PAT を格納

2.で発行した PAT を、OCI Vault のシークレットとして格納します。OCI DevOps の GitHub 接続は、このシークレットの OCIDを参照します(PAT を平文で持ちません)。

resource "oci_kms_vault" "vault" {
  compartment_id = var.compartment_ocid
  display_name   = "${var.prefix}vault"
  vault_type     = "DEFAULT"
}

resource "oci_kms_key" "key" {
  compartment_id      = var.compartment_ocid
  display_name        = "${var.prefix}key"
  management_endpoint = oci_kms_vault.vault.management_endpoint
  key_shape {
    algorithm = "AES"
    length    = 32 # バイト単位。AES-256。
  }
}

resource "oci_vault_secret" "github_pat" {
  compartment_id = var.compartment_ocid
  vault_id       = oci_kms_vault.vault.id
  key_id         = oci_kms_key.key.id
  secret_name    = "${var.prefix}github-pat"
  secret_content {
    content_type = "BASE64"
    content      = base64encode(var.github_pat)
  }
}

IAM(動的グループ / ポリシー)

DevOps のビルド/デプロイ・パイプラインが「OCIR への push」「Functions の更新」「PAT シークレットの読み取り」などを行えるよう、動的グループポリシーを作成します。

resource "oci_identity_dynamic_group" "devops_dg" {
  compartment_id = var.tenancy_ocid
  name           = "${var.prefix}dg"
  description    = "OCI DevOps build/deploy pipelines for handson"
  matching_rule  = "ANY {resource.type='devopsbuildpipeline', resource.type='devopsdeploypipeline', resource.type='devopsconnection', resource.type='devopstrigger'}"
}

resource "oci_identity_policy" "devops_policy" {
  compartment_id = var.compartment_ocid
  name           = "${var.prefix}policy"
  description    = "Allow OCI DevOps to build, push to OCIR and deploy Functions"
  statements = [
    "Allow dynamic-group ${oci_identity_dynamic_group.devops_dg.name} to manage repos in compartment id ${var.compartment_ocid}",
    "Allow dynamic-group ${oci_identity_dynamic_group.devops_dg.name} to manage functions-family in compartment id ${var.compartment_ocid}",
    "Allow dynamic-group ${oci_identity_dynamic_group.devops_dg.name} to use virtual-network-family in compartment id ${var.compartment_ocid}",
    "Allow dynamic-group ${oci_identity_dynamic_group.devops_dg.name} to read secret-family in compartment id ${var.compartment_ocid}",
    "Allow dynamic-group ${oci_identity_dynamic_group.devops_dg.name} to use keys in compartment id ${var.compartment_ocid}",
    "Allow dynamic-group ${oci_identity_dynamic_group.devops_dg.name} to manage devops-family in compartment id ${var.compartment_ocid}",
  ]
}

OCI DevOps(プロジェクト / 接続 / ビルド / デプロイ / トリガー)

いよいよ CI/CD 本体です。ここは構成要素が多いので、順番に作ります。

プロジェクトと通知トピック(DevOps プロジェクトは通知トピックが必須)

resource "oci_ons_notification_topic" "topic" {
  compartment_id = var.compartment_ocid
  name           = "${var.prefix}topic"
}

resource "oci_devops_project" "project" {
  compartment_id = var.compartment_ocid
  name           = "${var.prefix}project"
  notification_config {
    topic_id = oci_ons_notification_topic.topic.topic_id
  }
}

ログの有効化(これが無いとビルドが走りません)

見落としやすいのですが、DevOps プロジェクトは OCI Logging のサービス・ログを有効化していないとビルド実行そのものが失敗します。コンソールでは「可観測性と管理 → ロギング → サービス・ログの有効化」に相当する設定で、Terraform では次のように書きます。

resource "oci_logging_log_group" "devops" {
  compartment_id = var.compartment_ocid
  display_name   = "${var.prefix}log-group"
}

resource "oci_logging_log" "devops" {
  log_group_id = oci_logging_log_group.devops.id
  display_name = "${var.prefix}devops-log"
  log_type     = "SERVICE"
  is_enabled   = true

  configuration {
    compartment_id = var.compartment_ocid
    source {
      service     = "devops"
      source_type = "OCISERVICE"
      category    = "all" # コンソール表示は「DevOps Logs」
      resource    = oci_devops_project.project.id
    }
  }

  retention_duration = 30
}

GitHub 接続(PAT シークレットの OCID を参照)

resource "oci_devops_connection" "github" {
  project_id      = oci_devops_project.project.id
  connection_type = "GITHUB_ACCESS_TOKEN"
  display_name    = "${var.prefix}github-connection"
  access_token    = oci_vault_secret.github_pat.id # シークレットの OCID を渡す
}

デプロイ成果物(Docker イメージ)

OCIR 上のイメージ URI を指すアーティファクトです。${BUILDRUN_HASH} をビルドから受け取って解決できるよう、SUBSTITUTE_PLACEHOLDERS を指定します。

resource "oci_devops_deploy_artifact" "fn_image" {
  project_id                 = oci_devops_project.project.id
  display_name               = "${var.prefix}fn-image"
  deploy_artifact_type       = "DOCKER_IMAGE"
  argument_substitution_mode = "SUBSTITUTE_PLACEHOLDERS"
  deploy_artifact_source {
    deploy_artifact_source_type = "OCIR"
    image_uri                   = "${local.ocir_image_base}:$${BUILDRUN_HASH}"
  }
}

デプロイ・パイプライン(Functions へデプロイ)

resource "oci_devops_deploy_pipeline" "deploy_pipeline" {
  project_id   = oci_devops_project.project.id
  display_name = "${var.prefix}deploy-pipeline"
}

resource "oci_devops_deploy_environment" "fn_env" {
  project_id              = oci_devops_project.project.id
  display_name            = "${var.prefix}fn-env"
  deploy_environment_type = "FUNCTION"
  function_id             = oci_functions_function.hello.id
}

resource "oci_devops_deploy_stage" "deploy_fn" {
  deploy_pipeline_id = oci_devops_deploy_pipeline.deploy_pipeline.id
  display_name       = "${var.prefix}deploy-fn-stage"
  deploy_stage_type  = "DEPLOY_FUNCTION"

  function_deploy_environment_id  = oci_devops_deploy_environment.fn_env.id
  docker_image_deploy_artifact_id = oci_devops_deploy_artifact.fn_image.id

  deploy_stage_predecessor_collection {
    # デプロイ・パイプラインの最初のステージなので、先行はパイプライン自身の OCID。
    items {
      id = oci_devops_deploy_pipeline.deploy_pipeline.id
    }
  }
}

ビルド・パイプライン(3 ステージ:Build → Deliver → Trigger Deployment)

resource "oci_devops_build_pipeline" "build_pipeline" {
  project_id   = oci_devops_project.project.id
  display_name = "${var.prefix}build-pipeline"
}

# ① マネージド・ビルド(コンテナ・イメージのビルド)
resource "oci_devops_build_pipeline_stage" "build" {
  build_pipeline_id         = oci_devops_build_pipeline.build_pipeline.id
  display_name              = "${var.prefix}build-stage"
  build_pipeline_stage_type = "BUILD"
  build_spec_file           = "build_spec.yaml"
  image                     = "OL8_X86_64_STANDARD_10"
  primary_build_source      = "app-source"

  build_source_collection {
    items {
      name            = "app-source"
      connection_type = "GITHUB"
      connection_id   = oci_devops_connection.github.id
      repository_url  = var.github_repository_url
      branch          = var.github_branch
    }
  }

  build_pipeline_stage_predecessor_collection {
    # 最初のステージなので、先行はビルド・パイプライン自身の OCID。
    items {
      id = oci_devops_build_pipeline.build_pipeline.id
    }
  }
}

# ② デリバリ(ビルド成果物を OCIR へ push)
resource "oci_devops_build_pipeline_stage" "deliver" {
  build_pipeline_id         = oci_devops_build_pipeline.build_pipeline.id
  display_name              = "${var.prefix}deliver-stage"
  build_pipeline_stage_type = "DELIVER_ARTIFACT"

  deliver_artifact_collection {
    items {
      artifact_name = "fn_image" # build_spec.yaml の outputArtifacts[].name と一致
      artifact_id   = oci_devops_deploy_artifact.fn_image.id
    }
  }

  build_pipeline_stage_predecessor_collection {
    items {
      id = oci_devops_build_pipeline_stage.build.id
    }
  }
}

# ③ デプロイ・パイプライン起動
resource "oci_devops_build_pipeline_stage" "trigger_deploy" {
  build_pipeline_id         = oci_devops_build_pipeline.build_pipeline.id
  display_name              = "${var.prefix}trigger-deploy-stage"
  build_pipeline_stage_type = "TRIGGER_DEPLOYMENT_PIPELINE"

  deploy_pipeline_id             = oci_devops_deploy_pipeline.deploy_pipeline.id
  is_pass_all_parameters_enabled = true # BUILDRUN_HASH をデプロイへ引き継ぐ

  build_pipeline_stage_predecessor_collection {
    items {
      id = oci_devops_build_pipeline_stage.deliver.id
    }
  }
}

トリガー(GitHub の push でビルドを起動)

resource "oci_devops_trigger" "github" {
  project_id     = oci_devops_project.project.id
  display_name   = "${var.prefix}github-trigger"
  trigger_source = "GITHUB"
  connection_id  = oci_devops_connection.github.id

  actions {
    type             = "TRIGGER_BUILD_PIPELINE"
    build_pipeline_id = oci_devops_build_pipeline.build_pipeline.id
    filter {
      trigger_source = "GITHUB"
      events         = ["PUSH"]
      include {
        head_ref = var.github_branch
      }
    }
  }
}

apply する

定義がそろったら、Terraform を実行します。

cd terraform
terraform init
terraform plan
terraform apply

apply の中では、依存関係にしたがって次の順に作られます。

  1. ネットワーク(VCN / サブネット / IGW / ルート表)
  2. OCIR リポジトリ / Vault / キー / シークレット / IAM
  3. seed イメージの build & push(null_resource.seed_image
  4. Functions アプリ / 関数(seed イメージで作成)
  5. DevOps プロジェクト / ログ / GitHub 接続 / ビルド・デプロイ・パイプライン / トリガー

Vault の作成に 3〜4 分、キーの作成にさらに 2 分ほどかかるので、全体では 10 分弱を見ておくとよいです。

完了したら、後続の作業で使う値を出力から拾えます。

terraform output                              # 一覧
terraform output -raw function_id             # 関数の OCID(動作確認用)
terraform output -raw trigger_webhook_url     # GitHub Webhook の Payload URL

5. GitHub Webhook の登録(GitHub 側手作業)

OCI DevOps のトリガーは「GitHub からの Webhook を受け取る URL」と、署名検証用の Secret を提供します。push を起点に自動実行するには、この 2 つを GitHub リポジトリの Webhook に登録します。OCI DevOps は GitHub 側の Webhook を自動登録しないため、ここだけは GitHub 側の作業になります。

  • Webhook URL は Terraform の出力(terraform output -raw trigger_webhook_url)から取得できます。
  • GitHub リポジトリ → Settings → Webhooks → Add webhook で登録します(Content type は application/json、イベントは push)。

⚠️ Secret は必須で、しかも Terraform では取得できません。
OCI の GitHub トリガーの Secret は CreateTrigger API のレスポンス(GithubTriggerCreateResult.secret)にだけ返る値で、作成後は API でもコンソールでも再取得できません。Terraform プロバイダはこの値を state に保存しないため、oci_devops_trigger だけで作ったトリガーに Webhook を向けると、GitHub の Recent Deliveries で 404 になります。

{ "code" : "NotAuthorizedOrNotFound", "message" : "Invalid payload URL or secret" }

そのため トリガーだけは OCI CLI で作成して Secret を控え、Terraform には import して管理下に戻すのが確実です。

cd terraform

PROJ=$(terraform output -raw devops_project_id)
BP=$(terraform output -raw build_pipeline_id)
CONN=$(terraform output -raw devops_connection_id)
BRANCH=develop   # var.github_branch と同じ値

# Terraform が作ったトリガーを管理から外して削除
terraform state rm oci_devops_trigger.github
oci devops trigger delete --trigger-id "$(terraform output -raw trigger_id)" --force

# CLI で作り直す。レスポンスの "secret" と "trigger-url" を控える
oci devops trigger create-github-trigger \
  --project-id "$PROJ" \
  --connection-id "$CONN" \
  --display-name "handson-functions-devops-github-trigger" \
  --actions '[{"type":"TRIGGER_BUILD_PIPELINE","buildPipelineId":"'"$BP"'","filter":{"triggerSource":"GITHUB","events":["PUSH"],"include":{"headRef":"'"$BRANCH"'"}}}]'

# Terraform 管理に戻す(以降 plan に差分が出ないことを確認)
terraform import oci_devops_trigger.github <新しいトリガーのOCID>
terraform plan

控えた trigger-url を Payload URL に、secret を Secret に設定して Webhook を登録し直せば、GitHub の Recent Deliveries が 2xx になります。

6. 自動デプロイと関数動作確認

  1. リポジトリを git push します。
  2. OCI コンソールの DevOps プロジェクトで、ビルド・パイプラインが自動起動し、Build → Deliver → Trigger Deployment と進むのを確認します。
  3. 続いてデプロイ・パイプラインが起動し、Deploy Function ステージで関数が新しいイメージへ更新されます。
  4. 前回同様、関数を実行して確認します。
fn invoke handson-functions-devops-app handson-functions-devops-hello
{"message": "Hello World"}

または OCI CLI からも実行できます。

oci fn function invoke \
  --function-id <関数のOCID> \
  --file "-" \
  --body ''

func.py を書き換えて git push すると、以降はコード変更 → push → 自動ビルド → 自動デプロイが回るようになります。

ハマりどころ

  • 502 Failed to pull function image:前回同様、IGW とルートの設定漏れが原因になりがちです。今回は Terraform で作成されるので回避できますが、コンパートメントやサブネットを変えた場合は egress 経路を再確認してください。
  • seed イメージが無くて関数が作成できない:OCIR が空だと関数リソースの作成に失敗します。terraform apply 内の seed イメージ push(null_resource)が成功しているか確認してください(docker と OCI 認証トークンが必要)。
  • ポリシー不足でビルド/デプロイが失敗:動的グループのマッチ規則やポリシー文が正しいか、コンパートメントを間違えていないか確認します。
  • PAT のスコープ不足:プライベートリポジトリは repo、Webhook 自動登録は admin:repo_hook が必要です。
  • Webhook 未登録で push しても起動しない:手順5.の Webhook 登録を忘れていないか確認します。
  • 400-InvalidParameter: Oracle Linux 7 build runner images are no longer supported.:ビルド・ステージの imageOL7_X86_64_STANDARD_10 を指定すると出ます。OL8_X86_64_STANDARD_10 に変更してください。あわせて build_spec.yaml のコンテナ・エンジンにも注意(OL8 は Podman)。
  • Logs need to be enabled in order to run the builds. Please enable logs for your project.:DevOps プロジェクトのサービス・ログが未有効です。oci_logging_log_group + oci_logging_logservice = "devops" / category = "all" / resource にプロジェクト OCID)を作成してください。

最後に

前回は Fn Project CLI を使って「手元から」デプロイしました。本記事では OCI DevOps を使い、GitHub への push を起点に、OCI 側は Terraform を使い構築した CI/CD パイプラインでデプロイしました。

前回記事の締めくくりでも触れたとおり、

手元で気軽に試せる一方、実運用で CI/CD パイプラインを組む場合は OCI DevOps、Infrastructure as Code で管理したい場合は Terraform といった選択肢もあります。

本記事はまさに、その 「OCI DevOps」+「Terraform」を組み合わせた形です。ローカルで感覚を掴む段階を過ぎ、チーム開発や継続的デリバリを見据えるなら、この構成が有力な選択肢になります。