OCI:WAFとFunctionsを使ったSorryPageの自動切り替えの実装手順

# はじめに

- OCIでWebサービスをロードバランサ経由で利用している場合に、Sorryページを表示する手順になります。
- この方法では、WAFはレスポンスとしてWebページを提供することができるため、LBのバックエンドサーバが利用不可の場合に自動的にSorryページに切り替えます。
- Functionsは必要ですが、2023年12月現在でもOCIでSorryPageを表示する有力な手段です。サンプルコードの説明も交えて説明します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266598/47d9052e-0313-9857-0334-31c7641fd5d8.png)

### Appendix: 本記事以外で、SorryPageを出す方法紹介

この記事で紹介する方法以外にも２つ、SorryPageに切り替える手法がありますので、以下に参考として記載します。

- ② LBの機能でリダイレクト機能を利用する（Object Storageや、API Gatewayのコンテンツにリダイレクト）
- ③ LBのバックアップ機能で他のバックエンドサーバのSorryページに切り替える

それぞれのメリット・デメリットは以下です。

| 方法 | メリット | デメリット | 参考URL |
| ------ | ------ | ------ | ------ |
| ① ここで詳細する方法 | 専用Compute不要/URLは不変 | 自動化はFunctionsを利用 | ＜本記事＞ |
| ② LBリダイレクト機能 | 専用Compute不要 | SorryPageはURLが変わる | [クラウド・ネイティブ・サービスを使用したロード・バランサのカスタム・エラー・ページの実装](https://docs.oracle.com/ja/solutions/implement-lbaas-error-page/index.html#GUID-BCFAB6D5-6CE2-4468-8B5D-42A9409E0AE3) |
| ③ LB標準機能 | LB機能のみで自動的にできる | Sorryページ用Computeが必要 | [【OCI】Webサイトにソーリーページを実装する方法](https://qiita.com/araidon/items/7225e5d6de9a51875b8f) |


# 前提条件・環境
- Regionはus-phoenix-1 で利用
- VCNおよびサブネットは作成済み
- Load BalancerおよびWebコンテンツサーバ用Compute(NGINX)構成済み
- WAFに登録するSorryPage用コンテンツのために、画像はObject Storageへ配置済み
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266598/eb02cfbb-ce80-07cb-52e9-fdb0ba52e037.png)



# 構成順序(Agenda)

1. 前提条件環境の確認
1. WAFを構成
1. Fn(Functions)の構成
1. 通知先の作成
1. アラームの作成
1. 動作確認

------

# 1. 前提条件環境の動作確認

- ブラウザから本来のWebコンテンツを確認します。
- インターネット経由、Flexible Load Balancer経由で、NGINXのhttps://www.XXXXX.work にアクセスしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266598/e7fb3fbb-edbd-6736-3a62-3769fdeda888.png)


# 2. WAFを構成

### 2-1.WAFポリシーの作成とロードバランサーへの割り当て

- OCIコンソールからWAFポリシーを作成します。
    - ここでは保護ルール等には触れませんので、一旦デフォルトのまま何も制御しません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266598/8d7cdec5-9acf-aa9a-7a9f-2c7f94a9f3ef.png)

- 作成したポリシーを構成済みのロードバランサにアタッチします。
    - 以下は（Flexible Load BalancerにWAFをアタッチした後の画像）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266598/2a2319ae-24e7-1ebb-5a77-8c7635889429.png)


### 2-2.アクションの追加

- WAFポリシー画面内、[アクション]-[アクションの管理]を開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266598/5e1c8e86-5f7f-ab58-5e17-49c294f2ce59.png)

- [アクションの追加]を押下。
    - タイプ[返却HTTPレスポンス]を選択。
    - 名前は任意。ここでは「Sorry Page」としています。後述のFunctionsで指定する名称。
    - レスポンス・コードは任意（503が一般的）。
    - レスポンス・ページ本文にSorryページ用のHTMLを記載します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266598/c9176931-d8b1-ed80-5a75-64f73e3fa4b1.png)

:::note info
<img src="https:・・・・・.jpg"   は、自身の画像などに置き換えてください。この行は無くても問題ありません。　
:::

- Sample：レスポンス・コードに登録したHTMLファイル（Sorryページ）

```html:SorryPage.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Sorry Page</title>
</head>
<body>
    <h1>Sorry Page</h1>
    <p>申し訳ありませんが、現在サイトのメンテナンス中です。</p>
    <p>ご迷惑をおかけして申し訳ありません。</p>
    <img src="各自コンテンツ画像へのURIパスなどに置き換え.jpg"
        style="width: 30%;" alt="image">
</body>
</html>
```




