APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

【OCI】OCI Functions + API GatewayでシンプルなブログAPIを作ってみた

目次

はじめに

こんにちは、クラウド事業部の高橋です。
本投稿は、OCI Functions + API Gatewayを利用し、シンプルなブログ投稿APIを作成したハンズオン記事です。
私自身、OCI Functionsの実運用経験は多くなく、APIについても学習を進めている段階のため、そのどちらも利用できるハンズオンとして構成してみました。
これからOCI Functionsを利用してみたいと思った方のきっかけとなれば幸いです。

構成図

このハンズオン記事では、以下の構成図をもとにリソースを作成していきます。
通常、ブログシステムには記事の投稿・保存・一覧取得・削除などたくさんの機能が必要ですが、今回のハンズオンは記事の保存(POST)に絞って構築していきます。

  • 記事の保存(POST) : 送られてきたタイトルや本文を、JSON形式ファイルとしてクラウド上のObject Storageに保存します。

本来であれば、ブログを作成するにはWordPressのような大きなシステムと、MySQLのようなデータベースサーバーが必要です。しかし、今回の構成はサーバーレス(OCI Functions)を利用しているため、疎結合であり、リクエストが来た際にだけ起動するため、待機コストがゼロになる(リクエストがない間は課金されない)というメリットがあります。

OCI Functions とは

Oracle Cloud Infrastructure Functions は、完全に管理されている、マルチテナントでスケーラビリティが高いオンデマンドのFunctions-as-a-Serviceプラットフォームである。エンタープライズグレードのOracle Cloud Infrastructure上に構築されており、Fn Projectオープン・ソース・エンジンによって強化されています。ビジネス・ニーズを満たすためのコードの記述に焦点を当てる場合は、OCI Functions (Functionsのみに省略される場合もあります。以前はOracle Functionsと呼ばれていました)を使用します。 docs.oracle.com

OCI API Gateway とは

APIゲートウェイ・サービスでは、ネットワーク内からアクセス可能なプライベート・エンドポイントとともに、インターネット・トラフィックを受け入れる場合にパブリックIPアドレスで公開できるAPIを公開できます。エンドポイントは、API検証、リクエストとレスポンスの変換、CORS、認証と認可およびリクエスト制限をサポートします。 APIゲートウェイ・サービスを使用して、APIクライアントからのトラフィックを処理し、バックエンド・サービスにルーティングするために、リージョナル・サブネットに1つ以上のAPIゲートウェイを作成します。単一のAPIゲートウェイを使用して、複数のバックエンド・サービス(ロード・バランサ、コンピュート・インスタンス、OCI関数など)を単一の統合APIエンドポイントにリンクできます。 docs.oracle.com

料金

OCI Functions

OCI Functionsは以下の価格で提供されます。

呼び出し - 1ヶ月あたり200万件以下の場合 : 無料
呼び出し - 1ヶ月あたり200万件を超過する場合 : $0.0000002
実行時間 - 1ヶ月あたり40万時間以下の場合 : 無料
実行時間 - 1ヶ月あたり40万時間を超過する場合 : $0.00001417
www.oracle.com

API Gateway

API Gatewayは以下の価格で提供されます。従量課金制のため、1ヶ月あたり100万回のAPI呼び出しが$3.00となり、それ以上は加算されます。

API Gateway - 1ヶ月あたり100万回のAPI呼び出し : $3.00 www.oracle.com

実際にやってみた

  • 前提条件
    ・OCIアカウントの作成が完了していること。
    ・テナンシ / コンパートメントの作成が完了していること。
    ・作業するユーザーが権限を保持していること。具体的にはFunctionsとObject Storageを管理できる権限です。
    ・ローカル環境にHomeBrew / Docker / FnCLI / OCI CLI / Python3.xがインストールされていること。

1. Object Storage バケット作成

DBの代替となる保存先バケットを作成します。
左側ハンバーガーボタンより、[ストレージ] -> [オブジェクト・ストレージとアーカイブ・ストレージ] -> [バケット] -> [バケットの作成]で新規バケットを作成しましょう。
名前以外は特に変更せず、デフォルトで問題ありません。

