function computeのTableStore Triggerを試してみる

Alibaba Cloudの記事になりますが、TableStore(NoSQL)のデータに変更があった際にサーバーレス関数のFunction Computeを起動する構成を試してみたいと思います。

AlibabaでTerraformを使ってリソース作成するのはやったことないので、一度コンソールで作成してから各リソースをimportすることで理解を深めて、その後リソース作り直しました。

RAMはコンソールから作った際のものを使い回しているので、もしかしたら最初からTerraformだとエラーになる可能性もありますが、ご了承ください。

Terraformのソースコードも貼っておきますので参考にしてみてください。

https://github.com/dychi/tf-alibaba-function-compute-trigger

## 前提条件

- Terraformのインストール
- Terraform実行するためのAlibaba Cloudのアクセスキーとシークレットキー

## Terraformで作成

Terraformで作成します。必要なリソースは以下です。

- Function Compute v3：サーバーレス関数
    - ソースコード
    - Function Compute Trigger：実行トリガー
- TableStore：NoSQL
- RAM Role / Policy：権限管理ロールとポリシー
- Simple Log Service(SLS)：ログサービス

ディレクトリ構成は以下になります。

```bash
.
├── Makefile // コマンド定義（Option）
├── README.md
├── function_compute.tf // Function Compute
├── local.tf
├── log_service.tf // Simple Log Service
├── main.tf
├── ram.tf // RAM
├── src // Function Compute用のソースコード
│   └── tablestore-trigger-func-code.zip
├── table_store.tf // TableStore
├── terraform.tfvars
└── variables.tf
```

### Function Compute v3

ざっくり説明：AWSのLambdaのようなサービス

Terraformのコード

```hcl
# Function Computeの設定
resource "alicloud_fcv3_function" "table_store" {
  function_name = "tablestore-trigger-func"
  memory_size   = "512"
  timeout       = "60"
  runtime       = "nodejs18"
  handler       = "index.handler"
  cpu           = "0.35"
  disk_size     = "512"
  code {
    zip_file = filebase64("${path.module}/src/tablestore-trigger-func-code.zip")
  }
  role = alicloud_ram_role.fc_default.arn
  log_config {
    log_begin_rule = "DefaultRegex"
    logstore       = alicloud_log_store.main.logstore_name
    project        = alicloud_log_project.main.project_name
  }

  environment_variables = {
    "TZ" = "Asia/Tokyo"
  }
  timeouts {}
  internet_access = "true"
}

# TableStoreのトリガーを設定
resource "alicloud_fcv3_trigger" "trigger" {
  trigger_type    = "tablestore"
  trigger_name    = "test-tablestore-trigger"
  qualifier       = "LATEST"
  trigger_config  = jsonencode({})
  source_arn      = local.trigger_table_store_arn
  invocation_role = alicloud_ram_role.trigger_role.arn
  function_name   = alicloud_fcv3_function.table_store.function_name
  timeouts {}
}

```

コードはOSSというストレージに配置しておくこともできますが、今回はコードをzipにしてあげるようにしています。

TableStoreのトリガーをTerraformで作成するときには、TableStoreのテーブルのコンソール画面でStreamを有効にする必要がありました。

![CleanShot 0006-10-31 at 14.21.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224001/4a1eab46-64ce-bd0c-9c12-a1a971ef6c3b.png)


#### ソースコード

TableStoreのイベントを受け取り、PKとemailをコンソールに吐き出します。

```jsx
'use strict';

// ロガーの設定
const logger = {
  info: console.log,
  error: console.error
};

import cbor from 'cbor';

/**
 * ハンドラ関数
 * @param {Buffer} event - イベントデータ
 * @param {Object} context - コンテキスト
 * @param {Function} callback - コールバック関数
 */
export const handler = async (event, context, callback) => {
  try {
    logger.info("イベントの処理を開始します");

    // イベントデータの解析
    const records = await cbor.decode(event);

    for (const record of records.Records || []) {
      logger.info("レコードを処理: %j", record);

      const pk = getPkValue(record, "PK");
      const email = getAttributeValue(record, "email");

      // PK と email の値を処理
      logger.info("PK: %s, email: %s", pk, email);
    }

    callback(null, 'OK');
  } catch (e) {
    logger.error("イベントの処理中にエラーが発生しました: %s", e.message);
    callback(null, 'Error');
  }
};

/**
 * 属性値を取得する
 * @param {Object} record - レコード
 * @param {string} column - 列名
 * @returns {any} 属性値
 */
const getAttributeValue = (record, column) => {
  const attrs = record.Columns || [];
  for (const x of attrs) {
    if (x.ColumnName === column) {
      return x.Value;
    }
  }
  return null;
};

/**
 * 主キー値を取得する
 * @param {Object} record - レコード
 * @param {string} column - 列名
 * @returns {any} 主キー値
 */
const getPkValue = (record, column) => {
  const attrs = record.PrimaryKey || [];
  for (const x of attrs) {
    if (x.ColumnName === column) {
      return x.Value;
    }
  }
  return null;
};
```