# 3. OCI Functions の構成

### 3-1. アプリケーションおよび関数の作成

- OCI公式ドキュメントの通り、まずはサンプル(Hello-worldが表示される)の関数を作成します。
- ここではサンプルが動作することのみを確認します。
    - https://docs.oracle.com/ja-jp/iaas/Content/Functions/Tasks/functionsquickstartguidestop.htm


### 3-2. WAF設定を更新するための関数の作成

- 以下の手順で、3-1で作成したアプリケーション「helloworld-app」の中に、「update-waf」という関数を作成します。

1. 任意のディレクトリを作成。ここでは「update-waf」ディレクトリを作成。
1. Pythonランタイムの関数を作成。関数名は「update-waf」。
    - ディレクトリ内に3つのファイルが生成されることを確認。「func.py」「func.yaml」「requirements.txt」
- ここまでの内容は以下3行のみ。
```bash
mkdir update-waf
cd update-waf
fn init --runtime python update-waf
```

### 3-3. WAF設定を更新するためのコードをデプロイ

#### 3-3-1. requirements.txt　の内容を変更します。
    - [oci]を1行追加するのみです。
```plaintext:requirements.txt
fdk>=0.1.57
oci
```

#### 3-3-2. func.py　の内容を変更します。
- 以下の[func.py]の内容を丸ごとコピペします。
- 以下Noteを参考に中身を変更します。

:::note info
1. 以下部分Action Nameのみ、自身で作成したWAFのアクション名に変更してください。
    if messageType == "FIRING":
         action_name = "SorryPage"
2. WAFポリシーのOCIDも、自身のWAFポリシーのものに置き換えます。
    waf_policy_id = 
:::


```diff_python:func.py
import io
import json
import oci

from fdk import response

def handler(ctx, data: io.BytesIO = None):
    messageType = "OK"

    # アラームから受けたJsonの内容から、[alarmdata[]のstatus]キーの値を取り出し、変数[messageType]に代入
    try:
        body = json.loads(data.getvalue())
        messageType = body.get('alarmMetaData')[0].get('status')
    except (Exception, ValueError) as ex:
        print('ERROR: Missing parsing json payload: ' + str(ex), flush=True)

    # Functions自身がOCIのリソースプリンシパル認証を利用する場合の十八番
    signer = oci.auth.signers.get_resource_principals_signer()

    # 変更したいWAFポリシーのOCIDに置き換える
-    waf_policy_id = 'ocid1.webappfirewallpolicy.oc1.phx.amaaaaaavun44msamplesamplesamplesamplesample'
+    waf_policy_id = '自身のOCI WAFポリシーのOCIDを記載する'

    # 変数[messageType]の内容を判断し、OK_TO_FIRINGの場合、SorryPage を変数[action_name]に代入
    # 補足：WAFポリシー内で作成するアクションの名前に合わせて、"SorryPage" を変更する。"Pre-configured Allow Action"はデフォルトで存在するもの。
    if messageType == "FIRING":
        action_name = "SorryPage"
    else:
-       action_name = "Pre-configured Allow Action"
+       action_name = "{手順2-2で作成したWAFのアクション名に置き換える}"

    # 以下、WAFポリシーを変更し、リクエスト制御で、デフォルトレスポンスをWAFで事前作成していた「SorryPage」に変更する
    waf_client = oci.waf.WafClient(config={},signer=signer)
    update_default_access_control = oci.waf.models.RequestAccessControl(
        default_action_name=action_name)
    waf_policy_details = oci.waf.models.UpdateWebAppFirewallPolicyDetails(
        request_access_control=update_default_access_control)

    #↑までは準備。以下で実際に設定変更する
    try:
        waf_client.update_web_app_firewall_policy(waf_policy_id, waf_policy_details)
    except  (Exception, ValueError) as ex:
        print('error Update WAF failed: ' + str(ex), flush=True)

    # 戻り値として、action_nameと、typeも表示させてアクションが正しいかを判断する
    return response.Response(
        ctx,
        response_data=json.dumps({"status": "Update WAF Success","action":action_name,"messageType":body.get("type")}),
        headers={"Content-Type": "application/json"}
    )

```

#### 3-3-3. 関数をデプロイします。
- 以下を実行します。
    - 以下1行の実行で手元のファイルコンテンツ一式を用いてコンテナが生成され、OCI上のコードリポジトリにアップロード、OCI Functionsに関数がデプロイされます。

```bash
pwd #実行ディレクトリの確認のみ。 update-waf ディレクトリ内で実行すること。
fn -v deploy --app helloworld-app
```