2. VCN / サブネット作成

今回の検証に用いるVCN / サブネットを作成します。
左側ハンバーガーボタンより、[ネットワーキング] -> [仮想クラウド・ネットワーク] -> [アクション] -> [VCNウィザードの起動]でインターネット接続性を持つVCNの作成をクリックします。
VCN名、コンパートメントを選択し、IPv4 CIDRはデフォルトのままで問題ありません(あるいは任意の値を入力してください)。
今回の構成はあくまでも外からのAPI Gateway経由での呼び出し(Invoke)を想定しているため、パブリックサブネット・プライベートサブネットを構成します。
次に、パブリックサブネットに自グローバルIPのHTTPS:443の穴あけをしておきましょう。
[ネットワーキング] -> [仮想クラウド・ネットワーク] -> [<作成したVCN>] -> [サブネット] -> [<自動作成されたパブリックサブネット>] -> [セキュリティ] -> [Default Security List for ]をクリックし、セキュリティ・ルールのイングレス・ルールを追加します。
プロトコルはTCPで、ソース・ポート範囲は空白のまま、宛先ポート範囲に443を入れます。

3. 動的グループ・ポリシー設定(Functions to Object Storage)

FunctionsからObject Storageにファイルを書き込むために、Functionsに権限を付与する必要があります。
左側ハンバーガーボタンより、[アイデンティティとセキュリティ] -> [ドメイン]をクリックします。
現在使用しているドメイン(通常はDefault)を選択します。もし作業コンパートメントにDefaultドメインがない場合は、ルートコンパートメントに存在する可能性があります。
[動的グループタブ] -> [動的グループの作成]をクリックします。
名前と説明は任意で構いません。一致ルールには以下を記載しましょう。
作業コンパートメントのOCIDを調べるには、左側ハンバーガーボタンより、[アイデンティティとセキュリティ] -> [コンパートメント]をクリックし、詳細タブのOCIDをコピーしましょう。

ALL {resource.type = 'fnfunc', resource.compartment.id = '<作業コンパートメントのOCID>'}

再びメニューから、[アイデンティティとセキュリティ] -> [ポリシー] -> [ポリシーの作成]をクリックします。
名前は任意の値で構いません。コンパートメントは作業コンパートメントを設定します。ポリシービルダーで以下をペーストしましょう。

Allow dynamic-group <動的グループ名> to manage objects in compartment <コンパートメント名> where target.bucket.name = '<バケット名>'

4. アプリケーション作成

Functionsを入れるためのアプリケーションと呼ばれるものをコンソールで作成します。
左側ハンバーガーボタンより、[開発者サービス] -> [アプリケーション] -> [アプリケーションの作成]をクリックします。
名前は任意の値で構いません。VCNとサブネットは、先ほど作成したコンパートメントにあるものを選択し、VCNは作成したものを使いましょう。
サブネットはVCNウィザードで作成していればパブリック・プライベートの2つがあります。ここでは商用環境をイメージしてプライベートサブネットを選択しました。

5. ローカル環境でのPython開発準備

前提条件にも記載しましたが、ローカル環境にDocker / Fn CLI / OCI CLIのインストールが必要となります。簡単な手順を記載します。
なお、私の環境はMacOS Tahoe 26.2で実施しています。

Docker
公式サイトからインストールし、起動します。 www.docker.com

