Meta Conversions API Gatewayへの複数ホスト名割り当て

## この記事について

この記事は、「[Relic Advent Calendar 2022](https://qiita.com/advent-calendar/2022/relic)」の18日目の記事です。

Meta(Facebook) 広告の **Conversions API Gateway** サーバに対して、複数のホスト名（ドメイン名）を割り当てる方法について紹介します。

想定読者:
- 既にConversions API Gatwewayを運用されていて、そこに複数のホスト名を割り当てたいと思っている方
- UNIX/Linux の基本的なコマンド操作に慣れている方
- AWS環境の基本的な操作に慣れている方


<details>
<summary>この記事を書いた背景</summary>

[CAPI Gatewayの公式ドキュメント](https://developers.facebook.com/docs/marketing-api/conversions-api/guides/gateway#multi-pixel-and-multi-domain-support-for-meta-pixel)には、下記のように複数ドメインサポートがある旨の記載がありました。

> Multi-Pixel and Multi-Domain Support for Meta Pixel
Conversions API Gateway supports multiple domains. For example, you can have multiple domains such as “domain.com” , “domain.co.uk”, “anotherdomain.com”. All of these domains could be configured within a single instance of Conversions API Gateway during deployment. Please review the setup guide for more details.

しかし、CAPI GatewayサーバのIPアドレスにドメインを割り当てただけだと、アクセスしてもNginx 404エラー画面が表示され、計測データ送信もできない状態でした。
また、管理画面上などに該当する設定項目が見当たらず、公式ドキュメントにも詳しい設定方法見当たりませんでした。

そこで、仕方なく実際にEC2インスタンス上で稼働するCAPI Gatwayシステムの状況を調べたところ、最低限設定可能な方法が見つかりました。この記事は、その記録です。

</details>


### 前提条件

Conversions API Gatewayの[標準セットアップ手順](https://developers.facebook.com/docs/marketing-api/conversions-api/guides/gateway/setup)は、既存のAWS環境に対して、EC2サーバインスタンスを追加する、というものです。
そのため、以降の設定方法はAWS環境上での動作を想定します。また、AWSアカウントのセットアップに関する説明は省略します。

また、 Conversions API GatewayのAppバージョンは、執筆時点で最新の v1.5.8 を前提としています。

### 複数ホスト名対応の動機

=> 端的にいうと、運用・管理費用の節約です。

CAPI Gatewayサーバは、AWS EC2 m5.large 以上のインスタンスを常時稼働させる必要があります。
個々のサイトのConversion計測リクエストがそこまで多くない場合に、1ドメイン1サーバの構成は非経済的なので、1サーバに集約したい、という判断です。

## Conversions API Gateway (CAPI Gateway) とは

Meta (Facebook)広告コンバージョン計測用ピクセルからのイベントデータを受け取り、Meta計測サーバに転送します。サーバホスト名を適切に設定することで、サードパーティCookieの利用に制限のあるブラウザでも、計測データを取得することが可能になります。

[公式ドキュメント](https://developers.facebook.com/docs/marketing-api/conversions-api/guides/gateway)より引用
> コンバージョンAPIゲートウェイは、イベントマネージャのセルフサービス構成オプションです。コンバージョンAPIゲートウェイを使用すると、事業者は、専用の開発者リソースなしに、以下のベストプラクティスガイダンスを統合して、Metaピクセルや冗長設定のコンバージョンAPIによりイベントを送信できます。そのため、サードパーティパートナーやコーディングが不要になります。

CAPI Gatewayのアーキテクチャ概要
![capi-gateway-overview.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434222/d3fcd8fe-2fbe-7ddc-e2c7-47b76713063e.png)
（出典: https://developers.facebook.com/docs/marketing-api/conversions-api/guides/gateway , 画像取得日: 2022-12-18）

### CAPI Gatwayの実体

[CAPI Gatewayの標準セットアップ手順](https://developers.facebook.com/docs/marketing-api/conversions-api/guides/gateway/setup) に従い、AWS CloudFormationのテンプレートをデプロイすると、CAPI Gateway Appの稼働するEC2サーバインスタンスが作成されます。

EC2インスタンスにログイン※して確認したところ、実体は [microk8s](https://microk8s.io) (kubernetes) で管理されたマイクロサービスであることがわかりました。
<details>
<summary>※EC2インスタンスへのログイン設定について </summary>

EC2インスタンスには [AWS SSM Session Manager](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/session-manager.html) agentがインストール済みのため、SSM関連の設定をすればそのままEC2コンソール画面からログイン可能です。

</details>

![capi-gateway-ec2-internal.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434222/d370cea8-f7dc-ae82-553f-52400c909a44.png)

主な構成要素は、
- Ingress (Nginx)
- CAPI Gateway データ受信サーバ
- CAPI Gateway 管理画面 App
- DB (MySQL)
- Logging/Monitoring関連サービス

のようです。

この構成要素のうち、 **Ingress サービス** (Nginx) が、初期セットアップ時に指定したホスト名宛のリクエストしか受信しない設定となっています。
複数ホスト名（ドメイン名）でのアクセスを許可するため、この Ingress の設定を更新する手順を以下に記載します。


## 設定方法

CAPI Gateway (EC2) およびALB, Session ManagerなどのAWSインフラ設定が一通り完了している状態で、新規にドメインを追加する設定操作例です。

設定手順説明用パラメータ:
- Pixelタグ設置先ドメイン: `*.dev.example.com`
- CAPI Gatewayに割り当てるホスト名: `capig.dev.example.com`

### 1. EC2上のmicrok8s Ingress (Nginx) 設定の更新

1. CAPI Gateway EC2インスタンスにログイン (AWS EC2コンソール -> Session Manager など)
1. ドメイン追加スクリプト実行

    ```sh
    $ cd /home/ubuntu
    $ ./add-capi-domain.sh dev.example.com
    ```

    - "ingress.networking.k8s.io/capig-data-ingress-dev.example.com created" のようなメッセージが表示されれば作成成功です

<details>
    <summary>詳細確認方法</summary>

`microk8s kubectl describe`   コマンドを実行し、下記のような表示が出れば、設定に成功しています。引数で与える対象ルール名は、 ` ingress capig-data-ingress-` + 追加したドメイン名です。

  ```sh
 $ microk8s kubectl describe ingress capig-data-ingress-dev.example.com
Name:             capig-data-ingress-dev.example.com
Namespace:        default
Address:          10.152.183.70
Default backend:  default-http-backend:80 (<error: endpoints "default-http-backend" not found>)
Rules:
  Host               Path  Backends
  ----               ----  --------
  *.dev.example.com
                     /capig          capig:capig (10.1.141.111:8080)
                     /auth           hub:ui (10.1.141.68:8080)
                     /static         capig:capig (10.1.141.111:8080)
                     /.open-bridge   capig:capig (10.1.141.111:8080)
                     /eventbus       capig:capig (10.1.141.111:8080)
                     /events         capig:capig (10.1.141.111:8080)
                     /events_ws      capig:capig (10.1.141.111:8080)
                     /capi           capig:capig (10.1.141.111:8080)
                     /tr             capig:capig (10.1.141.111:8080)
                     /sdk            capig:capig (10.1.141.111:8080)
                     /echo           capig:capig (10.1.141.111:8080)
                     /testendpoint   capig:capig (10.1.141.111:8080)
                     /token          capig:capig (10.1.141.111:8080)
Annotations:         meta.helm.sh/release-name: capig-dev.example.com
                     meta.helm.sh/release-namespace: default
                     nginx.ingress.kubernetes.io/configuration-snippet: more_set_headers "Access-Control-Allow-Origin: $http_origin";
                     nginx.ingress.kubernetes.io/cors-allow-methods: PUT, GET, POST, OPTIONS
                     nginx.ingress.kubernetes.io/enable-cors: true
                     nginx.ingress.kubernetes.io/proxy-body-size: 1024m
Events:              <none>
```
</details>

### スクリプトと関連ファイル

ドメイン追加スクリプト `add-capi-domain.sh`

```sh
#!/bin/sh

KUBECTL_CMD='microk8s kubectl'
TEMPLATE_FILE=${TEMPLATE_FILE:-template-ingress-capig.yml}

DOMAIN=$1

sed "s/%%BASE_DOMAIN%%/${DOMAIN}/g" ${TEMPLATE_FILE} | \
  $KUBECTL_CMD apply -f -
```

k8sの管理コマンドである `kubectl` を実行して、設定ファイルテンプレートの内容を適用する、というものです。
下記のような設定テンプレートファイルに、追加したいホスト名を埋め込むことで、ホスト名ごとの設定を追加します。

<details>
<summary>設定ファイルテンプレート例</summary>

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    meta.helm.sh/release-name: capig-%%BASE_DOMAIN%%
    meta.helm.sh/release-namespace: default
    nginx.ingress.kubernetes.io/enable-cors: "true"
    nginx.ingress.kubernetes.io/configuration-snippet: |
      more_set_headers "Access-Control-Allow-Origin: $http_origin";
    nginx.ingress.kubernetes.io/cors-allow-methods: PUT, GET, POST, OPTIONS
    nginx.ingress.kubernetes.io/proxy-body-size: 1024m
  name: capig-data-ingress-%%BASE_DOMAIN%%
  namespace: default
spec:
  ingressClassName: cloudbridge-ingress-class
  rules:
  - host: "*.%%BASE_DOMAIN%%"
    http:
      paths:
      - backend:
          service:
            name: capig
            port:
              name: capig
        path: /capig
        pathType: Prefix
      - backend:
          service:
            name: hub
            port:
              name: ui
        path: /auth
        pathType: Prefix
      - backend:
          service:
            name: capig
            port:
              name: capig
        path: /static
        pathType: Prefix
      - backend:
          service:
            name: capig
            port:
              name: capig
        path: /.open-bridge
        pathType: Prefix
      - backend:
          service:
            name: capig
            port:
              name: capig
        path: /eventbus
        pathType: Prefix
      - backend:
          service:
            name: capig
            port:
              name: capig
        path: /events
        pathType: Prefix
      - backend:
          service:
            name: capig
            port:
              name: capig
        path: /events_ws
        pathType: Prefix
      - backend:
          service:
            name: capig
            port:
              name: capig
        path: /capi
        pathType: Prefix
      - backend:
          service:
            name: capig
            port:
              name: capig
        path: /tr
        pathType: Prefix
      - backend:
          service:
            name: capig
            port:
              name: capig
        path: /sdk
        pathType: Prefix
      - backend:
          service:
            name: capig
            port:
              name: capig
        path: /echo
        pathType: Prefix
      - backend:
          service:
            name: capig
            port:
              name: capig
        path: /testendpoint
        pathType: Prefix
      - backend:
          service:
            name: capig
            port:
              name: capig
        path: /token
        pathType: Prefix
```
</details>

#### 補足: CORS設定について

上記設定では、CORS設定(`access-control-allow-*`)を意図的に追加しています。

これは、例えば dev.example.com の配下に、 www1.dev.example.com や www2.dev.example.com など複数の計測対象サイトがあるケースに対応しやすくするためです。
CORS設定を有効化することで、capig.dev.example.com のような計測対象サイトのsiblingsホスト名1つを使い回して利用することが可能となります。

逆に、もしCORS設定を追加しない場合、capig.www1.dev.example.com, capig.www2.dev.example.com のように、サイトごとにCAPI Gateway用のドメインを割り当てる必要があります。

関連: https://github.com/kubernetes/ingress-nginx/issues/8469


### 2. SSL/TLS証明書発行

ALBでHTTPSリクエストを受け付けられるように、CAPI Gateway サーバが稼働するAWSアカウントのACM上で、 *.dev.example.com ドメイン用のSSL証明書の登録 or 新規作成（DNS認証など）を行なってください。

### 3. DNSレコード設定

 CAPI Gatewayサーバにリクエストが到達するように、 Pixelタグ設置先ドメインのDNS（AWS Route53など）の管理者の方に、下記レコード設定を依頼してください

- capig.dev.example.com -> CAPI Gateway Server IPアドレスの Aレコード
    - AWS Route53の場合は、 ALBのDNS名のaliasを推奨 ( 例: capig-alb-xxxxxxxx.ap-northeast-1.elb.amazonaws.com )
- SSL/TLS証明書発行のための CNAME レコード（手順2でDNS認証のSSL/TLS証明書作成を選択した場合に必要です）

### 4. ALB へのSSL証明書設定

ACMに新規追加した証明書を, CAPI GatewayサーバのALBに設定してください。

EC2 -> LoadBalancers -> 対象ALB -> HTTP:443 Listener -> Certificates
    -> Listener certificates for SNI -> Add certificate

### 5. 動作確認

ここまでの設定で、 CAPI Gatewayサーバ (例: capig.dev.example.com) にリクエストが送信可能な状態になっているはずです。
下記のようなコマンドを実行して疎通確認をしてください。

```sh
$ curl -i -X OPTIONS https://capig.dev.example.com
HTTP/2 204
date: Fri, 11 Nov 2022 08:35:02 GMT
access-control-allow-credentials: true
access-control-allow-origin: *
access-control-allow-methods: PUT, GET, POST, OPTIONS
access-control-allow-headers: DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization
access-control-max-age: 1728000
```

### その他確認事項

本スクリプトによる設定結果について、その有効期間について確認しました。

確認結果:
- 本スクリプトによる設定結果は、EC2インスタンス再起動でも消失しない
- 本スクリプトによる設定結果は、CAPI Gateway の自動更新（v1系のマイナーアップデート）の実行後も消失しない

## 複数ホスト名対応後のCAPI Gateway構成イメージ

![capi-gateway-multi-hostnames.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434222/e027c22d-b5ee-5830-766e-e80c52d96b5d.png)


## FutureWork

- [Helm](https://helm.sh) でのIngress (Nginx) 設定管理に移行
- CORS設定の `access-control-allow-origin` にホスト名を指定できるようにする

## まとめ

構築済みのMeta Conversions API Gatewayサーバに対して、複数のホスト名（ドメイン名）を割り当てる方法について紹介しました。
動機はお金（の節約）です。