##### 参考： Functionsデプロイ時の挙動
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266598/30586e01-e427-cb5c-847c-5aff0c195449.png)

デプロイに成功すると、関数が作成されます。以下はOCI上の画面イメージです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266598/45b26977-56f6-498c-b4db-be1c158b9e51.png)



# 4. 通知先の作成

- 多くのケースでは通知先としてEメールアドレスを設定することが多いと思いますが、ここではFunctionsを通知先になるように設定します。
- OCIコンソールから [開発者サービス]-[アプリケーション統合]-[通知]を開き、[トピックの作成]を押下します。トピック名は任意。
- その後トピック内のサブスクリプションで以下の様に作成したファンクションを設定します。
- サンプル：
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266598/65d9e1a0-6293-f147-5959-8424b82c547e.png)



# 5.アラームの作成

- LBを監視し、エラーしたら先ほどのファンクションを起動するようなアラームを作成します。
- OCIコンソールから [監視および管理]-[モニタリング]-[アラーム定義]を開き、[アラーム作成]を押下します。
- 以下の様な設定を行います。ポイントは★
    - アラーム名：任意
    - アラームの重大度：任意
    - アラーム本体：任意
    - タグ：任意
    - メトリック・ネームスペース：oci_lbaas ★
    - メトリック名：UnHealthyBackendServers ★
    - 間隔：1分
    - 統計：Min
    - ディメンション：backendSetName ★
    - ディメンション値：{ロードバランサーで設定している名前} ★
    - 集計メトリック・ストリーム： 無効（デフォルト）
    - トリガー・ルール：次と等しい　「１」　
    - 宛先サービス：通知　★（通知サービスの中、トピックの設定がFunctionsであること）
    - メッセージのグループ化：メトリック・ストリーム全体で通知をグループ化（デフォルト）
    - メッセージの形式：フォーマットされたメッセージの送信　or  RAWメッセージの送信 ★
    - 通知の繰り返し：任意
    - 通知頻度：任意

- サンプル：
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266598/fb0e2f0c-549e-5a6c-48f9-616fa6b9de2e.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266598/652267db-c0bc-234f-180f-8eed4fe3a1ef.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266598/4552bbc8-918d-7699-e650-c33d8b0e665c.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266598/bbdae37b-6746-cbd2-393d-6f5a62fb3ffb.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266598/2847fd75-0e88-96b6-57a6-300d2952305b.png)


# 5. 動作確認

### 5-1. 障害時の動作 (Sorryページ表示)

- NGINX Computeを停止

- サイトに接続したときの状態
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266598/cf52345d-ada7-cf88-9ad8-1cad698fca24.png)

- アラームの状態
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266598/9b272df8-213b-3a01-ffe6-7685fdbd6785.png)

- WAFの状態
    - （WAFポリシーを開き、「リクエスト制御の管理」ボタンを押下しないと設定が確認できない
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266598/ba075ba1-8937-ec1f-bae4-66a37c01b37d.png)

- アクション名が「SorryPage」に変わっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266598/fad5ef16-0f59-9322-87d2-422de847dac9.png)

- Functionsの実行ログ（OCI Loggingサービス
    - アラームからRecieveし、FunctionsをInvoke（実行）している
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266598/1fa40e15-fa05-1525-af02-0913caac0b40.png)


### 5-2. 復旧時の動作 (本来のコンテンツを提供)

- NGINX Computeを起動

- サイトに接続したときの状態
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266598/29f18a4e-283a-0421-6f8b-50286c303d36.png)

- アラームの状態
    - 復旧している
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266598/973af5e2-b86c-32a3-6833-14aa3db23c3d.png)

- WAFの状態
    - アクション名がデフォルトの「Pre-configuration Allow Action」となっている（ブラウザで画面更新後に確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266598/e61748f9-0e8f-98ee-b509-466a46b54428.png)



## 参考URL一覧
- クラウド・ネイティブ・サービスを使用したロード・バランサのカスタム・エラー・ページの実装
    - https://docs.oracle.com/ja/solutions/implement-lbaas-error-page/index.html#GUID-BCFAB6D5-6CE2-4468-8B5D-42A9409E0AE3
- 【OCI】Webサイトにソーリーページを実装する方法
    - https://qiita.com/araidon/items/7225e5d6de9a51875b8f
- 関数QuickStartガイド
    - https://docs.oracle.com/ja-jp/iaas/Content/Functions/Tasks/functionsquickstartguidestop.htm
- ファンクションの作成およびデプロイ
    - https://docs.oracle.com/ja-jp/iaas/Content/Functions/Tasks/functionsuploading.htm


以上