ソースコードはFunction Computeのコンソールでも編集できます。

![CleanShot 0006-10-31 at 14.02.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224001/d0abc639-96b4-464e-1c24-03596707a5fa.png)


### TableStore

ざっくり説明：AWSのDynamoDBのようなサービス

```hcl
# TableStoreのインスタンス
resource "alicloud_ots_instance" "test_instance" {
  name          = "test-fc"
  accessed_by   = "Any"
  instance_type = "Capacity"
  network_type_acl = [
    "VPC",
    "CLASSIC",
    "INTERNET" # Terraformで操作のために追加しました
  ]
  resource_group_id = var.resource_group_id
  tags = {
    env = "test"
  }
}

# TableStoreのテーブル
resource "alicloud_ots_table" "event_forms" {
  instance_name = alicloud_ots_instance.test_instance.name
  table_name    = "event_forms"
  time_to_live  = -1
  max_version   = 1
  primary_key {
    name = "PK"
    type = "String"
  }

  depends_on = [
    alicloud_ots_instance.test_instance
  ]
}
```

PrimaryKeyは PK として、その他のカラムは `email` と `allowCampaignSend` と `age` を追加しますが、Terraformのコード内では特に指定する必要がありません。

`network_type_acl` に "INTERNET" を追加していないとテーブルの操作ができませんでした。

### RAM

ざっくり説明：AWSのIAMのようなサービス

```bash
# Function Computeのデフォルトロール
resource "alicloud_ram_role" "fc_default" {
  name = "AliyunFcDefaultRole"
  document = jsonencode({
    "Statement" : [
      {
        "Action" : "sts:AssumeRole",
        "Effect" : "Allow",
        "Principal" : {
          "Service" : [
            "fc.aliyuncs.com"
          ]
        }
      }
    ],
    "Version" : "1"
  })
  description = "Default Service Role for FC to operate other resource"
}

# デフォルトのポリシーをアタッチ
resource "alicloud_ram_role_policy_attachment" "fc_default_policy" {
  policy_name = "AliyunFCDefaultRolePolicy"
  policy_type = "System"
  role_name   = alicloud_ram_role.fc_default.name
  depends_on = [
    alicloud_ram_role.fc_default
  ]
}

// TableStore Stream から Function Compute サービス関数をトリガーするためのロールとポリシーを作成
resource "alicloud_ram_role" "trigger_role" {
  name = "AliyunTableStoreStreamNotificationRole"
  document = jsonencode({
    Version = "1"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = [
            "ots.aliyuncs.com"
          ]
          RAM = [
            "acs:ram::${var.alicloud_account_id}:root" // Alibaba Cloud Account ID(自分のとは異なる)
          ]
        }
      }
    ]
  })
  description = "trigger role for OTS to invoke functions of {serviceName}"
  timeouts {}
}

# TableStore Stream から Function Compute サービス関数をトリガーするためのポリシー
resource "alicloud_ram_policy" "trigger_policy" {
  policy_name = "AliyunTableStoreStreamNotificationRolePolicy"
  policy_document = jsonencode({
    "Version" : "1",
    "Statement" : [
      {
        "Action" : [
          "ots:BatchGet*",
          "ots:Describe*",
          "ots:Get*",
          "ots:List*"
        ],
        "Resource" : "*",
        "Effect" : "Allow"
      },
      {
        "Action" : [
          "fc:InvokeFunction"
        ],
        "Resource" : "*",
        "Effect" : "Allow"
      }
    ]
  })
  description = "Function Compute サービス関数をトリガーするための TableStore Stream の認可ポリシー"
  timeouts {}
}

# TableStore Stream から Function Compute サービス関数をトリガーするためのロールにポリシーをアタッチ
resource "alicloud_ram_role_policy_attachment" "trigger_policy_attach" {
  policy_name = alicloud_ram_policy.trigger_policy.policy_name
  policy_type = "Custom"
  role_name   = alicloud_ram_role.trigger_role.name
  timeouts {}
  depends_on = [
    alicloud_ram_role.trigger_role
  ]
}

```

