Skip to content

feat(BOXP-120): lolice cluster メンバーポータルのセルフサービス化#11314

Open
boxp wants to merge 21 commits into
mainfrom
feature/BOXP-120-lolice-member-portal
Open

feat(BOXP-120): lolice cluster メンバーポータルのセルフサービス化#11314
boxp wants to merge 21 commits into
mainfrom
feature/BOXP-120-lolice-member-portal

Conversation

@boxp

@boxp boxp commented Jul 17, 2026

Copy link
Copy Markdown
Owner

概要

lolice cluster(PalWorldゲームサーバー)への参加をセルフサービス化するメンバーポータルを実装します。

実装内容

  • Cloudflare Pages: 申請フォーム(/index.html)と参加手順(/guide.html)の���スティング
  • Cloudflare Workers: フォーム受信・承認メール送信・Cloudflare Access Policy自動更新のバックエンド
  • Cloudflare KV: 承認待ちリクエスト(メールアドレス+トークン)の一時保存(TTL 7日)
  • Cloudflare D1 (SQLite): 承認済みメールの強整合ストレージ(KVの結果整合性を回避)
  • Resend: メール送信(承認メール・案内メール)
  • AWS SSM Parameter Store: シークレット(CF_API_TOKEN・RESEND_API_KEY)の安全な管理

フロー

  1. 参加希望者がフォームにメールアドレスを入力して送信
  2. boxp(tiyotiyouda@gmail.com)に承認確認ページのリンクが届く
  3. 確認ページでPOSTボタンを押すとメールアドレスがCloudflare Access policyに自動追加
  4. 申請者に参加手順ページのURLが案内される

並行承認の安全性

addEmailToAccessPolicy でCloudflare Access PolicyのRead-Modify-Writeを行う際:

  • 承認済みメールをCloudflare D1(SQLite、ACID保証)に永続化してからAPI呼び出し
  • PUTの度にD1から全承認メールを読み込んでポリシーにマージ(D1は強整合なので並行書き込みも即座に反映)
  • 検証ステップでD1の最新スナップショットとポリシーの一致を確認、差異があれば再試行(最大5回)

これにより、複数の承認が並行した場合でも最終的にすべての承認済みメールがポリシーに反映される。

デプロイ前に必要な設定(terraform apply 前)

シークレットをAWS SSM Parameter Storeに設定する必要があります:

# CF_API_TOKEN(Zero Trust: Edit + Account Settings: Read 権限)
aws ssm put-parameter \
  --name /lolice-member-portal/CF_API_TOKEN \
  --value "<Cloudflare API Token>" \
  --type SecureString \
  --region ap-northeast-1

# RESEND_API_KEY(resend.com で取得)
aws ssm put-parameter \
  --name /lolice-member-portal/RESEND_API_KEY \
  --value "<Resend API Key>" \
  --type SecureString \
  --region ap-northeast-1

設定後、通常通り terraform apply を実行するとSSMからシークレットが自動で読み込まれます。

tfaction

terraform/cloudflare/b0xp.io/lolice-member-portal/ ディレクトリが追加されます。
PRマージ後に tfaction が terraform plan → apply を実行します。

…ber-portal

CI失敗の原因: aqua/ディレクトリが存在せずterraformバイナリが見つからなかった。
moltworkerと同じv1.15.8のterraform、v0.63.1のtflint、v0.72.0のtrivyを設定。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@boxp-tfaction

This comment has been minimized.

- GET /api/approve now returns a confirmation HTML page; POST /api/approve
  performs the actual approval, preventing link-preview bots from approving
  requests without admin intent
- Validate payload is a non-null object in POST /api/request before accessing
  payload.email, returning 400 instead of 500 for null/non-object bodies
- Wrap read-modify-write in addEmailToAccessPolicy with a 3-attempt retry
  loop to handle concurrent approval race conditions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@boxp-tfaction

This comment has been minimized.

@boxp-tfaction

This comment has been minimized.

acquirePolicyLock/releasePolicyLockを追加し、addEmailToAccessPolicy内の
read-modify-write全体をKVベースのアドバイザリーロックで保護する。

KVはアトミックCASを持たないためベストエフォートだが、既存のPUT後の
検証(verify)ループと組み合わせることで、並行承認による上書きを
検出・再試行し、全メールアドレスが最終的にポリシーに含まれることを保証する。
maxRetriesも5→8に増加。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@boxp-tfaction

This comment has been minimized.

…limiting

- Replace KV-based advisory lock (non-atomic, racy) with a singleton
  PolicyUpdater Durable Object. The DO runtime serializes concurrent
  requests, eliminating the read-modify-write race on Access Policy PUT.