Fn CLI
curlコマンド(curl -LSs https://raw.githubusercontent.com/fnproject/cli/master/install | sh)でインストールします。

OCI CLI
brew install oci-cliでインストールします。
次に、OCIコンソールでAPIキーの発行をしましょう。
OCIコンソールの画面右上の人型アイコンをクリックし、自分のメールアドレスをクリックします。
[トークンおよびキー] -> [APIキー] -> [APIキーの追加] -> [APIキー・ペアの生成] -> [秘密キーのダウンロード]で作成できます。
構成ファイルのプレビューで表示される内容をコピーし、メモ帳に貼り付けておきましょう。 次に以下コマンドを順番に実行し、設定を済ませておきます。

# 1. 設定用フォルダの作成
mkdir -p ~/.oci

# 2. キー・ペアの移動とリネーム
mv ~/Downloads/<ダウンロードしたファイル名>.pem ~/.oci/oci_api_key.pem

# 3. キー・ペアの権限変更
chmod 600 ~/.oci/oci_api_key.pem

# 4. Configファイルの作成
vi ~/.oci/config

# 5. 先ほどコピーした構成テキストをペーストします。

# 6. 構成テキストのkey_fileを修正して、Configファイルに上書き保存します。
key_file=/Users/<ユーザー名>/.oci/oci_api_key.pem

認証トークンの作成
Dockerログインをするための認証トークンを作成します。
OCIコンソールの画面右上の人型アイコンをクリックし、表示されたメニューから自身のメールアドレスを選択します。
[トークンおよびキー] -> [認証トークン] -> [トークンの生成] で説明を記載し、トークンを生成します。一度限りの表示のため、メモ帳に貼り付けておきましょう。
このトークンは後ほど、Dockerログインする際に利用します。

Fn Context の設定
ローカルの Fn CLI と OCI 環境を紐付けます。以下のコマンドを順に実行してください。
ネームスペース名の確認は、OCIコンソールの画面右上の人型アイコンをクリック -> [テナンシ名] -> [オブジェクト・ストレージ・ネームスペース]で確認できます。

# 1. コンテキストの作成 (レジストリURLなどは自分の環境に合わせて変更)
fn create context <コンテキスト名(任意の値でOK)> --provider oracle

# 2. コンテキストの選択
fn use context <コンテキスト名(上記で決めたもの)>

# 3. コンパートメントIDの設定
fn update context oracle.compartment-id <作業コンパートメントのOCID>

# 4. APIエンドポイントの設定 (東京リージョンの場合)
fn update context api-url https://functions.ap-tokyo-1.oraclecloud.com

# 5. コンテナレジストリの登録(東京リージョンの場合)
fn update context registry nrt.ocir.io/<ネームスペース名>/<リポジトリ名(任意の値でOK)>

OCIR(Docker)へのログイン
Docker がイメージをプッシュできるようにログインします。
パスワードには OCI ユーザー設定で発行した認証トークンを使用します。

docker login nrt.ocir.io
# Username: <ネームスペース名>/<ユーザー名(メールアドレス)>
# Password: <認証トークン>

6. 関数の作成準備

開発環境が整ったので、いよいよ関数の作成準備を実施していきます。
以下の作業を上から順に流れで実施していけば準備は完了です。

# 1. 任意のプロジェクトディレクトリを作成し、移動
mkdir <project directory>
cd <project directory>

# 2. Fn CLIをPythonランタイムで初期化
fn init --runtime python post-func

# 3. 作成されたpost-funcディレクトリに移動
cd post-func

# 4. ライブラリの定義をするためにrequirements.txtに以下上書き
fdk>=0.1.71
oci>=2.116.0

# 5. 設定ファイルであるfunc.yamlに以下上書き(memoryを256MBに変更)
schema_version: 20180708
name: post-func
version: 0.0.1
runtime: python
memory: 256
entrypoint: /python/bin/fdk /function/func.py handler

7. 関数func.pyの作成

post-funcディレクトリにあるfunc.pyに以下の関数を上書き保存します。
途中、ご自身の環境に対応するNAMESPACE_NAMEと、BUCKET_NAMEの書き換えを忘れずに実施してください。

import io
import json
import logging
import uuid
import datetime
import oci

from fdk import response

NAMESPACE_NAME = "<OCIテナンシのオブジェクト・ストレージ・ネームスペース>" 
BUCKET_NAME = "<作成したバケット名>"  

def handler(ctx, data: io.BytesIO = None):
    logging.getLogger().info("Function invoked")
    
    try:
        #1. 入力データの取得
        input_data = {}
        try:
            body = data.getvalue()
            if body:
                input_data = json.loads(body)
        except Exception:
            pass 

        #2. 認証プロバイダーの初期化
        #Resource Principalを使って、Function自身を認証します
        signer = oci.auth.signers.get_resource_principals_signer()
        
        #3. Object Storage クライアントの作成
        object_storage = oci.object_storage.ObjectStorageClient(config={}, signer=signer)

        #4. 保存データの作成
        post_id = str(uuid.uuid4())
        blog_post = {
            "id": post_id,
            "title": input_data.get("title", "No Title"),
            "content": input_data.get("content", "No Content"),
            "createdAt": datetime.datetime.now().isoformat()
        }
        
        content_str = json.dumps(blog_post)
        object_name = f"posts/{post_id}.json"

        logging.getLogger().info(f"Uploading to {BUCKET_NAME}/{object_name}")

        #5. Object Storageへアップロード
        object_storage.put_object(
            namespace_name=NAMESPACE_NAME,
            bucket_name=BUCKET_NAME,
            object_name=object_name,
            put_object_body=content_str.encode('utf-8')
        )

        return response.Response(
            ctx, 
            response_data=json.dumps({"message": "Success", "postId": post_id}),
            headers={"Content-Type": "application/json"}
        )

    except (Exception, ValueError) as ex:
        logging.getLogger().error(f"Error: {str(ex)}")
        return response.Response(
            ctx, 
            response_data=json.dumps({"error": str(ex)}),
            headers={"Content-Type": "application/json"},
            status_code=500
        )

8. 関数のデプロイ

ここまで来たらOCI Functionsの作業はもう終わりです!
デプロイし、実際にObject StorageにJSONが書き込まれているか、テストしてみましょう。

# 1. Fn CLIデプロイコマンド
fn -v deploy --app <app name> --no-cache

# 2. テスト実行コマンド
echo -n '{"title":"Python Test", "content":"Hello OCI Functions"}' | fn invoke <アプリケーション名> post-func

# 3. 成功時のレスポンス
{"message": "Success", "postId": "787acfd2-3ada-4d01-9785-3c5917fe48fc"}

無事に成功した場合、Object StorageにpostIdでアップロードされたことを確認できます。

9. API Gatewayの作成

次に、外部から呼び出すためのAPI Gatewayを作成しましょう。
左側ハンバーガーボタンより、[開発者サービス] -> [ゲートウェイ] -> [ゲートウェイの作成]をクリックします。
名前は任意の値で、タイプはパブリックにしましょう。コンパートメントは作業コンパートメントを指定します。
ネットワーク部分はこれまでと同じように、作業用VCNを選択し、サブネットはパブリックとします。
作成後、しばらく待つとアクティブとなります。

10. デプロイメント作成とルート設定

次に、ゲートウェイにデプロイメントを作成し、URLとFunctionsを紐付けます。
作成したAPI Gatewayの左メニューから[デプロイメント] -> [デプロイメントの作成]をクリックします。
名前は任意の値で、パス接頭辞は/v1とし、作業コンパートメントを設定して次をクリックします。
認証は一旦そのまま認証なしを選択し、次をクリックします。(商用環境は認証必須だと思いますが...一旦。)
ルート設定はパスを/postsに、メソッドをPOSTに、バックエンド・タイプをOracle ファンクションに変更します。
アプリケーションは事前に作成していたOCI Functionsアプリケーションを選択し、関数名はpost-funcを設定します。
最後に、設定値の確認をして問題がなければ、作成でOKです。

11. 動的グループ・ポリシーの設定(API Gateway to Functions)

API GatewayからFunctionsを呼び出するために、権限を付与する必要があります。
左側ハンバーガーボタンより、[アイデンティティとセキュリティ] -> [ドメイン]をクリックします。
現在使用しているドメイン(通常はDefault)を選択します。もし作業コンパートメントにDefaultドメインがない場合は、ルートコンパートメントに存在する可能性があります。
[動的グループタブ] -> [動的グループの作成]をクリックします。
名前と説明は任意で構いません。一致ルールには以下を記載しましょう。
作業コンパートメントのOCIDを調べるには、左側ハンバーガーボタンより、[アイデンティティとセキュリティ] -> [コンパートメント]をクリックし、詳細タブのOCIDをコピーしましょう。

ALL {resource.type = 'apigateway', resource.compartment.id = '<作業コンパートメントのOCID>'}

再びメニューから、[アイデンティティとセキュリティ] -> [ポリシー] -> [ポリシーの作成]をクリックします。
名前は任意の値で構いません。コンパートメントは作業コンパートメントを設定します。ポリシービルダーで以下をペーストしましょう。

Allow dynamic-group <動的グループ名> to use functions-family in compartment <コンパートメント名>

12. API Gatewayからの呼び出し

ここまでの作業を終えたら、実際にcurlコマンドを実行してみましょう!

# curlコマンド
curl -i -X POST -H "Content-Type: application/json" \
 -d '{"title":"Final Test", "content":"From API Gateway"}' \
 https://<API Gatewayのホスト名>/v1/posts

# 成功レスポンス
HTTP/2 200 
strict-transport-security: max-age=31536000
server: Oracle API Gateway
x-frame-options: sameorigin
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
opc-request-id: /30C01BDB57E44B0EA1CDF3DCD07021C6/82F2E4B2F05349C395F2439B06C09E50
content-type: application/json
date: Thu, 08 Jan 2026 04:42:03 GMT
content-length: 72

{"message": "Success", "postId": "43bd2196-fd7e-43a1-a83f-10673250bf52"}%  

おめでとうございます、無事にPOSTリクエストが成功し、記事の投稿が完了しました!

13. 検証中のトラブルについて

今回のハンズオン検証中に遭遇したトラブルについて、まとめてみます。

  • API Gatewayを呼び出した時にCouldn't connect to serverが発生する。
    API Gatewayを構築した後、curlコマンドでの呼び出しを試した際に以下のエラーが発生しました。
curl: (28) Failed to connect to <API Gateway ホスト名> port 443 after 75136 ms: Couldn't connect to server

原因はAPI Gatewayを配置したパブリックサブネットのセキュリティ・リストのイングレス・ルールに自グローバルIPのルールが存在しないためでした。
API GatewayのメトリックにあるAPIリクエストの数値が変化しなかったため、API Gatewayまで到達していないと考え、セキュリティ・リストの確認をして気づくことができました。
※ ハンズオン部分には手順として追記済みです。

  • API Gatewayを呼び出した時に504 Gateway Timeoutが発生する。
    上記のCouldn't connect to serverを解消した際に、再度打鍵をしたところ以下のエラーが発生しました。
{"code":504,"message":"Gateway Timeout"}%        

原因はAPI Gatewayを配置したパブリックサブネットのセキュリティ・リストのエグレス・ルール(0.0.0.0/0)にステートレスのチェックがついていたことでした。
0.0.0.0/0ですべてのプロトコルを許可しているのに、API Gateway(パブリックサブネット) <-> OCI Functions(プライベートサブネット)への疎通がNGとなってしまい、Timeoutとなりました。
検証のタイミングでステートレスにチェックを入れてしまったようです...。切り分けのため、OCI Functionsを単体で呼び出して、正常に呼び出されることを確認できたため、API Gateway <-> OCI Functionsのルートに異常があるのでは?と疑うことができ、原因に気がつくことができました。

おわりに

かなり長くなってしまいましたが、OCI Functions + API Gatewayを利用し、外部からブログ投稿APIを呼び出すことに成功しました。
普段はTerraformなどのIaCでAWSリソースを触ることの多い私ですが、コンソールでのイチからの構築は非常に新鮮で、かつOCI特有の動的グループ・ポリシーの書き方に難儀しました...。
ただ、API Gatewayの部分は非常に分かりやすく、AWSに比べてシンプルだなと感じました。
今回はPOST APIのみの実装となりましたが、時間があればGET APIの実装もしてみたいと思います。 次回は構築したOCI Functions + API Gatewayの監視をDatadogで検証しようと思います。
検証しました↓ techblog.ap-com.co.jp

関連記事

APC技術ブログでOCI Functionsを利用してみた記事は他にもありますので、ぜひ参考にしてみてください。

techblog.ap-com.co.jp techblog.ap-com.co.jp

お知らせ

私達クラウド事業部はクラウド技術を活用したSI/SESのご支援をしております。

www.ap-com.co.jp

また、一緒に働いていただける仲間も募集中です!
今年もまだまだ組織規模拡大中なので、ご興味持っていただけましたらぜひお声がけください。

www.ap-com.co.jp