`AliyunTableStoreStreamNotificationRole`を作成するときに、コンソールで確認した自分のAccountIDと異なっていたので、コンソールからトリガーを作成したときに作られたAccountIDをvariablesにセットしています。

### Simple Log Service(SLS)

ざっくり説明：AWSのCloudWatch Logsのようなサービス

```hcl
# SLSのプロジェクト
resource "alicloud_log_project" "main" {
  project_name = "function-compute-log"
}

# SLSのログストア
resource "alicloud_log_store" "main" {
  project_name          = alicloud_log_project.main.id
  logstore_name         = "function-log"
  shard_count           = 3
  auto_split            = true
  max_split_shard_count = 64
  retention_period      = 90
  enable_web_tracking   = true
  append_meta           = false
  timeouts {}
  depends_on = [
    alicloud_log_project.main
  ]
}

```

## 検証

上記で作成したリソースでTableStoreにデータを追加したときに、Function Computeが実行されることを確認します。

#### 画面からTableStoreにデータを追加

追加するデータは、`email`が `test@gmail.com` で、`allowCampaignSend` が `false` 、`age`が`26` とします。

OKを押すとデータが追加されます。

![CleanShot 0006-10-31 at 14.31.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224001/5b7e8835-a2b6-194d-ac64-aba3e6421a92.png)


#### Function Computeで実行ログを確認

Function Computeの `Logs` から実行ログを確認すると、先ほど追加したデータが表示されていることがわかります。

![CleanShot 0006-10-31 at 14.33.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224001/b58b1741-8379-6113-958b-cfd301e533d6.png)


これでTableStoreにデータを追加したときに、Function Computeが実行されることを確認できました！

まとめに行く前に検証してみて良かったところと詰まったところを書いておきます。

## 良かったところ

- Function Computeのコンソール上でコードを編集する時にVSCodeを使える
- コンソールのVSCodeで編集してdeployまで、が早くて開発体験として良かった
    - `npm install cbor`して利用するときもターミナル開いて出来て簡単
- VSCodeの拡張機能として標準でTONGYI Lingmaが入っている
    - TONGYI Lingmaとは、Qwenを活用したAIコーディングアシスタント
    - ※ ただしデフォルト中国語なので日本語で回答するように指示してあげないといけない
- Function ComputeのLogsタブでTraceで処理時間確認できるの便利そう

## 詰まったところ

- TableStoreからのeventがCBOR形式で送られてくる
    - Testsタブから作成したイベントはJSON形式だったので混乱した
- TerraformでTableStoreのトリガーを作成しようと思ったが、サポートされていないようで、コンソールからTableのStreamをEnableにする必要がある
- 最初からTerraformでチャレンジしようと思ったが、Terraformのドキュメントがあまり細かいパラメータまで書いていないのと馴染みがなくて断念した→やはり最初コンソールで作ってimportしていくのが初学者には良さそう

## まとめ

TableStoreの変更をトリガーにFunction Compute 3.0を実行することができました。

初めてやってみたので大変なところはありましたが、Terraform化する過程で公式ドキュメントとか読んで少しは理解が深まったのかなと思います。

Function Compute2.0にはAsparaDB RDS for MySQLのトリガーもありましたが、廃止されてしまったようです。ただコンソールの画面を見ているとcoming soonとなっていたのでそのうち対応されるかもしれないですね。現在はEventBridgeを使うことで出来るとのこと。

調べてみたらAWSのAuroraにもRDSのトリガーはありましたが、Aurora MySQLのネイティブ関数というものを使うらしい。（詳しくは[こちら](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Integrating.Lambda.html)）

## 参考記事

- [alicloud_fcv3_function | Resources | aliyun/alicloud | Terraform | Terraform Registry](https://registry.terraform.io/providers/aliyun/alicloud/latest/docs/resources/fcv3_function)
- [alicloud_ots_table | Resources | aliyun/alicloud | Terraform | Terraform Registry](https://registry.terraform.io/providers/aliyun/alicloud/latest/docs/resources/ots_table)
- [EventBridge を経由した、Function Compute (Alibaba Cloud) の RDS トリガー検証｜クラウドテクノロジーブログ｜ソフトバンク](https://www.softbank.jp/biz/blog/cloud-technology/articles/202302/rds-trigger/)
- [Aurora MySQL ネイティブ関数を使用した Lambda 関数の呼び出し - Amazon Aurora](https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Integrating.NativeLambda.html)