- Add IP-based rate limiting (3 req/hour) to POST /api/request to prevent
  admin email spam from bots.
- Remove dead KV lock code (acquirePolicyLock, releasePolicyLock).
- Add durable_object_namespace_binding to worker.tf.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@boxp-tfaction

This comment has been minimized.

…om worker.tf

cloudflare/cloudflare v4.52 does not support durable_object_namespace_binding
in cloudflare_workers_script. Replace PolicyUpdater Durable Object with
inline read-modify-write+verify retry loop (3 attempts) in addEmailToAccessPolicy.
Manual approvals are sequential in practice so the retry handles the edge case.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@boxp-tfaction

This comment has been minimized.

…e Object

Replace the retry-based read-modify-write in addEmailToAccessPolicy with a
singleton PolicyUpdateDO Durable Object. The DO's single-threaded event loop
queues concurrent approval requests, so each GET→PUT sees the fully committed
result of the previous write and no email addition can be silently overwritten.

Also add durable_object_namespace_binding and migrations block to worker.tf.
@boxp-tfaction

boxp-tfaction Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

❌ terraform/cloudflare/b0xp.io/lolice-member-portal: terraform validate

Build link

terraform validate
Details
Jul 17 13:06:30.688 WRN The policy file is ignored unless it is allowed by "aqua policy allow" command.

$ aqua policy allow "/home/runner/work/arch/arch/aqua/aqua-policy.yaml"

If you want to keep ignoring the policy file without the warning, please run "aqua policy deny" command.

$ aqua policy deny "/home/runner/work/arch/arch/aqua/aqua-policy.yaml"

  program=aqua version=2.62.0 env=linux/amd64 exe_name=terraform package_name=hashicorp/terraform package_version=v1.15.8 registry=standard policy_file=/home/runner/work/arch/arch/aqua/aqua-policy.yaml doc=https://aquaproj.github.io/docs/reference/codes/003
╷
│ Error: Unsupported block type
│ 
│   on worker.tf line 17, in resource "cloudflare_workers_script" "lolice_member_portal":
│   17:   durable_object_namespace_binding {
│ 
│ Blocks of type "durable_object_namespace_binding" are not expected here.
╵
╷
│ Error: Unsupported block type
│ 
│   on worker.tf line 57, in resource "cloudflare_workers_script" "lolice_member_portal":
│   57:   migrations {
│ 
│ Blocks of type "migrations" are not expected here.
╵

…pdates

Cloudflare provider v4.52 does not support durable_object_namespace_binding
or migrations blocks in cloudflare_workers_script, causing terraform validate
to fail. Replace the DO-based serialization with an addEmailToAccessPolicy
function that performs a read-modify-write loop and verifies the email was
persisted after each PUT. If a concurrent PUT overwrote the write, the
verify step detects the missing entry and retries with backoff (up to 5
attempts), ensuring eventual consistency without requiring Durable Objects.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@boxp-tfaction

This comment has been minimized.

Replace read-modify-write retry loop with PolicyUpdater Durable Object
so concurrent approvals are queued and executed serially

Replace non-atomic KV rate limiting with atomic DO storage counters

Remove default="" from cf_api_token/resend_api_key to prevent
deploying with blank secrets
@boxp-tfaction

boxp-tfaction Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

❌ terraform/cloudflare/b0xp.io/lolice-member-portal: terraform validate

Build link

terraform validate
Details
Jul 18 01:48:14.634 WRN The policy file is ignored unless it is allowed by "aqua policy allow" command.

$ aqua policy allow "/home/runner/work/arch/arch/aqua/aqua-policy.yaml"

If you want to keep ignoring the policy file without the warning, please run "aqua policy deny" command.

$ aqua policy deny "/home/runner/work/arch/arch/aqua/aqua-policy.yaml"

  program=aqua version=2.62.0 env=linux/amd64 exe_name=terraform package_name=hashicorp/terraform package_version=v1.15.8 registry=standard policy_file=/home/runner/work/arch/arch/aqua/aqua-policy.yaml doc=https://aquaproj.github.io/docs/reference/codes/003
╷
│ Error: Unsupported block type
│ 
│   on worker.tf line 52, in resource "cloudflare_workers_script" "lolice_member_portal":
│   52:   durable_object_namespace_binding {
│ 
│ Blocks of type "durable_object_namespace_binding" are not expected here.
╵
╷
│ Error: Unsupported block type
│ 
│   on worker.tf line 57, in resource "cloudflare_workers_script" "lolice_member_portal":
│   57:   migrations {
│ 
│ Blocks of type "migrations" are not expected here.
╵

DO storage does not support expirationTtl option (unlike KV).
Store {count, windowStart} record and reset when window expires.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@boxp-tfaction

boxp-tfaction Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

❌ terraform/cloudflare/b0xp.io/lolice-member-portal: terraform validate

Build link

terraform validate
Details
Jul 18 02:01:57.921 WRN The policy file is ignored unless it is allowed by "aqua policy allow" command.

$ aqua policy allow "/home/runner/work/arch/arch/aqua/aqua-policy.yaml"

If you want to keep ignoring the policy file without the warning, please run "aqua policy deny" command.

$ aqua policy deny "/home/runner/work/arch/arch/aqua/aqua-policy.yaml"

  program=aqua version=2.62.0 env=linux/amd64 exe_name=terraform package_name=hashicorp/terraform package_version=v1.15.8 registry=standard policy_file=/home/runner/work/arch/arch/aqua/aqua-policy.yaml doc=https://aquaproj.github.io/docs/reference/codes/003
╷
│ Error: Unsupported block type
│ 
│   on worker.tf line 52, in resource "cloudflare_workers_script" "lolice_member_portal":
│   52:   durable_object_namespace_binding {
│ 
│ Blocks of type "durable_object_namespace_binding" are not expected here.
╵
╷
│ Error: Unsupported block type
│ 
│   on worker.tf line 57, in resource "cloudflare_workers_script" "lolice_member_portal":
│   57:   migrations {
│ 
│ Blocks of type "migrations" are not expected here.
╵

Cloudflare TF provider v4.52.8 does not support durable_object_namespace_binding
or migrations blocks in cloudflare_workers_script, causing CI failures.

Replace the DO approach with a KV-sourced strategy:
- Add APPROVED_EMAILS KV namespace as durable source of truth for approvals
- On each policy update: write email to KV first, then merge ALL KV emails
  into the policy (deduplicating with existing include rules)
- Verify email appears after PUT; retry with fresh GET if absent (handles
  the rare case where a concurrent PUT overwrote our addition)
- Rate limiting uses PENDING_REQUESTS KV with expirationTtl

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@boxp-tfaction

This comment has been minimized.

Replace secret_text_binding (which required Terraform variables for secret
values) with separate cloudflare_workers_secret resources. Secrets are
initialized with a dummy placeholder and lifecycle.ignore_changes prevents
Terraform from overwriting values set manually via Cloudflare Dashboard.

This eliminates the need for cf_api_token and resend_api_key variables,
so terraform plan in CI succeeds without secret values.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@boxp-tfaction

This comment has been minimized.

@boxp-tfaction

boxp-tfaction Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

❌ terraform/cloudflare/b0xp.io/lolice-member-portal: terraform validate

Build link

terraform validate
Details
Jul 18 02:29:44.420 WRN The policy file is ignored unless it is allowed by "aqua policy allow" command.

$ aqua policy allow "/home/runner/work/arch/arch/aqua/aqua-policy.yaml"

If you want to keep ignoring the policy file without the warning, please run "aqua policy deny" command.

$ aqua policy deny "/home/runner/work/arch/arch/aqua/aqua-policy.yaml"

  program=aqua version=2.62.0 env=linux/amd64 exe_name=terraform package_name=hashicorp/terraform package_version=v1.15.8 registry=standard policy_file=/home/runner/work/arch/arch/aqua/aqua-policy.yaml doc=https://aquaproj.github.io/docs/reference/codes/003
╷
│ Error: Unsupported block type
│ 
│   on worker.tf line 17, in resource "cloudflare_workers_script" "lolice_member_portal":
│   17:   durable_object_namespace_binding {
│ 
│ Blocks of type "durable_object_namespace_binding" are not expected here.
╵

…ET verification

- Remove PolicyUpdater Durable Object (unsupported in cloudflare provider v4.52)
- Restore APPROVED_EMAILS KV namespace as durable source of truth
- Rewrite addEmailToAccessPolicy to merge ALL KV emails on every PUT attempt
- Verify with a separate fresh GET (not PUT response) that ALL KV emails exist
- Retry up to 5 times with exponential backoff on concurrent-overwrite detection
@boxp-tfaction

This comment has been minimized.

addEmailToAccessPolicy でKV list()の1ページ目(最大1,000件)しか
処理していなかった問題を修正。list_complete/cursorによるページネーション
ループで全メールアドレスを取得するように変更。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@boxp-tfaction

This comment has been minimized.

…rrent approvals

The verification step was checking kvEmails from the start of the loop
iteration. If a concurrent approval was persisted to KV after that
snapshot was taken, our PUT could overwrite their addition and the
stale-snapshot check would still pass (missing their email).

Fix: re-scan APPROVED_EMAILS KV in step 6 to get the current complete
set, then verify the live policy contains all of them. If any are
absent, the loop retries the full read-modify-write cycle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@boxp-tfaction

This comment has been minimized.

boxp and others added 3 commits July 18, 2026 02:53
…and SSM for secrets

- Replace APPROVED_EMAILS KV namespace with Cloudflare D1 database so that the
  retry+verify loop in addEmailToAccessPolicy always sees a consistent view of
  approved emails. KV's eventual consistency meant concurrent approvals could
  miss each other's writes even during the verify re-read; D1 (SQLite, ACID)
  eliminates this gap.
- Remove dummy secret values and lifecycle ignore_changes from cloudflare_workers_secret.
  Secrets are now read from AWS SSM Parameter Store via data sources, ensuring
  terraform apply always deploys the real credentials and no dummy placeholder
  can be accidentally left in place.
- Add AWS provider (hashicorp/aws ~> 5.0) to backend.tf and provider.tf.
- SSM prerequisites: aws ssm put-parameter --name /lolice-member-portal/CF_API_TOKEN
  and /lolice-member-portal/RESEND_API_KEY must be created before first apply.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sion constraint

- Bump aws provider constraint from ~> 5.0 to ~> 6.0 to match other directories in this repo
- Add hashicorp/aws 6.54.0 entry to .terraform.lock.hcl (copied from bastion which uses same version)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@boxp-tfaction

boxp-tfaction Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Build link

terraform init -input=false -upgrade
Details
Jul 18 02:56:07.190 WRN The policy file is ignored unless it is allowed by "aqua policy allow" command.

$ aqua policy allow "/home/runner/work/arch/arch/aqua/aqua-policy.yaml"

If you want to keep ignoring the policy file without the warning, please run "aqua policy deny" command.

$ aqua policy deny "/home/runner/work/arch/arch/aqua/aqua-policy.yaml"

  program=aqua version=2.62.0 env=linux/amd64 exe_name=terraform package_name=hashicorp/terraform package_version=v1.15.8 registry=standard policy_file=/home/runner/work/arch/arch/aqua/aqua-policy.yaml doc=https://aquaproj.github.io/docs/reference/codes/003
╷
│ Error: Duplicate provider lock
│ 
│   on .terraform.lock.hcl line 75:
│   (source code not available)
│ 
│ This lockfile already declared a lock for provider
│ registry.terraform.io/hashicorp/aws at .terraform.lock.hcl:4,1-47.
╵

Remove the manually-added 6.54.0 entry and keep only the CI-generated 5.100.0
entry from update-aqua-checksums. Align backend.tf constraint to ~> 5.0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@boxp-tfaction

boxp-tfaction Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

❌ Plan Failed (terraform/cloudflare/b0xp.io/lolice-member-portal)

CI link

Error: reading SSM Parameter (/lolice-member-portal/CF_API_TOKEN): couldn't find resource

  with data.aws_ssm_parameter.cf_api_token,
  on worker.tf line 21, in data "aws_ssm_parameter" "cf_api_token":
  21: data "aws_ssm_parameter" "cf_api_token" {


Error: reading SSM Parameter (/lolice-member-portal/RESEND_API_KEY): couldn't find resource

  with data.aws_ssm_parameter.resend_api_key,
  on worker.tf line 26, in data "aws_ssm_parameter" "resend_api_key":
  26: data "aws_ssm_parameter" "resend_api_key" {
  • Create
    • cloudflare_d1_database.approved_emails
    • cloudflare_record.lolice_member_portal
    • cloudflare_worker_route.lolice_member_portal
    • cloudflare_workers_kv_namespace.pending_requests
    • cloudflare_workers_script.lolice_member_portal

⚠️ Warnings

Warning: Deprecated Resource

  with cloudflare_worker_route.lolice_member_portal,
  on worker.tf line 87, in resource "cloudflare_worker_route" "lolice_member_portal":
  87: resource "cloudflare_worker_route" "lolice_member_portal" {

`cloudflare_worker_route` is now deprecated and will be removed in the next
major version. Use `cloudflare_workers_route` instead.

Error: reading SSM Parameter (/lolice-member-portal/CF_API_TOKEN): couldn't find resource

  with data.aws_ssm_parameter.cf_api_token,
  on worker.tf line 21, in data "aws_ssm_parameter" "cf_api_token":
  21: data "aws_ssm_parameter" "cf_api_token" {


Error: reading SSM Parameter (/lolice-member-portal/RESEND_API_KEY): couldn't find resource

  with data.aws_ssm_parameter.resend_api_key,
  on worker.tf line 26, in data "aws_ssm_parameter" "resend_api_key":
  26: data "aws_ssm_parameter" "resend_api_key" {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant