From b6bcd26c6f21ea7417beb0cebd48ba7d7b12705a Mon Sep 17 00:00:00 2001 From: Winrey Date: Mon, 1 Jun 2026 15:21:18 +0800 Subject: [PATCH 01/13] docs: add team9 dev aws migration design --- ...-team9-dev-aws-account-migration-design.md | 322 ++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-01-team9-dev-aws-account-migration-design.md diff --git a/docs/superpowers/specs/2026-06-01-team9-dev-aws-account-migration-design.md b/docs/superpowers/specs/2026-06-01-team9-dev-aws-account-migration-design.md new file mode 100644 index 00000000..1e43d42f --- /dev/null +++ b/docs/superpowers/specs/2026-06-01-team9-dev-aws-account-migration-design.md @@ -0,0 +1,322 @@ +# Team9 Dev AWS Account Migration Design + +Date: 2026-06-01 + +## Summary + +Migrate the AWS-backed dev dependency environment from the `ww` AWS account to +the new `t9` AWS account. Team9 itself continues to run on Railway during this +phase, but AWS resources that Team9 depends on are rebuilt or copied into `t9`. + +The migration preserves dev data where it matters: PostgreSQL, EFS, and S3 are +copied. Redis is recreated empty. A short dev downtime / write-freeze window is +accepted and is part of the consistency plan. + +## Accounts and Region + +- Source account: `ww` / `471112576951` +- Target account: `t9` / `149614785083` +- Region: `us-east-1` + +## Scope + +In scope: + +- aHand Hub dev AWS resources. +- folder9 dev AWS resources. +- openclaw-hive dev AWS resources. +- Team9 dev S3 file data, even though the Team9 runtime remains on Railway. +- Capability Hub S3 buckets in `t9`, split by `dev` and `prod`. +- Terraform state infrastructure for the new account. +- GitHub deployment trust should move toward OIDC roles in `t9`. + +Out of scope for this phase: + +- Moving the Team9 Railway runtime to AWS. +- Deleting the existing `ww` dev resources immediately after cutover. +- Migrating Redis runtime data. +- Reworking application behavior outside what is needed for AWS account + migration. + +## Existing Source State + +Known `ww` dev resources: + +- ECS clusters: + - `openclaw-hive-dev` + - `folder9-dev` +- ECS services: + - `ahand-hub-dev` + - `control-plane-dev` + - `file-keeper-dev` + - `traefik-dev` + - `folder9-dev` + - `folder9-dashboard-dev` + - `folder9-traefik-dev` +- RDS: + - `openclaw-hive-dev`, PostgreSQL 16.10, `db.t4g.micro`, 20 GiB gp3, + Single-AZ, publicly accessible, unencrypted, 7 day backup retention. +- ElastiCache: + - `ahand-hub-dev`, Redis, `cache.t4g.micro`, single node. +- EFS: + - `folder9-dev` around 377 MB. + - `folder9-dev-acme` around 59 KB. + - `openclaw-hive-dev` around 33 MB. + - `openclaw-hive-dev-efs` around 1.8 MB. +- S3: + - `ahand-hub-dev`, around 19 KB / 2 objects. + - `t9-development`, around 118 MB / 137 objects. + - `capability-hub`, around 653 MB / 432 objects. +- ECR repositories: + - `control-plane` + - `file-keeper` + - `ahand-hub` + - `openclaw-hive` + - `folder9` + - `folder9-dashboard` + +Known `t9` state: + +- Default VPC exists, but no target ECS/RDS/ECR/S3 baseline is present. +- No existing S3 bucket names from the target naming plan were found during + read-only `head-bucket` checks. + +## Naming Decisions + +S3 buckets use: + +```text +team9-- +``` + +Approved target bucket names: + +| Purpose | Dev bucket | Prod bucket | +| ------------------------------ | -------------------------- | -------------------------------------- | +| Team9 uploaded files | `team9-files-dev` | `team9-files-prod` | +| aHand Hub file operations | `team9-ahand-hub-dev` | `team9-ahand-hub-prod` | +| Capability Hub generated files | `team9-capability-hub-dev` | `team9-capability-hub-prod` | +| Terraform state | `team9-tfstate` | Shared bucket, separated by state keys | + +S3 data mappings: + +| Source | Target | +| ------------------- | -------------------------------------------- | +| `ww:ahand-hub-dev` | `t9:team9-ahand-hub-dev` | +| `ww:t9-development` | `t9:team9-files-dev` | +| `ww:capability-hub` | `t9:team9-capability-hub-prod` | +| none | `t9:team9-capability-hub-dev`, created empty | + +## S3 Access Model + +`team9-files-*`: + +- Private buckets. +- Access through service credentials and presigned upload/download URLs. +- The initial AWS migration copies data but does not force Railway Team9 to + switch immediately. +- A later Team9 S3 cutover must either update database references from the old + bucket name / URL to the new bucket name / URL, or keep old bucket access + available until no stored references use it. + +`team9-ahand-hub-*`: + +- Private buckets. +- Access limited to aHand Hub service roles and migration operators. +- Preserve the current dev lifecycle rule behavior for file operation objects. + +`team9-capability-hub-*`: + +- Public-readable objects, service-only writes. +- This matches the current Capability Hub usage where generated files can be + returned to Team9 and stored as durable `fileUrl` values. +- Public write is never allowed. +- CloudFront can be added later without changing bucket names. + +## Target Infrastructure Design + +Create a dedicated VPC in `t9`; do not use the default VPC for migrated dev +services. The target VPC should have at least two Availability Zones, public +subnets for load balancers, and private subnets for ECS tasks and data stores +where practical. + +Rebuild the following target resources in `t9`: + +- Terraform state: + - `team9-tfstate` S3 bucket. + - DynamoDB lock table. +- IAM: + - GitHub OIDC provider. + - Repo-scoped deploy roles for aHand, folder9, and openclaw-hive. + - ECS task execution and task roles. + - Least-privilege S3 policies per service bucket. +- Networking: + - Dedicated VPC, subnets, route tables, security groups. + - NLBs for the dev endpoints that are currently fronted by NLB / Traefik. +- Compute: + - ECS clusters and services for aHand Hub, folder9, folder9 dashboard, + openclaw-hive control plane, file keeper, and Traefik services. +- Data: + - RDS PostgreSQL restored from the final `openclaw-hive-dev` snapshot. + - ElastiCache Redis recreated empty. + - EFS file systems recreated and populated from source EFS. + - S3 buckets created with the approved names and policies. +- Registry: + - ECR repositories copied or recreated in `t9`. + +## Data Migration Plan + +Use a write-freeze window for final consistency. + +Pre-copy phase: + +1. Create target S3 buckets and baseline policies. +2. Run full S3 copies from `ww` to `t9`. +3. Create target EFS file systems. +4. Run full EFS copies from `ww` to `t9`. +5. Prepare target RDS subnet groups, security groups, and parameter settings. +6. Prepare target ECS task definitions and service configuration. + +Write-freeze phase: + +1. Stop writes to source dev services. The simplest acceptable method is to + scale down or stop the relevant `ww` dev ECS services and prevent new write + traffic during the window. +2. Take the final `openclaw-hive-dev` RDS snapshot. +3. Restore the final snapshot into `t9`. +4. Run final incremental S3 syncs. +5. Run final incremental EFS syncs. +6. Update target SSM parameters and service env references to target resource + names and endpoints. +7. Start target ECS services. +8. Cut DNS records for dev service hostnames to the target `t9` load balancers. + +Post-cutover phase: + +1. Run smoke tests against target dev domains. +2. Validate app-level flows from Railway Team9 into the new AWS-backed services. +3. Keep `ww` dev resources available for rollback, but do not allow normal + writes there after a successful cutover. +4. Delete or archive `ww` dev only after a separate confirmation. + +## Configuration and Secrets + +Secrets must be copied without printing values to logs or terminal output. + +Target SSM parameters should preserve the existing logical names where possible +inside the target account, but values must point at target resources: + +- aHand Hub: + - `DATABASE_URL` + - `REDIS_URL` + - `S3_BUCKET=team9-ahand-hub-dev` + - `S3_REGION=us-east-1` + - service tokens and webhook secrets. +- folder9: + - `DATABASE_URL` + - `PSK` + - webhook and Sentry settings. +- folder9 dashboard: + - `DATABASE_URL` + - `FOLDER9_PSK` + - `JWT_SECRET` + - dashboard password. +- openclaw-hive: + - task-definition and GitHub deployment settings should point at target + account ECR repositories, target cluster names, target EFS IDs, and target + service endpoints. +- Capability Hub: + - `S3_BUCKET=team9-capability-hub-dev` for dev. + - `S3_BUCKET=team9-capability-hub-prod` for prod. + - `S3_PUBLIC=true` when durable public object URLs are required. + +GitHub Actions should use OIDC roles in the `t9` account. Static AWS access keys +should be removed from the deploy path after replacement is verified. + +## DNS and External Runtime Handling + +The dev service domains should stay stable from the caller perspective. DNS +records are repointed to `t9` load balancers during cutover. + +Team9 on Railway is not migrated in this phase. If a Railway env var points to a +stable dev domain, no Railway change is required for that dependency after DNS +cutover. If a Railway env var points directly to an AWS endpoint, bucket name, or +account-scoped resource, it must be changed in an explicit Railway cutover step. + +Team9 file bucket cutover is separate from copying `t9-development` data: + +- Copy now to `team9-files-dev`. +- Keep current Railway file configuration unchanged unless a Team9 S3 cutover is + explicitly scheduled. +- When scheduled, update Team9 env vars and database references consistently. + +## Validation + +Before cutover: + +- Confirm all target S3 bucket names still return `404` or are owned by `t9`. +- Confirm target Terraform state bucket and lock table are accessible. +- Confirm ECR images exist in `t9`. +- Confirm SSM parameters exist in `t9` with expected names and no empty required + values. +- Confirm target RDS restore has expected databases and users. +- Confirm EFS mounts are readable by target tasks. + +During cutover: + +- Confirm source dev services are stopped or write-blocked. +- Confirm final RDS snapshot restore completes. +- Confirm final S3 and EFS syncs report no unexpected failures. +- Confirm DNS records resolve to target load balancers. + +After cutover: + +- aHand Hub health endpoint responds. +- folder9 API and dashboard health checks respond. +- openclaw-hive control plane can create/list/start/stop dev instances. +- file keeper paths resolve and existing copied EFS data is visible. +- Team9 Railway can call the dev control plane through the existing configured + base URL. +- S3 read/write checks pass for service roles: + - Team9 files bucket private access. + - aHand Hub private bucket access. + - Capability Hub public read and service-only write. + +## Rollback + +Rollback is DNS/config based: + +1. Stop or scale down target `t9` dev services. +2. Repoint dev DNS records back to `ww` load balancers. +3. Restart source `ww` dev services if they were stopped. +4. Leave copied `t9` data intact for diagnosis. + +Rollback is simplest before new writes happen in `t9`. If writes happen in `t9` +after cutover, choose whether to discard them or perform a reverse data sync +before returning traffic to `ww`. + +## Risks and Mitigations + +- S3 bucket names are global. + - Use the approved new names and re-check immediately before creation. +- Stored file URLs can point at old buckets. + - Copy data now, but schedule Team9 file bucket cutover separately with DB URL + and bucket-reference handling. +- openclaw-hive deployment is less Terraformized than aHand/folder9. + - Standardize target deploy roles, account IDs, ECR references, task + definitions, SSM parameters, and EFS IDs as part of the migration plan. +- Secrets can leak during manual migration. + - Copy secrets through AWS CLI / SSM APIs without printing values. +- RDS source is currently unencrypted. + - Restore target with encryption enabled if practical; document any limitation + in the implementation plan. +- Capability Hub public objects need careful write protection. + - Public read policy must be object-read only; write/delete/list remain limited + to service and operator roles. + +## Deferred Work + +- Move Team9 runtime from Railway to AWS. +- Add CloudFront in front of public object buckets. +- Fully Terraformize openclaw-hive beyond what is required for this migration. +- Delete legacy `ww` dev resources after a separate cleanup approval. From c43360e10a4b463335c6da1e5e5a2ce97a544f65 Mon Sep 17 00:00:00 2001 From: Winrey Date: Mon, 1 Jun 2026 18:22:50 +0800 Subject: [PATCH 02/13] docs: add team9 dev aws migration plan --- ...6-06-01-team9-dev-aws-account-migration.md | 1731 +++++++++++++++++ 1 file changed, 1731 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-01-team9-dev-aws-account-migration.md diff --git a/docs/superpowers/plans/2026-06-01-team9-dev-aws-account-migration.md b/docs/superpowers/plans/2026-06-01-team9-dev-aws-account-migration.md new file mode 100644 index 00000000..86db0c33 --- /dev/null +++ b/docs/superpowers/plans/2026-06-01-team9-dev-aws-account-migration.md @@ -0,0 +1,1731 @@ +# Team9 Dev AWS Account Migration Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Move the AWS-backed Team9 dev dependency environment from the `ww` AWS account to the `t9` AWS account while preserving RDS, EFS, and S3 data. + +**Architecture:** Use a staged migration: bootstrap shared `t9` resources first, migrate IaC and deploy trust in each service repo, pre-copy mutable data, then use an accepted dev write-freeze window for final RDS/EFS/S3 sync and DNS cutover. Railway Team9 stays in place; its AWS dependency endpoints continue to use stable dev domains wherever possible. + +**Tech Stack:** AWS CLI, Terraform, S3, DynamoDB, VPC, ECS/Fargate, ECR, RDS PostgreSQL, ElastiCache Redis, EFS, SSM Parameter Store, GitHub Actions OIDC, Cloudflare DNS, pnpm/Prettier for docs. + +--- + +## Source Documents + +- Spec: `/Users/winrey/Projects/weightwave/team9/docs/superpowers/specs/2026-06-01-team9-dev-aws-account-migration-design.md` +- Source AWS profile: `ww` +- Target AWS profile: `t9` +- Source AWS account: `471112576951` +- Target AWS account: `149614785083` +- Region: `us-east-1` + +## Scope Split + +The spec spans several independently deployable repos. Keep the top-level migration in this Team9 plan, but commit implementation changes in the repo that owns each deploy surface: + +- `/Users/winrey/Projects/weightwave/team9`: migration docs and target-account bootstrap Terraform. +- `/Users/winrey/Projects/weightwave/aHand`: aHand Hub Terraform and GitHub Actions account migration. +- `/Users/winrey/Projects/weightwave/folder9`: folder9 Terraform and GitHub Actions account migration. +- `/Users/winrey/Projects/weightwave/openclaw-hive`: dev deployment roles, ECR references, task definitions, and EFS/RDS/Redis target wiring. +- `/Users/winrey/Projects/weightwave/capability-hub`: S3 env documentation and smoke verification only unless runtime deployment wiring is found during execution. + +Do not apply Terraform, copy data, change DNS, or stop services until the plan step explicitly says to do so. + +## File Map + +Create in Team9: + +- `/Users/winrey/Projects/weightwave/team9/infra/aws/t9-bootstrap/versions.tf`: Terraform version and providers. +- `/Users/winrey/Projects/weightwave/team9/infra/aws/t9-bootstrap/providers.tf`: AWS provider using profile `t9`. +- `/Users/winrey/Projects/weightwave/team9/infra/aws/t9-bootstrap/main.tf`: state bucket, lock table, migration S3 buckets, ECR repos. +- `/Users/winrey/Projects/weightwave/team9/infra/aws/t9-bootstrap/outputs.tf`: bucket and ECR outputs. +- `/Users/winrey/Projects/weightwave/team9/infra/aws/t9-dev-core/versions.tf`: Terraform version and providers for dev core. +- `/Users/winrey/Projects/weightwave/team9/infra/aws/t9-dev-core/backend.tf`: remote state in `team9-tfstate`. +- `/Users/winrey/Projects/weightwave/team9/infra/aws/t9-dev-core/providers.tf`: AWS provider using profile `t9`. +- `/Users/winrey/Projects/weightwave/team9/infra/aws/t9-dev-core/main.tf`: dedicated dev VPC, subnets, openclaw ECS cluster, RDS subnet group and security group, openclaw EFS. +- `/Users/winrey/Projects/weightwave/team9/infra/aws/t9-dev-core/outputs.tf`: values consumed by aHand, folder9, and openclaw-hive migration steps. +- `/Users/winrey/Projects/weightwave/team9/docs/aws/team9-dev-aws-migration-runbook.md`: operator runbook for data copy, write-freeze, DNS cutover, rollback. + +Modify in aHand: + +- `/Users/winrey/Projects/weightwave/aHand/infra/shared/backend.tf` +- `/Users/winrey/Projects/weightwave/aHand/infra/shared/providers.tf` +- `/Users/winrey/Projects/weightwave/aHand/infra/shared/variables.tf` +- `/Users/winrey/Projects/weightwave/aHand/infra/envs/dev/backend.tf` +- `/Users/winrey/Projects/weightwave/aHand/infra/envs/dev/providers.tf` +- `/Users/winrey/Projects/weightwave/aHand/infra/modules/ahand-hub/variables.tf` +- `/Users/winrey/Projects/weightwave/aHand/infra/modules/ahand-hub/ssm.tf` +- `/Users/winrey/Projects/weightwave/aHand/infra/modules/ahand-hub/iam.tf` +- `/Users/winrey/Projects/weightwave/aHand/.github/workflows/deploy-hub.yml` +- `/Users/winrey/Projects/weightwave/aHand/deploy/hub/deploy.sh` + +Modify in folder9: + +- `/Users/winrey/Projects/weightwave/folder9/infra/shared/backend.tf` +- `/Users/winrey/Projects/weightwave/folder9/infra/shared/providers.tf` +- `/Users/winrey/Projects/weightwave/folder9/infra/shared/terraform.tfvars` +- `/Users/winrey/Projects/weightwave/folder9/infra/envs/dev/backend.tf` +- `/Users/winrey/Projects/weightwave/folder9/infra/envs/dev/providers.tf` +- `/Users/winrey/Projects/weightwave/folder9/infra/envs/dev/main.tf` +- `/Users/winrey/Projects/weightwave/folder9/infra/envs/dev/terraform.tfvars` +- `/Users/winrey/Projects/weightwave/folder9/.github/workflows/deploy.yml` +- `/Users/winrey/Projects/weightwave/folder9/.github/workflows/deploy-dashboard.yml` +- `/Users/winrey/Projects/weightwave/folder9/deploy.sh` +- `/Users/winrey/Projects/weightwave/folder9/dashboard/deploy.sh` + +Modify in openclaw-hive: + +- `/Users/winrey/Projects/weightwave/openclaw-hive/.github/workflows/control-plane-dev.yml` +- `/Users/winrey/Projects/weightwave/openclaw-hive/.github/workflows/file-keeper-dev.yml` +- `/Users/winrey/Projects/weightwave/openclaw-hive/.github/workflows/aws-images-dev.yml` +- `/Users/winrey/Projects/weightwave/openclaw-hive/.github/workflows/efs-webdav-dev.yml` +- `/Users/winrey/Projects/weightwave/openclaw-hive/control-plane/task-definition.dev.template.json` +- `/Users/winrey/Projects/weightwave/openclaw-hive/deploy/traefik/deploy-dev.sh` +- `/Users/winrey/Projects/weightwave/openclaw-hive/deploy/traefik/deploy.sh` +- `/Users/winrey/Projects/weightwave/openclaw-hive/Makefile` + +Reference in capability-hub: + +- `/Users/winrey/Projects/weightwave/capability-hub/src/config/config.schema.ts` +- `/Users/winrey/Projects/weightwave/capability-hub/src/file/file.service.ts` +- `/Users/winrey/Projects/weightwave/capability-hub/src/file/s3-client.provider.ts` + +## Task 1: Preflight Snapshot and Branches + +**Files:** + +- No file changes. + +- [ ] **Step 1: Verify all repos are clean before editing** + +Run: + +```bash +for repo in team9 aHand folder9 openclaw-hive capability-hub; do + echo "== $repo ==" + git -C "/Users/winrey/Projects/weightwave/$repo" status --short + git -C "/Users/winrey/Projects/weightwave/$repo" branch --show-current +done +``` + +Expected: each repo prints its short status followed by its current branch. `team9` should start on `dev`; the other repos may already be on local work branches. + +If any repo has unrelated dirty files, leave those files untouched and record them in the runbook before editing. + +- [ ] **Step 2: Create implementation branches** + +Run: + +```bash +git -C /Users/winrey/Projects/weightwave/team9 switch -c codex/team9-dev-aws-migration-plan-execution +git -C /Users/winrey/Projects/weightwave/aHand switch -c codex/team9-dev-aws-migration-ahand +git -C /Users/winrey/Projects/weightwave/folder9 switch -c codex/team9-dev-aws-migration-folder9 +git -C /Users/winrey/Projects/weightwave/openclaw-hive switch -c codex/team9-dev-aws-migration-openclaw +``` + +Expected: each command prints `Switched to a new branch`. + +- [ ] **Step 3: Confirm AWS identities** + +Run: + +```bash +aws sts get-caller-identity --profile ww --query '{Account:Account,Arn:Arn}' --output table +aws sts get-caller-identity --profile t9 --query '{Account:Account,Arn:Arn}' --output table +``` + +Expected: + +```text +ww Account: 471112576951 +t9 Account: 149614785083 +``` + +Do not continue if either account differs. + +## Task 2: Create Target Account Bootstrap Terraform + +**Files:** + +- Create: `/Users/winrey/Projects/weightwave/team9/infra/aws/t9-bootstrap/versions.tf` +- Create: `/Users/winrey/Projects/weightwave/team9/infra/aws/t9-bootstrap/providers.tf` +- Create: `/Users/winrey/Projects/weightwave/team9/infra/aws/t9-bootstrap/main.tf` +- Create: `/Users/winrey/Projects/weightwave/team9/infra/aws/t9-bootstrap/outputs.tf` + +- [ ] **Step 1: Create bootstrap directory** + +Run: + +```bash +mkdir -p /Users/winrey/Projects/weightwave/team9/infra/aws/t9-bootstrap +``` + +Expected: command exits with status 0. + +- [ ] **Step 2: Add Terraform versions file** + +Write `/Users/winrey/Projects/weightwave/team9/infra/aws/t9-bootstrap/versions.tf`: + +```hcl +terraform { + required_version = ">= 1.6.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} +``` + +- [ ] **Step 3: Add provider file** + +Write `/Users/winrey/Projects/weightwave/team9/infra/aws/t9-bootstrap/providers.tf`: + +```hcl +provider "aws" { + region = "us-east-1" + profile = "t9" + + default_tags { + tags = { + Project = "team9" + ManagedBy = "Terraform" + Account = "t9" + } + } +} +``` + +- [ ] **Step 4: Add bootstrap resources** + +Write `/Users/winrey/Projects/weightwave/team9/infra/aws/t9-bootstrap/main.tf`: + +```hcl +locals { + s3_buckets = { + team9_files_dev = { name = "team9-files-dev", public_read = false } + team9_files_prod = { name = "team9-files-prod", public_read = false } + ahand_hub_dev = { name = "team9-ahand-hub-dev", public_read = false } + ahand_hub_prod = { name = "team9-ahand-hub-prod", public_read = false } + capability_hub_dev = { name = "team9-capability-hub-dev", public_read = true } + capability_hub_prod = { name = "team9-capability-hub-prod", public_read = true } + } + + # aHand's own shared Terraform creates the ahand-hub ECR repository. + # folder9 dev looks up its ECR repositories as pre-existing resources, so + # bootstrap creates those two repos before the dev stack runs. + ecr_repositories = toset([ + "control-plane", + "file-keeper", + "efs-webdav", + "openclaw-hive", + "folder9", + "folder9-dashboard", + ]) +} + +resource "aws_s3_bucket" "tfstate" { + bucket = "team9-tfstate" +} + +resource "aws_s3_bucket_versioning" "tfstate" { + bucket = aws_s3_bucket.tfstate.id + + versioning_configuration { + status = "Enabled" + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "tfstate" { + bucket = aws_s3_bucket.tfstate.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +resource "aws_s3_bucket_public_access_block" "tfstate" { + bucket = aws_s3_bucket.tfstate.id + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_dynamodb_table" "terraform_state_lock" { + name = "terraform-state-lock" + billing_mode = "PAY_PER_REQUEST" + hash_key = "LockID" + + attribute { + name = "LockID" + type = "S" + } +} + +resource "aws_s3_bucket" "service" { + for_each = local.s3_buckets + bucket = each.value.name +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "service" { + for_each = aws_s3_bucket.service + bucket = each.value.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +resource "aws_s3_bucket_public_access_block" "service_private" { + for_each = { + for key, cfg in local.s3_buckets : key => cfg + if cfg.public_read == false + } + + bucket = aws_s3_bucket.service[each.key].id + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_s3_bucket_public_access_block" "service_public_read" { + for_each = { + for key, cfg in local.s3_buckets : key => cfg + if cfg.public_read == true + } + + bucket = aws_s3_bucket.service[each.key].id + block_public_acls = true + block_public_policy = false + ignore_public_acls = true + restrict_public_buckets = false +} + +resource "aws_s3_bucket_policy" "capability_hub_public_read" { + for_each = { + for key, cfg in local.s3_buckets : key => cfg + if cfg.public_read == true + } + + bucket = aws_s3_bucket.service[each.key].id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "PublicReadObjects" + Effect = "Allow" + Principal = "*" + Action = "s3:GetObject" + Resource = "${aws_s3_bucket.service[each.key].arn}/*" + } + ] + }) +} + +resource "aws_s3_bucket_lifecycle_configuration" "team9_files" { + bucket = aws_s3_bucket.service["team9_files_dev"].id + + rule { + id = "auto-delete-pending-uploads" + status = "Enabled" + + filter { + tag { + key = "status" + value = "pending" + } + } + + expiration { + days = 1 + } + } +} + +resource "aws_s3_bucket_lifecycle_configuration" "ahand_hub_dev" { + bucket = aws_s3_bucket.service["ahand_hub_dev"].id + + rule { + id = "expire-file-ops-dev" + status = "Enabled" + + filter { + prefix = "file-ops/" + } + + expiration { + days = 7 + } + + abort_incomplete_multipart_upload { + days_after_initiation = 1 + } + } +} + +resource "aws_ecr_repository" "service" { + for_each = local.ecr_repositories + name = each.value + image_tag_mutability = "MUTABLE" + + image_scanning_configuration { + scan_on_push = true + } +} +``` + +- [ ] **Step 5: Add outputs** + +Write `/Users/winrey/Projects/weightwave/team9/infra/aws/t9-bootstrap/outputs.tf`: + +```hcl +output "tfstate_bucket" { + value = aws_s3_bucket.tfstate.bucket +} + +output "lock_table" { + value = aws_dynamodb_table.terraform_state_lock.name +} + +output "service_buckets" { + value = { + for key, bucket in aws_s3_bucket.service : key => bucket.bucket + } +} + +output "ecr_repository_urls" { + value = { + for name, repo in aws_ecr_repository.service : name => repo.repository_url + } +} +``` + +- [ ] **Step 6: Format and validate** + +Run: + +```bash +cd /Users/winrey/Projects/weightwave/team9/infra/aws/t9-bootstrap +terraform fmt -recursive +terraform init +terraform validate +terraform plan -out /tmp/t9-bootstrap.tfplan +``` + +Expected: + +```text +Success! The configuration is valid. +``` + +The plan output must contain only create actions for the bootstrap resources in this task, with `0 to change` and `0 to destroy`. + +- [ ] **Step 7: Review the plan before apply** + +Run: + +```bash +cd /Users/winrey/Projects/weightwave/team9/infra/aws/t9-bootstrap +terraform show -no-color /tmp/t9-bootstrap.tfplan | sed -n '1,240p' +``` + +Expected: only `team9-tfstate`, `terraform-state-lock`, approved `team9-*` S3 buckets, and listed ECR repositories are planned. + +- [ ] **Step 8: Commit bootstrap Terraform** + +Run: + +```bash +cd /Users/winrey/Projects/weightwave/team9 +git add infra/aws/t9-bootstrap +git commit -m "infra: add t9 aws bootstrap" +``` + +Expected: commit succeeds. + +## Task 3: Apply Target Bootstrap Resources + +**Files:** + +- No file changes. + +- [ ] **Step 1: Re-check bucket name availability** + +Run: + +```bash +for b in team9-tfstate team9-files-dev team9-files-prod team9-ahand-hub-dev team9-ahand-hub-prod team9-capability-hub-dev team9-capability-hub-prod; do + aws s3api head-bucket --profile t9 --bucket "$b" >/tmp/head-"$b".out 2>/tmp/head-"$b".err + code=$? + echo "$b $code $(tr '\n' ' ' /dev/null 2>&1; then + echo "Target RDS openclaw-hive-dev already exists in t9. Stop and decide whether to keep it or replace it." >&2 + exit 1 +fi + +aws rds restore-db-instance-from-db-snapshot \ + --profile t9 \ + --region us-east-1 \ + --db-instance-identifier openclaw-hive-dev \ + --db-snapshot-identifier "${SNAPSHOT_ID}-t9-encrypted" \ + --db-instance-class db.t4g.micro \ + --db-subnet-group-name "$DB_SUBNET_GROUP" \ + --vpc-security-group-ids "$RDS_SG_ID" \ + --publicly-accessible \ + --no-multi-az \ + --storage-type gp3 + +aws rds wait db-instance-available \ + --profile t9 \ + --region us-east-1 \ + --db-instance-identifier openclaw-hive-dev + +aws rds describe-db-instances \ + --profile t9 \ + --region us-east-1 \ + --db-instance-identifier openclaw-hive-dev \ + --query 'DBInstances[0].Endpoint.Address' \ + --output text +``` +```` + +- [ ] **Step 2: Add EFS copy procedure to runbook** + +Append: + +````markdown +## EFS Copy + +Copy these source file systems: + +- `fs-05f7f1c836631ddce` (`folder9-dev`) +- `fs-057baa1f60ff58b91` (`folder9-dev-acme`) +- `fs-0f3888df726d8d9f8` (`openclaw-hive-dev`) +- `fs-0920bb3d4db7aa843` (`openclaw-hive-dev-efs`) + +Use AWS DataSync when available. If DataSync is not already configured, run a temporary migration EC2 instance with both source and target EFS mounted over reachable networking. Copy with: + +```bash +sudo rsync -aHAX --numeric-ids --info=progress2 /mnt/source/ /mnt/target/ +sudo find /mnt/target -maxdepth 2 -type f | head -50 +``` + +Run the same `rsync` commands once before write-freeze and once during write-freeze. +```` + +- [ ] **Step 3: Execute RDS final snapshot only during write-freeze** + +Run the runbook commands under `RDS Final Snapshot and Restore`. + +Expected: + +```text +aws rds wait db-snapshot-completed exits 0 in both accounts. +aws rds wait db-instance-available exits 0 in t9. +The final command prints the target RDS endpoint hostname. +``` + +- [ ] **Step 4: Execute EFS final sync only during write-freeze** + +Run the runbook commands under `EFS Copy`. + +Expected: + +```text +rsync exits 0. +``` + +- [ ] **Step 5: Commit data migration runbook additions** + +Run: + +```bash +cd /Users/winrey/Projects/weightwave/team9 +git add docs/aws/team9-dev-aws-migration-runbook.md +git commit -m "docs: add rds and efs migration steps" +``` + +Expected: commit succeeds. + +## Task 10: Write-Freeze, Cutover, and Validation + +**Files:** + +- Modify: `/Users/winrey/Projects/weightwave/team9/docs/aws/team9-dev-aws-migration-runbook.md` + +- [ ] **Step 1: Add write-freeze commands** + +Append: + +````markdown +## Write-Freeze + +Scale source dev services down: + +```bash +aws ecs update-service --profile ww --region us-east-1 --cluster openclaw-hive-dev --service ahand-hub-dev --desired-count 0 +aws ecs update-service --profile ww --region us-east-1 --cluster openclaw-hive-dev --service control-plane-dev --desired-count 0 +aws ecs update-service --profile ww --region us-east-1 --cluster openclaw-hive-dev --service file-keeper-dev --desired-count 0 +aws ecs update-service --profile ww --region us-east-1 --cluster folder9-dev --service folder9-dev --desired-count 0 +aws ecs update-service --profile ww --region us-east-1 --cluster folder9-dev --service folder9-dashboard-dev --desired-count 0 +``` + +Do not scale down `traefik-dev` or `folder9-traefik-dev` until DNS cutover is ready; keeping them up allows fast rollback before target validation. +```` + +- [ ] **Step 2: Add target start commands** + +Append: + +````markdown +## Start Target Services + +After final data sync and target SSM values are verified: + +```bash +aws ecs update-service --profile t9 --region us-east-1 --cluster openclaw-hive-dev --service ahand-hub-dev --desired-count 1 +aws ecs update-service --profile t9 --region us-east-1 --cluster openclaw-hive-dev --service control-plane-dev --desired-count 1 +aws ecs update-service --profile t9 --region us-east-1 --cluster openclaw-hive-dev --service file-keeper-dev --desired-count 1 +aws ecs update-service --profile t9 --region us-east-1 --cluster folder9-dev --service folder9-dev --desired-count 2 +aws ecs update-service --profile t9 --region us-east-1 --cluster folder9-dev --service folder9-dashboard-dev --desired-count 1 +``` +```` + +- [ ] **Step 3: Add DNS validation commands** + +Append: + +````markdown +## DNS Cutover Checks + +After Cloudflare records point to `t9` NLB names: + +```bash +dig +short ahand-hub.dev.team9.ai +dig +short folder.dev.team9.ai +dig +short git.folder.dev.team9.ai +dig +short admin.folder.dev.team9.ai +dig +short plane.claw.dev.team9.ai +``` + +Each hostname must resolve to the new target load balancer chain. +```` + +- [ ] **Step 4: Add smoke test commands** + +Append: + +````markdown +## Smoke Tests + +Run: + +```bash +curl -fsS https://ahand-hub.dev.team9.ai/health || curl -fsS https://ahand-hub.dev.team9.ai/ +curl -fsS https://folder.dev.team9.ai/health || curl -fsS https://folder.dev.team9.ai/ +curl -fsS https://admin.folder.dev.team9.ai/ || true +curl -fsS https://plane.claw.dev.team9.ai/health || curl -fsS https://plane.claw.dev.team9.ai/ +``` + +Also verify from Railway Team9 by creating or listing a dev OpenClaw instance through the existing Team9 dev UI/API. +```` + +- [ ] **Step 5: Add rollback commands** + +Append: + +````markdown +## Rollback + +Before any intentional writes to `t9`, rollback is DNS and scale based: + +```bash +aws ecs update-service --profile t9 --region us-east-1 --cluster openclaw-hive-dev --service ahand-hub-dev --desired-count 0 +aws ecs update-service --profile t9 --region us-east-1 --cluster openclaw-hive-dev --service control-plane-dev --desired-count 0 +aws ecs update-service --profile t9 --region us-east-1 --cluster openclaw-hive-dev --service file-keeper-dev --desired-count 0 +aws ecs update-service --profile t9 --region us-east-1 --cluster folder9-dev --service folder9-dev --desired-count 0 +aws ecs update-service --profile t9 --region us-east-1 --cluster folder9-dev --service folder9-dashboard-dev --desired-count 0 + +aws ecs update-service --profile ww --region us-east-1 --cluster openclaw-hive-dev --service ahand-hub-dev --desired-count 1 +aws ecs update-service --profile ww --region us-east-1 --cluster openclaw-hive-dev --service control-plane-dev --desired-count 1 +aws ecs update-service --profile ww --region us-east-1 --cluster openclaw-hive-dev --service file-keeper-dev --desired-count 1 +aws ecs update-service --profile ww --region us-east-1 --cluster folder9-dev --service folder9-dev --desired-count 2 +aws ecs update-service --profile ww --region us-east-1 --cluster folder9-dev --service folder9-dashboard-dev --desired-count 1 +``` + +Then repoint Cloudflare records back to the old `ww` NLB targets. +```` + +- [ ] **Step 6: Commit cutover runbook additions** + +Run: + +```bash +cd /Users/winrey/Projects/weightwave/team9 +git add docs/aws/team9-dev-aws-migration-runbook.md +git commit -m "docs: add dev cutover and rollback steps" +``` + +Expected: commit succeeds. + +## Task 11: Final Verification + +**Files:** + +- No file changes. + +- [ ] **Step 1: Verify no source account IDs remain in active deploy files** + +Run: + +```bash +rg -n "471112576951|weightwave-tfstate|profile\\s*=\\s*\"ww\"" \ + /Users/winrey/Projects/weightwave/aHand/infra \ + /Users/winrey/Projects/weightwave/aHand/.github/workflows \ + /Users/winrey/Projects/weightwave/aHand/deploy \ + /Users/winrey/Projects/weightwave/folder9/infra \ + /Users/winrey/Projects/weightwave/folder9/.github/workflows \ + /Users/winrey/Projects/weightwave/folder9/deploy.sh \ + /Users/winrey/Projects/weightwave/folder9/dashboard/deploy.sh \ + /Users/winrey/Projects/weightwave/openclaw-hive/.github/workflows \ + /Users/winrey/Projects/weightwave/openclaw-hive/control-plane/task-definition.dev.template.json \ + /Users/winrey/Projects/weightwave/openclaw-hive/deploy/traefik \ + /Users/winrey/Projects/weightwave/openclaw-hive/Makefile +``` + +Expected: no matches in active deploy/config files. Matches in historical docs are acceptable only if outside the listed paths. + +- [ ] **Step 2: Verify target AWS inventory** + +Run: + +```bash +aws ecs list-clusters --profile t9 --region us-east-1 --output table +aws ecr describe-repositories --profile t9 --region us-east-1 --query 'repositories[].repositoryName' --output table +aws s3api list-buckets --profile t9 --query 'Buckets[?starts_with(Name, `team9-`)].Name' --output table +aws ssm describe-parameters --profile t9 --region us-east-1 --query 'Parameters[].Name' --output table +``` + +Expected: all target clusters, repos, buckets, and parameters are present. + +- [ ] **Step 3: Verify repo test/format commands** + +Run: + +```bash +cd /Users/winrey/Projects/weightwave/team9 && pnpm prettier --check docs/superpowers/specs/2026-06-01-team9-dev-aws-account-migration-design.md docs/superpowers/plans/2026-06-01-team9-dev-aws-account-migration.md docs/aws/team9-dev-aws-migration-runbook.md +cd /Users/winrey/Projects/weightwave/capability-hub && pnpm test -- src/file/file.service.spec.ts src/config/config.schema.spec.ts +``` + +Expected: + +```text +All matched files use Prettier code style! +PASS src/file/file.service.spec.ts +PASS src/config/config.schema.spec.ts +``` + +- [ ] **Step 4: Record final commit IDs** + +Run: + +```bash +for repo in team9 aHand folder9 openclaw-hive; do + git -C "/Users/winrey/Projects/weightwave/$repo" log -1 --oneline +done +``` + +Expected: each repo prints the migration branch commit intended for review. From 00942b7c46ec94b77e4a8e6fc2a244e7eaac445c Mon Sep 17 00:00:00 2001 From: Winrey Date: Mon, 1 Jun 2026 19:18:07 +0800 Subject: [PATCH 03/13] infra: add t9 aws bootstrap --- .gitignore | 5 + infra/aws/t9-bootstrap/.terraform.lock.hcl | 25 +++ infra/aws/t9-bootstrap/main.tf | 180 +++++++++++++++++++++ infra/aws/t9-bootstrap/outputs.tf | 19 +++ infra/aws/t9-bootstrap/providers.tf | 12 ++ infra/aws/t9-bootstrap/versions.tf | 10 ++ 6 files changed, 251 insertions(+) create mode 100644 infra/aws/t9-bootstrap/.terraform.lock.hcl create mode 100644 infra/aws/t9-bootstrap/main.tf create mode 100644 infra/aws/t9-bootstrap/outputs.tf create mode 100644 infra/aws/t9-bootstrap/providers.tf create mode 100644 infra/aws/t9-bootstrap/versions.tf diff --git a/.gitignore b/.gitignore index d355626b..3943d184 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,11 @@ apps/client/src-tauri/target/ # Git worktrees .worktrees/ +# Terraform local state and provider cache +**/.terraform/ +*.tfstate +*.tfstate.* + # Enterprise (when using submodule, this is handled by git automatically) # Only ignore if developing locally without submodule setup # enterprise/ diff --git a/infra/aws/t9-bootstrap/.terraform.lock.hcl b/infra/aws/t9-bootstrap/.terraform.lock.hcl new file mode 100644 index 00000000..cdc1668d --- /dev/null +++ b/infra/aws/t9-bootstrap/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} diff --git a/infra/aws/t9-bootstrap/main.tf b/infra/aws/t9-bootstrap/main.tf new file mode 100644 index 00000000..b220c868 --- /dev/null +++ b/infra/aws/t9-bootstrap/main.tf @@ -0,0 +1,180 @@ +locals { + s3_buckets = { + team9_files_dev = { name = "team9-files-dev", public_read = false } + team9_files_prod = { name = "team9-files-prod", public_read = false } + ahand_hub_dev = { name = "team9-ahand-hub-dev", public_read = false } + ahand_hub_prod = { name = "team9-ahand-hub-prod", public_read = false } + capability_hub_dev = { name = "team9-capability-hub-dev", public_read = true } + capability_hub_prod = { name = "team9-capability-hub-prod", public_read = true } + } + + # aHand's own shared Terraform creates the ahand-hub ECR repository. + # folder9 dev looks up its ECR repositories as pre-existing resources, so + # bootstrap creates those two repos before the dev stack runs. + ecr_repositories = toset([ + "control-plane", + "file-keeper", + "efs-webdav", + "openclaw-hive", + "folder9", + "folder9-dashboard", + ]) +} + +resource "aws_s3_bucket" "tfstate" { + bucket = "team9-tfstate" +} + +resource "aws_s3_bucket_versioning" "tfstate" { + bucket = aws_s3_bucket.tfstate.id + + versioning_configuration { + status = "Enabled" + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "tfstate" { + bucket = aws_s3_bucket.tfstate.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +resource "aws_s3_bucket_public_access_block" "tfstate" { + bucket = aws_s3_bucket.tfstate.id + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_dynamodb_table" "terraform_state_lock" { + name = "terraform-state-lock" + billing_mode = "PAY_PER_REQUEST" + hash_key = "LockID" + + attribute { + name = "LockID" + type = "S" + } +} + +resource "aws_s3_bucket" "service" { + for_each = local.s3_buckets + bucket = each.value.name +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "service" { + for_each = aws_s3_bucket.service + bucket = each.value.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +resource "aws_s3_bucket_public_access_block" "service_private" { + for_each = { + for key, cfg in local.s3_buckets : key => cfg + if cfg.public_read == false + } + + bucket = aws_s3_bucket.service[each.key].id + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_s3_bucket_public_access_block" "service_public_read" { + for_each = { + for key, cfg in local.s3_buckets : key => cfg + if cfg.public_read == true + } + + bucket = aws_s3_bucket.service[each.key].id + block_public_acls = true + block_public_policy = false + ignore_public_acls = true + restrict_public_buckets = false +} + +resource "aws_s3_bucket_policy" "capability_hub_public_read" { + for_each = { + for key, cfg in local.s3_buckets : key => cfg + if cfg.public_read == true + } + + bucket = aws_s3_bucket.service[each.key].id + + depends_on = [aws_s3_bucket_public_access_block.service_public_read] + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "PublicReadObjects" + Effect = "Allow" + Principal = "*" + Action = "s3:GetObject" + Resource = "${aws_s3_bucket.service[each.key].arn}/*" + } + ] + }) +} + +resource "aws_s3_bucket_lifecycle_configuration" "team9_files" { + bucket = aws_s3_bucket.service["team9_files_dev"].id + + rule { + id = "auto-delete-pending-uploads" + status = "Enabled" + + filter { + tag { + key = "status" + value = "pending" + } + } + + expiration { + days = 1 + } + } +} + +resource "aws_s3_bucket_lifecycle_configuration" "ahand_hub_dev" { + bucket = aws_s3_bucket.service["ahand_hub_dev"].id + + rule { + id = "expire-file-ops-dev" + status = "Enabled" + + filter { + prefix = "file-ops/" + } + + expiration { + days = 7 + } + + abort_incomplete_multipart_upload { + days_after_initiation = 1 + } + } +} + +resource "aws_ecr_repository" "service" { + for_each = local.ecr_repositories + name = each.value + image_tag_mutability = "MUTABLE" + + image_scanning_configuration { + scan_on_push = true + } +} diff --git a/infra/aws/t9-bootstrap/outputs.tf b/infra/aws/t9-bootstrap/outputs.tf new file mode 100644 index 00000000..c90c1c0d --- /dev/null +++ b/infra/aws/t9-bootstrap/outputs.tf @@ -0,0 +1,19 @@ +output "tfstate_bucket" { + value = aws_s3_bucket.tfstate.bucket +} + +output "lock_table" { + value = aws_dynamodb_table.terraform_state_lock.name +} + +output "service_buckets" { + value = { + for key, bucket in aws_s3_bucket.service : key => bucket.bucket + } +} + +output "ecr_repository_urls" { + value = { + for name, repo in aws_ecr_repository.service : name => repo.repository_url + } +} diff --git a/infra/aws/t9-bootstrap/providers.tf b/infra/aws/t9-bootstrap/providers.tf new file mode 100644 index 00000000..6b5d8b13 --- /dev/null +++ b/infra/aws/t9-bootstrap/providers.tf @@ -0,0 +1,12 @@ +provider "aws" { + region = "us-east-1" + profile = "t9" + + default_tags { + tags = { + Project = "team9" + ManagedBy = "Terraform" + Account = "t9" + } + } +} diff --git a/infra/aws/t9-bootstrap/versions.tf b/infra/aws/t9-bootstrap/versions.tf new file mode 100644 index 00000000..3fc806d9 --- /dev/null +++ b/infra/aws/t9-bootstrap/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.6.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} From a2eecd8a178e7b026a9c7ee43715ad58fafca489 Mon Sep 17 00:00:00 2001 From: Winrey Date: Mon, 1 Jun 2026 19:54:28 +0800 Subject: [PATCH 04/13] infra: add t9 dev core network --- infra/aws/t9-dev-core/.terraform.lock.hcl | 25 ++++ infra/aws/t9-dev-core/backend.tf | 10 ++ infra/aws/t9-dev-core/main.tf | 172 ++++++++++++++++++++++ infra/aws/t9-dev-core/outputs.tf | 35 +++++ infra/aws/t9-dev-core/providers.tf | 13 ++ infra/aws/t9-dev-core/versions.tf | 10 ++ 6 files changed, 265 insertions(+) create mode 100644 infra/aws/t9-dev-core/.terraform.lock.hcl create mode 100644 infra/aws/t9-dev-core/backend.tf create mode 100644 infra/aws/t9-dev-core/main.tf create mode 100644 infra/aws/t9-dev-core/outputs.tf create mode 100644 infra/aws/t9-dev-core/providers.tf create mode 100644 infra/aws/t9-dev-core/versions.tf diff --git a/infra/aws/t9-dev-core/.terraform.lock.hcl b/infra/aws/t9-dev-core/.terraform.lock.hcl new file mode 100644 index 00000000..cdc1668d --- /dev/null +++ b/infra/aws/t9-dev-core/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} diff --git a/infra/aws/t9-dev-core/backend.tf b/infra/aws/t9-dev-core/backend.tf new file mode 100644 index 00000000..d3d9a619 --- /dev/null +++ b/infra/aws/t9-dev-core/backend.tf @@ -0,0 +1,10 @@ +terraform { + backend "s3" { + bucket = "team9-tfstate" + key = "team9/dev-core/terraform.tfstate" + region = "us-east-1" + profile = "t9" + dynamodb_table = "terraform-state-lock" + encrypt = true + } +} diff --git a/infra/aws/t9-dev-core/main.tf b/infra/aws/t9-dev-core/main.tf new file mode 100644 index 00000000..875c9a02 --- /dev/null +++ b/infra/aws/t9-dev-core/main.tf @@ -0,0 +1,172 @@ +data "aws_availability_zones" "available" { + state = "available" +} + +locals { + azs = slice(data.aws_availability_zones.available.names, 0, 2) +} + +resource "aws_vpc" "dev" { + cidr_block = "10.90.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true + + tags = { + Name = "team9-dev" + } +} + +resource "aws_internet_gateway" "dev" { + vpc_id = aws_vpc.dev.id + + tags = { + Name = "team9-dev" + } +} + +resource "aws_subnet" "public" { + for_each = { + a = { cidr = "10.90.1.0/24", az = local.azs[0] } + b = { cidr = "10.90.2.0/24", az = local.azs[1] } + } + + vpc_id = aws_vpc.dev.id + cidr_block = each.value.cidr + availability_zone = each.value.az + map_public_ip_on_launch = true + + tags = { + Name = "team9-dev-public-${each.key}" + } +} + +resource "aws_route_table" "public" { + vpc_id = aws_vpc.dev.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.dev.id + } + + tags = { + Name = "team9-dev-public" + } +} + +resource "aws_route_table_association" "public" { + for_each = aws_subnet.public + subnet_id = each.value.id + route_table_id = aws_route_table.public.id +} + +resource "aws_ecs_cluster" "openclaw_hive_dev" { + name = "openclaw-hive-dev" +} + +resource "aws_security_group" "rds" { + name = "openclaw-hive-dev-rds" + description = "PostgreSQL ingress from Team9 dev VPC" + vpc_id = aws_vpc.dev.id + + ingress { + from_port = 5432 + to_port = 5432 + protocol = "tcp" + cidr_blocks = [aws_vpc.dev.cidr_block] + description = "PostgreSQL from dev VPC" + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_security_group" "openclaw_traefik" { + name = "openclaw-hive-dev-traefik" + description = "OpenClaw Hive dev Traefik ingress and egress" + vpc_id = aws_vpc.dev.id + + ingress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + description = "HTTP from internet" + } + + ingress { + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + description = "HTTPS from internet" + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_db_subnet_group" "openclaw_hive_dev" { + name = "openclaw-hive-dev" + subnet_ids = [for subnet in aws_subnet.public : subnet.id] +} + +resource "aws_security_group" "efs" { + name = "openclaw-hive-dev-efs" + description = "NFS ingress from Team9 dev VPC" + vpc_id = aws_vpc.dev.id + + ingress { + from_port = 2049 + to_port = 2049 + protocol = "tcp" + cidr_blocks = [aws_vpc.dev.cidr_block] + description = "NFS from dev VPC" + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_efs_file_system" "openclaw_hive_dev" { + creation_token = "openclaw-hive-dev" + encrypted = true + + tags = { + Name = "openclaw-hive-dev" + } +} + +resource "aws_efs_file_system" "openclaw_hive_dev_efs" { + creation_token = "openclaw-hive-dev-efs" + encrypted = true + + tags = { + Name = "openclaw-hive-dev-efs" + } +} + +resource "aws_efs_mount_target" "openclaw_hive_dev" { + for_each = aws_subnet.public + file_system_id = aws_efs_file_system.openclaw_hive_dev.id + subnet_id = each.value.id + security_groups = [aws_security_group.efs.id] +} + +resource "aws_efs_mount_target" "openclaw_hive_dev_efs" { + for_each = aws_subnet.public + file_system_id = aws_efs_file_system.openclaw_hive_dev_efs.id + subnet_id = each.value.id + security_groups = [aws_security_group.efs.id] +} diff --git a/infra/aws/t9-dev-core/outputs.tf b/infra/aws/t9-dev-core/outputs.tf new file mode 100644 index 00000000..e9cf7e6e --- /dev/null +++ b/infra/aws/t9-dev-core/outputs.tf @@ -0,0 +1,35 @@ +output "vpc_id" { + value = aws_vpc.dev.id +} + +output "public_subnet_ids" { + value = [for subnet in aws_subnet.public : subnet.id] +} + +output "openclaw_cluster_name" { + value = aws_ecs_cluster.openclaw_hive_dev.name +} + +output "rds_security_group_id" { + value = aws_security_group.rds.id +} + +output "openclaw_traefik_security_group_id" { + value = aws_security_group.openclaw_traefik.id +} + +output "db_subnet_group_name" { + value = aws_db_subnet_group.openclaw_hive_dev.name +} + +output "efs_security_group_id" { + value = aws_security_group.efs.id +} + +output "openclaw_hive_dev_efs_id" { + value = aws_efs_file_system.openclaw_hive_dev.id +} + +output "openclaw_hive_dev_extra_efs_id" { + value = aws_efs_file_system.openclaw_hive_dev_efs.id +} diff --git a/infra/aws/t9-dev-core/providers.tf b/infra/aws/t9-dev-core/providers.tf new file mode 100644 index 00000000..408fb73a --- /dev/null +++ b/infra/aws/t9-dev-core/providers.tf @@ -0,0 +1,13 @@ +provider "aws" { + region = "us-east-1" + profile = "t9" + + default_tags { + tags = { + Project = "team9" + Environment = "dev" + ManagedBy = "Terraform" + Stack = "dev-core" + } + } +} diff --git a/infra/aws/t9-dev-core/versions.tf b/infra/aws/t9-dev-core/versions.tf new file mode 100644 index 00000000..3fc806d9 --- /dev/null +++ b/infra/aws/t9-dev-core/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.6.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} From 527ec96479c44e12a48beea2de15c103464d6964 Mon Sep 17 00:00:00 2001 From: Winrey Date: Tue, 2 Jun 2026 01:15:32 +0800 Subject: [PATCH 05/13] infra: add t9 dev iam roles --- infra/aws/t9-dev-core/iam.tf | 330 +++++++++++++++++++++++++++++++ infra/aws/t9-dev-core/outputs.tf | 16 ++ 2 files changed, 346 insertions(+) create mode 100644 infra/aws/t9-dev-core/iam.tf diff --git a/infra/aws/t9-dev-core/iam.tf b/infra/aws/t9-dev-core/iam.tf new file mode 100644 index 00000000..6fc2b6b5 --- /dev/null +++ b/infra/aws/t9-dev-core/iam.tf @@ -0,0 +1,330 @@ +data "aws_iam_policy_document" "github_oidc_openclaw_hive_dev" { + statement { + effect = "Allow" + + principals { + type = "Federated" + identifiers = [aws_iam_openid_connect_provider.github.arn] + } + + actions = ["sts:AssumeRoleWithWebIdentity"] + + condition { + test = "StringEquals" + variable = "token.actions.githubusercontent.com:aud" + values = ["sts.amazonaws.com"] + } + + condition { + test = "StringLike" + variable = "token.actions.githubusercontent.com:sub" + values = [ + "repo:weightwave/openclaw-hive:ref:refs/heads/dev", + "repo:weightwave/openclaw-hive:ref:refs/tags/dev-*", + "repo:weightwave/openclaw-hive:environment:development", + ] + } + } +} + +data "aws_iam_policy_document" "ecs_tasks_assume" { + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["ecs-tasks.amazonaws.com"] + } + + actions = ["sts:AssumeRole"] + } +} + +resource "aws_iam_openid_connect_provider" "github" { + url = "https://token.actions.githubusercontent.com" + client_id_list = ["sts.amazonaws.com"] + thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"] +} + +resource "aws_iam_role" "github_actions_openclaw_hive_dev_deploy" { + name = "GitHubActionsOpenClawHiveDevDeploy" + description = "Assumed via OIDC by dev OpenClaw Hive deployment workflows" + + assume_role_policy = data.aws_iam_policy_document.github_oidc_openclaw_hive_dev.json +} + +resource "aws_iam_role_policy" "github_actions_openclaw_hive_dev_deploy" { + name = "openclaw-hive-dev-deploy" + role = aws_iam_role.github_actions_openclaw_hive_dev_deploy.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "ECRAuth" + Effect = "Allow" + Action = ["ecr:GetAuthorizationToken"] + Resource = "*" + }, + { + Sid = "ECRPush" + Effect = "Allow" + Action = [ + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "ecr:InitiateLayerUpload", + "ecr:UploadLayerPart", + "ecr:CompleteLayerUpload", + "ecr:PutImage", + "ecr:DescribeRepositories", + "ecr:DescribeImages", + ] + Resource = [ + "arn:aws:ecr:us-east-1:149614785083:repository/control-plane", + "arn:aws:ecr:us-east-1:149614785083:repository/file-keeper", + "arn:aws:ecr:us-east-1:149614785083:repository/efs-webdav", + "arn:aws:ecr:us-east-1:149614785083:repository/openclaw-hive", + ] + }, + { + Sid = "ECSDeploy" + Effect = "Allow" + Action = [ + "ecs:RegisterTaskDefinition", + "ecs:DeregisterTaskDefinition", + "ecs:CreateService", + "ecs:UpdateService", + "ecs:DescribeServices", + "ecs:DescribeTaskDefinition", + "ecs:DescribeTasks", + "ecs:ListServices", + "ecs:ListTasks", + "ecs:RunTask", + "ecs:StopTask", + ] + Resource = "*" + }, + { + Sid = "CreateDeployLogGroups" + Effect = "Allow" + Action = ["logs:CreateLogGroup"] + Resource = "*" + }, + { + Sid = "ConfigureControlPlaneAutoscaling" + Effect = "Allow" + Action = [ + "application-autoscaling:RegisterScalableTarget", + "application-autoscaling:PutScalingPolicy", + ] + Resource = "*" + }, + { + Sid = "PassOpenClawEcsRoles" + Effect = "Allow" + Action = ["iam:PassRole"] + Resource = [ + aws_iam_role.ecs_task_execution.arn, + aws_iam_role.ecs_task.arn, + ] + }, + ] + }) +} + +resource "aws_iam_service_linked_role" "ecs_application_autoscaling" { + aws_service_name = "ecs.application-autoscaling.amazonaws.com" + description = "Allows Application Auto Scaling to manage ECS service desired counts" +} + +resource "aws_iam_role" "ecs_task_execution" { + name = "ecsTaskExecutionRole" + assume_role_policy = data.aws_iam_policy_document.ecs_tasks_assume.json +} + +resource "aws_iam_role_policy_attachment" "ecs_task_execution" { + role = aws_iam_role.ecs_task_execution.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" +} + +resource "aws_iam_role" "ecs_task" { + name = "ecsTaskRole" + assume_role_policy = data.aws_iam_policy_document.ecs_tasks_assume.json +} + +resource "aws_iam_role_policy" "ecs_task_control_plane_management" { + name = "ControlPlaneManagement" + role = aws_iam_role.ecs_task.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "ECSTaskManagement" + Effect = "Allow" + Action = [ + "ecs:RunTask", + "ecs:StopTask", + "ecs:DescribeTasks", + "ecs:DescribeTaskDefinition", + "ecs:RegisterTaskDefinition", + "ecs:DeregisterTaskDefinition", + "ecs:TagResource", + ] + Resource = "*" + }, + { + Sid = "EFSAccessPointManagement" + Effect = "Allow" + Action = [ + "elasticfilesystem:CreateAccessPoint", + "elasticfilesystem:DeleteAccessPoint", + "elasticfilesystem:DescribeAccessPoints", + "elasticfilesystem:TagResource", + ] + Resource = "*" + }, + { + Sid = "PassRole" + Effect = "Allow" + Action = ["iam:PassRole"] + Resource = [ + aws_iam_role.ecs_task_execution.arn, + aws_iam_role.ecs_task.arn, + ] + }, + { + Sid = "CloudWatchLogs" + Effect = "Allow" + Action = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:FilterLogEvents", + "logs:GetLogEvents", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:StartLiveTail", + "logs:StartQuery", + "logs:GetQueryResults", + ] + Resource = "*" + }, + { + Sid = "ECRImageManagement" + Effect = "Allow" + Action = [ + "ecr:DescribeImages", + "ecr:BatchDeleteImage", + "ecr:ListImages", + ] + Resource = "arn:aws:ecr:us-east-1:149614785083:repository/openclaw-hive" + }, + { + Sid = "SQSECSEvents" + Effect = "Allow" + Action = [ + "sqs:ReceiveMessage", + "sqs:DeleteMessage", + "sqs:GetQueueAttributes", + ] + Resource = "arn:aws:sqs:us-east-1:149614785083:openclaw-hive-ecs-events-dev" + }, + ] + }) +} + +resource "aws_iam_role_policy" "ecs_task_exec" { + name = "ECSExecPolicy" + role = aws_iam_role.ecs_task.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "ssmmessages:CreateControlChannel", + "ssmmessages:CreateDataChannel", + "ssmmessages:OpenControlChannel", + "ssmmessages:OpenDataChannel", + ] + Resource = "*" + }, + ] + }) +} + +resource "aws_iam_role_policy" "ecs_task_efs_access" { + name = "EFSAccess" + role = aws_iam_role.ecs_task.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "EFSMountAccess" + Effect = "Allow" + Action = [ + "elasticfilesystem:ClientMount", + "elasticfilesystem:ClientWrite", + "elasticfilesystem:ClientRootAccess", + ] + Resource = "*" + }, + { + Sid = "EFSManagement" + Effect = "Allow" + Action = [ + "elasticfilesystem:CreateFileSystem", + "elasticfilesystem:CreateAccessPoint", + "elasticfilesystem:CreateMountTarget", + "elasticfilesystem:DescribeFileSystems", + "elasticfilesystem:DescribeAccessPoints", + "elasticfilesystem:DescribeMountTargets", + "elasticfilesystem:DescribeMountTargetSecurityGroups", + "elasticfilesystem:DescribeFileSystemPolicy", + "elasticfilesystem:TagResource", + "elasticfilesystem:DeleteAccessPoint", + ] + Resource = "*" + }, + { + Sid = "EC2NetworkForEFS" + Effect = "Allow" + Action = [ + "ec2:DescribeSubnets", + "ec2:DescribeNetworkInterfaces", + "ec2:DescribeSecurityGroups", + ] + Resource = "*" + }, + ] + }) +} + +resource "aws_iam_role_policy" "ecs_task_traefik_discovery" { + name = "TraefikECSDiscovery" + role = aws_iam_role.ecs_task.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "ecs:ListClusters", + "ecs:ListTasks", + "ecs:DescribeClusters", + "ecs:DescribeTasks", + "ecs:DescribeContainerInstances", + "ecs:DescribeTaskDefinition", + "ec2:DescribeInstances", + "ec2:DescribeNetworkInterfaces", + ] + Resource = "*" + }, + ] + }) +} diff --git a/infra/aws/t9-dev-core/outputs.tf b/infra/aws/t9-dev-core/outputs.tf index e9cf7e6e..51f491dd 100644 --- a/infra/aws/t9-dev-core/outputs.tf +++ b/infra/aws/t9-dev-core/outputs.tf @@ -33,3 +33,19 @@ output "openclaw_hive_dev_efs_id" { output "openclaw_hive_dev_extra_efs_id" { value = aws_efs_file_system.openclaw_hive_dev_efs.id } + +output "github_oidc_provider_arn" { + value = aws_iam_openid_connect_provider.github.arn +} + +output "openclaw_hive_dev_deploy_role_arn" { + value = aws_iam_role.github_actions_openclaw_hive_dev_deploy.arn +} + +output "ecs_task_execution_role_arn" { + value = aws_iam_role.ecs_task_execution.arn +} + +output "ecs_task_role_arn" { + value = aws_iam_role.ecs_task.arn +} From 7b306d204c1b26bb6f4a4e15b320bba2cc7e17a7 Mon Sep 17 00:00:00 2001 From: Winrey Date: Tue, 2 Jun 2026 01:27:57 +0800 Subject: [PATCH 06/13] docs: add team9 dev aws migration runbook --- docs/aws/team9-dev-aws-migration-runbook.md | 157 ++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 docs/aws/team9-dev-aws-migration-runbook.md diff --git a/docs/aws/team9-dev-aws-migration-runbook.md b/docs/aws/team9-dev-aws-migration-runbook.md new file mode 100644 index 00000000..824694fe --- /dev/null +++ b/docs/aws/team9-dev-aws-migration-runbook.md @@ -0,0 +1,157 @@ +# Team9 Dev AWS Migration Runbook + +## Current Accounts + +- Source AWS account (`ww`): `471112576951` +- Target AWS account (`t9`): `149614785083` +- Region: `us-east-1` + +## S3 Pre-Copy + +Run before write-freeze. The 2026-06-02 source inventory is under 1 GiB: + +- `ahand-hub-dev`: 10 objects, 84.4 KiB +- `t9-development`: 140 objects, 118.0 MiB +- `capability-hub`: 432 objects, 652.7 MiB + +The migration target buckets are: + +- `ahand-hub-dev` -> `team9-ahand-hub-dev` +- `t9-development` -> `team9-files-dev` +- `capability-hub` -> `team9-capability-hub-prod` + +`team9-capability-hub-dev` is created separately and starts empty because no distinct old dev source bucket was found. + +Pre-copy was completed on 2026-06-02 and verified with matching source/target totals: + +- `team9-ahand-hub-dev`: 10 objects, 84.4 KiB +- `team9-files-dev`: 140 objects, 118.0 MiB +- `team9-capability-hub-prod`: 432 objects, 652.7 MiB +- `team9-capability-hub-dev`: 0 objects, 0 Bytes + +```bash +mkdir -p /tmp/team9-s3-migration +aws s3 sync s3://ahand-hub-dev /tmp/team9-s3-migration/ahand-hub-dev --region us-east-1 --profile ww +aws s3 sync /tmp/team9-s3-migration/ahand-hub-dev s3://team9-ahand-hub-dev --region us-east-1 --profile t9 +aws s3 sync s3://t9-development /tmp/team9-s3-migration/t9-development --region us-east-1 --profile ww +aws s3 sync /tmp/team9-s3-migration/t9-development s3://team9-files-dev --region us-east-1 --profile t9 +aws s3 sync s3://capability-hub /tmp/team9-s3-migration/capability-hub --region us-east-1 --profile ww +aws s3 sync /tmp/team9-s3-migration/capability-hub s3://team9-capability-hub-prod --region us-east-1 --profile t9 +``` + +Run again during write-freeze with `--delete`: + +```bash +aws s3 sync s3://ahand-hub-dev /tmp/team9-s3-migration/ahand-hub-dev --region us-east-1 --profile ww --delete +aws s3 sync /tmp/team9-s3-migration/ahand-hub-dev s3://team9-ahand-hub-dev --region us-east-1 --profile t9 --delete +aws s3 sync s3://t9-development /tmp/team9-s3-migration/t9-development --region us-east-1 --profile ww --delete +aws s3 sync /tmp/team9-s3-migration/t9-development s3://team9-files-dev --region us-east-1 --profile t9 --delete +aws s3 sync s3://capability-hub /tmp/team9-s3-migration/capability-hub --region us-east-1 --profile ww --delete +aws s3 sync /tmp/team9-s3-migration/capability-hub s3://team9-capability-hub-prod --region us-east-1 --profile t9 --delete +``` + +Verify after copy: + +```bash +for b in team9-ahand-hub-dev team9-files-dev team9-capability-hub-prod team9-capability-hub-dev; do + echo "TARGET $b" + aws s3 ls "s3://$b" --recursive --summarize --human-readable --profile t9 --region us-east-1 | tail -5 +done +``` + +## RDS Final Snapshot and Restore + +Run during write-freeze only: + +```bash +SNAPSHOT_ID="openclaw-hive-dev-final-$(date -u +%Y%m%d%H%M%S)" +aws rds create-db-snapshot \ + --profile ww \ + --region us-east-1 \ + --db-instance-identifier openclaw-hive-dev \ + --db-snapshot-identifier "$SNAPSHOT_ID" + +aws rds wait db-snapshot-completed \ + --profile ww \ + --region us-east-1 \ + --db-snapshot-identifier "$SNAPSHOT_ID" + +aws rds modify-db-snapshot-attribute \ + --profile ww \ + --region us-east-1 \ + --db-snapshot-identifier "$SNAPSHOT_ID" \ + --attribute-name restore \ + --values-to-add 149614785083 +``` + +Then copy the shared snapshot in the `t9` account so it is encrypted in the target account: + +```bash +aws rds copy-db-snapshot \ + --profile t9 \ + --region us-east-1 \ + --source-db-snapshot-identifier "arn:aws:rds:us-east-1:471112576951:snapshot:${SNAPSHOT_ID}" \ + --target-db-snapshot-identifier "${SNAPSHOT_ID}-t9-encrypted" \ + --kms-key-id alias/aws/rds + +aws rds wait db-snapshot-completed \ + --profile t9 \ + --region us-east-1 \ + --db-snapshot-identifier "${SNAPSHOT_ID}-t9-encrypted" + +DB_SUBNET_GROUP="$(terraform -chdir=/Users/winrey/Projects/weightwave/team9/infra/aws/t9-dev-core output -raw db_subnet_group_name)" +RDS_SG_ID="$(terraform -chdir=/Users/winrey/Projects/weightwave/team9/infra/aws/t9-dev-core output -raw rds_security_group_id)" + +if aws rds describe-db-instances \ + --profile t9 \ + --region us-east-1 \ + --db-instance-identifier openclaw-hive-dev >/dev/null 2>&1; then + echo "Target RDS openclaw-hive-dev already exists in t9. Stop and decide whether to keep it or replace it." >&2 + exit 1 +fi + +aws rds restore-db-instance-from-db-snapshot \ + --profile t9 \ + --region us-east-1 \ + --db-instance-identifier openclaw-hive-dev \ + --db-snapshot-identifier "${SNAPSHOT_ID}-t9-encrypted" \ + --db-instance-class db.t4g.micro \ + --db-subnet-group-name "$DB_SUBNET_GROUP" \ + --vpc-security-group-ids "$RDS_SG_ID" \ + --publicly-accessible \ + --no-multi-az \ + --storage-type gp3 + +aws rds wait db-instance-available \ + --profile t9 \ + --region us-east-1 \ + --db-instance-identifier openclaw-hive-dev + +aws rds describe-db-instances \ + --profile t9 \ + --region us-east-1 \ + --db-instance-identifier openclaw-hive-dev \ + --query 'DBInstances[0].Endpoint.Address' \ + --output text +``` + +## EFS Copy + +Copy these source file systems: + +- `fs-05f7f1c836631ddce` (`folder9-dev`) +- `fs-057baa1f60ff58b91` (`folder9-dev-acme`) +- `fs-0f3888df726d8d9f8` (`openclaw-hive-dev`) +- `fs-0920bb3d4db7aa843` (`openclaw-hive-dev-efs`) + +Target file systems created in `t9` so far: + +- `fs-01a332c4d065de570` (`openclaw-hive-dev`) +- `fs-08ba71f27e0f12b75` (`openclaw-hive-dev-efs`) + +Use AWS DataSync when available. If DataSync is not already configured, run a temporary migration EC2 instance with both source and target EFS mounted over reachable networking. Copy once before write-freeze and once during write-freeze: + +```bash +sudo rsync -aHAX --numeric-ids --info=progress2 /mnt/source/ /mnt/target/ +sudo find /mnt/target -maxdepth 2 -type f | head -50 +``` From 9da66a8158c2368243902b98177e3adada50d50f Mon Sep 17 00:00:00 2001 From: Winrey Date: Tue, 2 Jun 2026 02:59:49 +0800 Subject: [PATCH 07/13] infra: import t9 dev data services --- docs/aws/team9-dev-aws-migration-runbook.md | 65 +++++++++++++++++++++ infra/aws/t9-dev-core/main.tf | 61 +++++++++++++++++++ infra/aws/t9-dev-core/outputs.tf | 20 +++++++ 3 files changed, 146 insertions(+) diff --git a/docs/aws/team9-dev-aws-migration-runbook.md b/docs/aws/team9-dev-aws-migration-runbook.md index 824694fe..f5bb37f2 100644 --- a/docs/aws/team9-dev-aws-migration-runbook.md +++ b/docs/aws/team9-dev-aws-migration-runbook.md @@ -135,6 +135,67 @@ aws rds describe-db-instances \ --output text ``` +Executed during the 2026-06-02 write-freeze: + +- Source snapshot: `openclaw-hive-dev-final-20260601173022` +- Target encrypted snapshot: `openclaw-hive-dev-final-20260601173022-t9-encrypted` +- Restored target RDS: `openclaw-hive-dev.c89gkagwy37d.us-east-1.rds.amazonaws.com:5432` +- Target RDS is encrypted, PostgreSQL `16.10`, `db.t4g.micro`, `20 GiB gp3` +- Target RDS and `control-plane-dev` Redis are imported into `infra/aws/t9-dev-core` Terraform state + +The old `ww` dev ECS services were scaled to desired `0` before the final S3 sync and RDS snapshot: + +- `openclaw-hive-dev/file-keeper-dev` +- `openclaw-hive-dev/control-plane-dev` +- `openclaw-hive-dev/ahand-hub-dev` +- `openclaw-hive-dev/traefik-dev` +- `folder9-dev/folder9-traefik-dev` +- `folder9-dev/folder9-dashboard-dev` +- `folder9-dev/folder9-dev` + +## Redis + +Target `t9` Redis resources: + +- Control plane Redis Serverless cache: `control-plane-dev-z6rr48.serverless.use1.cache.amazonaws.com:6379` +- Control plane Redis SG: `sg-01024f55ce79e2342` +- aHand hub Redis cluster: `ahand-hub-dev` + +Redis runtime data was not copied. Treat it as ephemeral cache/session data. + +## ECS Entrypoints + +Target `t9` entrypoints created during the migration: + +- OpenClaw Traefik dev NLB: `traefik-dev-nlb-ba679d7f5738b11f.elb.us-east-1.amazonaws.com` +- OpenClaw Traefik dev SG: `sg-0368318519318a4ba` +- folder9 dev NLB: `folder9-dev-nlb-b95ffda8112d744a.elb.us-east-1.amazonaws.com` + +The OpenClaw Traefik script prints an incorrect reminder of `*.instance.instance.claw.dev.team9.ai`. +Use the actual desired DNS records from the dev domains instead. + +## ECR + +Copied `:dev` images from `ww` to `t9`: + +- `control-plane:dev` +- `file-keeper:dev` +- `efs-webdav:dev` +- `ahand-hub:dev` +- `folder9:dev` +- `folder9-dashboard:dev` + +`openclaw-hive:dev` did not complete via local `docker push`; use GitHub Actions or retry from a better network path before launching workloads that require it. + +## SSM Parameters + +Copied folder9 dev SSM parameters from `ww` to `t9` after folder9 Terraform apply: + +- `/folder9/dev/*` +- `/folder9-dashboard/dev/*` + +The copied `DATABASE_URL` values were rewritten from the old RDS endpoint to `openclaw-hive-dev.c89gkagwy37d.us-east-1.rds.amazonaws.com`. + ## EFS Copy Copy these source file systems: @@ -148,6 +209,8 @@ Target file systems created in `t9` so far: - `fs-01a332c4d065de570` (`openclaw-hive-dev`) - `fs-08ba71f27e0f12b75` (`openclaw-hive-dev-efs`) +- `fs-03ce541320ea7827c` (`folder9-dev`) +- `fs-0875869ddefaba679` (`folder9-dev-acme`) Use AWS DataSync when available. If DataSync is not already configured, run a temporary migration EC2 instance with both source and target EFS mounted over reachable networking. Copy once before write-freeze and once during write-freeze: @@ -155,3 +218,5 @@ Use AWS DataSync when available. If DataSync is not already configured, run a te sudo rsync -aHAX --numeric-ids --info=progress2 /mnt/source/ /mnt/target/ sudo find /mnt/target -maxdepth 2 -type f | head -50 ``` + +EFS content has not yet been copied as of this runbook update. Keep folder9 app services at desired `0` until EFS data and real SSM secrets are verified. diff --git a/infra/aws/t9-dev-core/main.tf b/infra/aws/t9-dev-core/main.tf index 875c9a02..542d298f 100644 --- a/infra/aws/t9-dev-core/main.tf +++ b/infra/aws/t9-dev-core/main.tf @@ -82,6 +82,10 @@ resource "aws_security_group" "rds" { protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } + + lifecycle { + ignore_changes = [ingress] + } } resource "aws_security_group" "openclaw_traefik" { @@ -111,6 +115,10 @@ resource "aws_security_group" "openclaw_traefik" { protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } + + lifecycle { + ignore_changes = [ingress] + } } resource "aws_db_subnet_group" "openclaw_hive_dev" { @@ -118,6 +126,31 @@ resource "aws_db_subnet_group" "openclaw_hive_dev" { subnet_ids = [for subnet in aws_subnet.public : subnet.id] } +resource "aws_db_instance" "openclaw_hive_dev" { + identifier = "openclaw-hive-dev" + + instance_class = "db.t4g.micro" + engine = "postgres" + engine_version = "16.10" + allocated_storage = 20 + storage_type = "gp3" + storage_encrypted = true + + db_subnet_group_name = aws_db_subnet_group.openclaw_hive_dev.name + vpc_security_group_ids = [aws_security_group.rds.id] + + publicly_accessible = true + multi_az = false + backup_retention_period = 7 + auto_minor_version_upgrade = true + deletion_protection = false + skip_final_snapshot = true + + lifecycle { + prevent_destroy = true + } +} + resource "aws_security_group" "efs" { name = "openclaw-hive-dev-efs" description = "NFS ingress from Team9 dev VPC" @@ -139,6 +172,27 @@ resource "aws_security_group" "efs" { } } +resource "aws_security_group" "redis" { + name = "control-plane-redis-dev-sg" + description = "Security group for Control Plane Dev Redis" + vpc_id = aws_vpc.dev.id + + ingress { + from_port = 6379 + to_port = 6380 + protocol = "tcp" + security_groups = [aws_security_group.openclaw_traefik.id] + description = "Redis from OpenClaw Hive dev ECS tasks" + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} + resource "aws_efs_file_system" "openclaw_hive_dev" { creation_token = "openclaw-hive-dev" encrypted = true @@ -170,3 +224,10 @@ resource "aws_efs_mount_target" "openclaw_hive_dev_efs" { subnet_id = each.value.id security_groups = [aws_security_group.efs.id] } + +resource "aws_elasticache_serverless_cache" "control_plane_dev" { + name = "control-plane-dev" + engine = "redis" + security_group_ids = [aws_security_group.redis.id] + subnet_ids = [for subnet in aws_subnet.public : subnet.id] +} diff --git a/infra/aws/t9-dev-core/outputs.tf b/infra/aws/t9-dev-core/outputs.tf index 51f491dd..e97eb7b1 100644 --- a/infra/aws/t9-dev-core/outputs.tf +++ b/infra/aws/t9-dev-core/outputs.tf @@ -22,6 +22,14 @@ output "db_subnet_group_name" { value = aws_db_subnet_group.openclaw_hive_dev.name } +output "rds_endpoint" { + value = aws_db_instance.openclaw_hive_dev.address +} + +output "rds_port" { + value = aws_db_instance.openclaw_hive_dev.port +} + output "efs_security_group_id" { value = aws_security_group.efs.id } @@ -49,3 +57,15 @@ output "ecs_task_execution_role_arn" { output "ecs_task_role_arn" { value = aws_iam_role.ecs_task.arn } + +output "redis_security_group_id" { + value = aws_security_group.redis.id +} + +output "redis_endpoint" { + value = aws_elasticache_serverless_cache.control_plane_dev.endpoint[0].address +} + +output "redis_port" { + value = aws_elasticache_serverless_cache.control_plane_dev.endpoint[0].port +} From d17360265a4745c5728416f183fdd34c2790f6f6 Mon Sep 17 00:00:00 2001 From: Winrey Date: Tue, 2 Jun 2026 03:03:21 +0800 Subject: [PATCH 08/13] docs: record folder9 shared secret migration --- docs/aws/team9-dev-aws-migration-runbook.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/aws/team9-dev-aws-migration-runbook.md b/docs/aws/team9-dev-aws-migration-runbook.md index f5bb37f2..242097b6 100644 --- a/docs/aws/team9-dev-aws-migration-runbook.md +++ b/docs/aws/team9-dev-aws-migration-runbook.md @@ -193,6 +193,7 @@ Copied folder9 dev SSM parameters from `ww` to `t9` after folder9 Terraform appl - `/folder9/dev/*` - `/folder9-dashboard/dev/*` +- `/folder9/shared/cloudflare_dns_token` The copied `DATABASE_URL` values were rewritten from the old RDS endpoint to `openclaw-hive-dev.c89gkagwy37d.us-east-1.rds.amazonaws.com`. From fca2f165bef0d26b5cd2f40fd0bbb67f598fff43 Mon Sep 17 00:00:00 2001 From: Winrey Date: Tue, 2 Jun 2026 03:32:54 +0800 Subject: [PATCH 09/13] docs: record t9 dev efs migration --- docs/aws/team9-dev-aws-migration-runbook.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/aws/team9-dev-aws-migration-runbook.md b/docs/aws/team9-dev-aws-migration-runbook.md index 242097b6..1db2cd81 100644 --- a/docs/aws/team9-dev-aws-migration-runbook.md +++ b/docs/aws/team9-dev-aws-migration-runbook.md @@ -213,11 +213,13 @@ Target file systems created in `t9` so far: - `fs-03ce541320ea7827c` (`folder9-dev`) - `fs-0875869ddefaba679` (`folder9-dev-acme`) -Use AWS DataSync when available. If DataSync is not already configured, run a temporary migration EC2 instance with both source and target EFS mounted over reachable networking. Copy once before write-freeze and once during write-freeze: +Executed during the 2026-06-02 write-freeze using temporary Fargate tasks and S3 tarballs: -```bash -sudo rsync -aHAX --numeric-ids --info=progress2 /mnt/source/ /mnt/target/ -sudo find /mnt/target -maxdepth 2 -type f | head -50 -``` +- `fs-05f7f1c836631ddce` -> `fs-03ce541320ea7827c`: `folder9.tgz`, restored count `11633` +- `fs-057baa1f60ff58b91` -> `fs-0875869ddefaba679`: `folder9-acme.tgz`, restored count `1` +- `fs-0f3888df726d8d9f8` -> `fs-01a332c4d065de570`: `openclaw.tgz`, restored count `2015` +- `fs-0920bb3d4db7aa843` -> `fs-08ba71f27e0f12b75`: `openclaw-extra.tgz`, restored count `268` -EFS content has not yet been copied as of this runbook update. Keep folder9 app services at desired `0` until EFS data and real SSM secrets are verified. +Temporary migration S3 buckets and inline IAM policies were removed after verification. +CloudWatch migration logs are retained for 7 days in `/ecs/efs-migration` in both accounts. +Keep folder9 app services at desired `0` until final app secret/runtime verification is complete. From 994f537901b01c44aa7b8e416d7bedf63b9638c7 Mon Sep 17 00:00:00 2001 From: Winrey Date: Wed, 3 Jun 2026 14:44:29 +0800 Subject: [PATCH 10/13] docs: mark openclaw runtime migration cancelled --- docs/aws/team9-dev-aws-migration-runbook.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/aws/team9-dev-aws-migration-runbook.md b/docs/aws/team9-dev-aws-migration-runbook.md index 1db2cd81..5af09582 100644 --- a/docs/aws/team9-dev-aws-migration-runbook.md +++ b/docs/aws/team9-dev-aws-migration-runbook.md @@ -174,6 +174,20 @@ Target `t9` entrypoints created during the migration: The OpenClaw Traefik script prints an incorrect reminder of `*.instance.instance.claw.dev.team9.ai`. Use the actual desired DNS records from the dev domains instead. +## OpenClaw Runtime + +OpenClaw runtime migration was cancelled on 2026-06-03. Do not continue the +`openclaw-hive:dev` image copy or deploy OpenClaw control-plane/file-keeper +services into `t9` unless the requirement changes again. + +Current `t9` `openclaw-hive-dev` ECS cluster services are: + +- `traefik-dev`: retained as shared ingress for aHand dev (`ahand-hub.dev.team9.ai`) +- `ahand-hub-dev`: retained for the aHand dev environment + +There are no `t9` OpenClaw control-plane/file-keeper/instance services running. +The old `ww` OpenClaw dev services remain scaled to desired `0`. + ## ECR Copied `:dev` images from `ww` to `t9`: @@ -186,6 +200,8 @@ Copied `:dev` images from `ww` to `t9`: - `folder9-dashboard:dev` `openclaw-hive:dev` did not complete via local `docker push`; use GitHub Actions or retry from a better network path before launching workloads that require it. +This image is no longer required for the current migration scope because +OpenClaw runtime migration has been cancelled. ## SSM Parameters From 4f4757cd8f745b4d9e71f02c480d16f859c3b06b Mon Sep 17 00:00:00 2001 From: Winrey Date: Wed, 3 Jun 2026 14:50:14 +0800 Subject: [PATCH 11/13] docs: record file keeper and openclaw shutdown --- docs/aws/team9-dev-aws-migration-runbook.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/aws/team9-dev-aws-migration-runbook.md b/docs/aws/team9-dev-aws-migration-runbook.md index 5af09582..676a8148 100644 --- a/docs/aws/team9-dev-aws-migration-runbook.md +++ b/docs/aws/team9-dev-aws-migration-runbook.md @@ -188,6 +188,17 @@ Current `t9` `openclaw-hive-dev` ECS cluster services are: There are no `t9` OpenClaw control-plane/file-keeper/instance services running. The old `ww` OpenClaw dev services remain scaled to desired `0`. +On 2026-06-03, file-keeper and OpenClaw runtime were explicitly treated as +shut down rather than migrated: + +- `ww/openclaw-hive-dev/file-keeper-dev`: desired `0`, running `0` +- `ww/openclaw-hive-dev/control-plane-dev`: desired `0`, running `0` +- `ww/openclaw-hive-dev`: no running or pending standalone tasks +- `t9/openclaw-hive-dev`: no file-keeper, control-plane, efs-webdav, or OpenClaw instance services/task definitions + +Leave historical `ww` task definitions registered for audit/rollback context; +they have no runtime cost while no ECS service or task is using them. + ## ECR Copied `:dev` images from `ww` to `t9`: From fab7a8cf9e21c3e1c29043ec5a67e6ae427515e4 Mon Sep 17 00:00:00 2001 From: Winrey Date: Wed, 3 Jun 2026 15:55:51 +0800 Subject: [PATCH 12/13] infra: cut over team9 dev aws dependencies --- docs/aws/team9-dev-aws-migration-runbook.md | 84 +++++++++++++-- infra/aws/t9-bootstrap/main.tf | 109 ++++++++++++++++++++ infra/aws/t9-bootstrap/outputs.tf | 8 ++ infra/aws/t9-dev-core/iam.tf | 20 ++++ 4 files changed, 212 insertions(+), 9 deletions(-) diff --git a/docs/aws/team9-dev-aws-migration-runbook.md b/docs/aws/team9-dev-aws-migration-runbook.md index 676a8148..ae149008 100644 --- a/docs/aws/team9-dev-aws-migration-runbook.md +++ b/docs/aws/team9-dev-aws-migration-runbook.md @@ -6,28 +6,86 @@ - Target AWS account (`t9`): `149614785083` - Region: `us-east-1` +## 2026-06-03 Cutover State + +The Team9 dev AWS-backed services have been migrated so that the old `ww` +account can be shut down after retention/audit approval. Railway production +was not modified. + +Runtime in `t9`: + +- aHand dev: `openclaw-hive-dev/ahand-hub-dev`, desired/running `1/1`, + task definition `ahand-hub-dev:2` +- Shared Traefik for aHand dev: `openclaw-hive-dev/traefik-dev`, + desired/running `1/1`, task definition `traefik-dev:2` +- folder9 dev: `folder9-dev/folder9-dev`, desired/running `1/1` +- folder9 dashboard dev: `folder9-dev/folder9-dashboard-dev`, + desired/running `1/1` +- folder9 Traefik dev: `folder9-dev/folder9-traefik-dev`, + desired/running `1/1` + +DNS cutover: + +- `ahand-hub.dev.team9.ai` -> `traefik-dev-nlb-ba679d7f5738b11f.elb.us-east-1.amazonaws.com` +- `folder.dev.team9.ai` -> `folder9-dev-nlb-b95ffda8112d744a.elb.us-east-1.amazonaws.com` +- `git.folder.dev.team9.ai` -> `folder9-dev-nlb-b95ffda8112d744a.elb.us-east-1.amazonaws.com` +- `admin.folder.dev.team9.ai` -> `folder9-dev-nlb-b95ffda8112d744a.elb.us-east-1.amazonaws.com` +- `files.dev.team9.ai` -> `d2tedyjbca4wja.cloudfront.net` + +The `files.dev.team9.ai` CloudFront distribution is now in `t9`: + +- Distribution ID: `E202H3U9IOAAUY` +- Origin: `team9-files-dev.s3.us-east-1.amazonaws.com` +- ACM certificate: `arn:aws:acm:us-east-1:149614785083:certificate/f5d21c68-8ca1-4258-b427-8fd0ce94fc17` +- Access model: CloudFront OAC reads the private `team9-files-dev` bucket +- Compatibility function strips both `/t9-development/` and `/team9-files-dev/` + path prefixes + +The old `ww` CloudFront distribution `E1TFNLFUI3702I` no longer has the +`files.dev.team9.ai` alias. + +Railway `development` variables were updated and the services were restarted: + +- `API-Gateway`: `S3_BUCKET=team9-files-dev`, S3 access key account `149614785083` +- `Im-worker`: `S3_BUCKET=team9-files-dev`, S3 access key account `149614785083` +- `capability-hub`: `S3_BUCKET=team9-capability-hub-dev`, S3 access key account `149614785083` + +Other Railway dev URLs point to migrated dev endpoints: + +- `S3_PUBLIC_URL=https://files.dev.team9.ai` +- `AHAND_HUB_URL=https://ahand-hub.dev.team9.ai` +- `FOLDER9_API_URL=https://folder.dev.team9.ai` +- `CAPABILITY_BASE_URL=https://gateway.capability.dev.team9.ai` + +OpenClaw runtime and file-keeper were intentionally not migrated. Keep them +off unless the requirement changes. + ## S3 Pre-Copy Run before write-freeze. The 2026-06-02 source inventory is under 1 GiB: - `ahand-hub-dev`: 10 objects, 84.4 KiB -- `t9-development`: 140 objects, 118.0 MiB -- `capability-hub`: 432 objects, 652.7 MiB +- `t9-development`: 141 objects, 118.0 MiB after the final 2026-06-03 delta copy +- `capability-hub`: 433 objects, 654.7 MiB after the final 2026-06-03 delta copy The migration target buckets are: - `ahand-hub-dev` -> `team9-ahand-hub-dev` - `t9-development` -> `team9-files-dev` +- `capability-hub` -> `team9-capability-hub-dev` - `capability-hub` -> `team9-capability-hub-prod` -`team9-capability-hub-dev` is created separately and starts empty because no distinct old dev source bucket was found. +No distinct old capability-hub dev source bucket was found, so the old +`capability-hub` bucket was copied into both `team9-capability-hub-dev` and +`team9-capability-hub-prod`. -Pre-copy was completed on 2026-06-02 and verified with matching source/target totals: +Pre-copy was completed on 2026-06-02. Final deltas were copied on 2026-06-03 +and verified with matching source/target totals: - `team9-ahand-hub-dev`: 10 objects, 84.4 KiB -- `team9-files-dev`: 140 objects, 118.0 MiB -- `team9-capability-hub-prod`: 432 objects, 652.7 MiB -- `team9-capability-hub-dev`: 0 objects, 0 Bytes +- `team9-files-dev`: 141 objects, 118.0 MiB +- `team9-capability-hub-dev`: 433 objects, 654.7 MiB +- `team9-capability-hub-prod`: 433 objects, 654.7 MiB ```bash mkdir -p /tmp/team9-s3-migration @@ -36,6 +94,7 @@ aws s3 sync /tmp/team9-s3-migration/ahand-hub-dev s3://team9-ahand-hub-dev --reg aws s3 sync s3://t9-development /tmp/team9-s3-migration/t9-development --region us-east-1 --profile ww aws s3 sync /tmp/team9-s3-migration/t9-development s3://team9-files-dev --region us-east-1 --profile t9 aws s3 sync s3://capability-hub /tmp/team9-s3-migration/capability-hub --region us-east-1 --profile ww +aws s3 sync /tmp/team9-s3-migration/capability-hub s3://team9-capability-hub-dev --region us-east-1 --profile t9 aws s3 sync /tmp/team9-s3-migration/capability-hub s3://team9-capability-hub-prod --region us-east-1 --profile t9 ``` @@ -47,6 +106,7 @@ aws s3 sync /tmp/team9-s3-migration/ahand-hub-dev s3://team9-ahand-hub-dev --reg aws s3 sync s3://t9-development /tmp/team9-s3-migration/t9-development --region us-east-1 --profile ww --delete aws s3 sync /tmp/team9-s3-migration/t9-development s3://team9-files-dev --region us-east-1 --profile t9 --delete aws s3 sync s3://capability-hub /tmp/team9-s3-migration/capability-hub --region us-east-1 --profile ww --delete +aws s3 sync /tmp/team9-s3-migration/capability-hub s3://team9-capability-hub-dev --region us-east-1 --profile t9 --delete aws s3 sync /tmp/team9-s3-migration/capability-hub s3://team9-capability-hub-prod --region us-east-1 --profile t9 --delete ``` @@ -171,8 +231,14 @@ Target `t9` entrypoints created during the migration: - OpenClaw Traefik dev SG: `sg-0368318519318a4ba` - folder9 dev NLB: `folder9-dev-nlb-b95ffda8112d744a.elb.us-east-1.amazonaws.com` -The OpenClaw Traefik script prints an incorrect reminder of `*.instance.instance.claw.dev.team9.ai`. -Use the actual desired DNS records from the dev domains instead. +The OpenClaw Traefik dev task definition now reads the Cloudflare token from +SSM instead of plaintext task environment variables and uses +`/letsencrypt/acme-t9.json` so the migrated `ww` ACME account state is not +reused in `t9`. + +The OpenClaw Traefik script used to print an incorrect reminder of +`*.instance.instance.claw.dev.team9.ai`. Use the actual desired DNS records +from the dev domains instead. ## OpenClaw Runtime diff --git a/infra/aws/t9-bootstrap/main.tf b/infra/aws/t9-bootstrap/main.tf index b220c868..073ba2d2 100644 --- a/infra/aws/t9-bootstrap/main.tf +++ b/infra/aws/t9-bootstrap/main.tf @@ -1,4 +1,6 @@ locals { + team9_files_dev_cdn_alias = "files.dev.team9.ai" + s3_buckets = { team9_files_dev = { name = "team9-files-dev", public_read = false } team9_files_prod = { name = "team9-files-prod", public_read = false } @@ -148,6 +150,113 @@ resource "aws_s3_bucket_lifecycle_configuration" "team9_files" { } } +resource "aws_acm_certificate" "team9_files_dev_cdn" { + domain_name = local.team9_files_dev_cdn_alias + validation_method = "DNS" + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_acm_certificate_validation" "team9_files_dev_cdn" { + certificate_arn = aws_acm_certificate.team9_files_dev_cdn.arn + validation_record_fqdns = [ + for option in aws_acm_certificate.team9_files_dev_cdn.domain_validation_options : + option.resource_record_name + ] +} + +resource "aws_cloudfront_origin_access_control" "team9_files_dev" { + name = "team9-files-dev-oac" + description = "OAC for the Team9 dev file bucket" + origin_access_control_origin_type = "s3" + signing_behavior = "always" + signing_protocol = "sigv4" +} + +resource "aws_cloudfront_function" "team9_files_dev_strip_bucket_prefix" { + name = "team9-files-dev-strip-bucket-prefix" + runtime = "cloudfront-js-2.0" + publish = true + code = <<-EOT +function handler(event) { + var req = event.request; + req.uri = req.uri.replace(/^\/t9-development\//, '/'); + req.uri = req.uri.replace(/^\/team9-files-dev\//, '/'); + return req; +} +EOT +} + +resource "aws_cloudfront_distribution" "team9_files_dev" { + enabled = true + is_ipv6_enabled = true + comment = "CDN for Team9 dev files" + aliases = [local.team9_files_dev_cdn_alias] + http_version = "http2" + price_class = "PriceClass_All" + + origin { + domain_name = aws_s3_bucket.service["team9_files_dev"].bucket_regional_domain_name + origin_id = "team9-files-dev-s3" + origin_access_control_id = aws_cloudfront_origin_access_control.team9_files_dev.id + } + + default_cache_behavior { + target_origin_id = "team9-files-dev-s3" + viewer_protocol_policy = "redirect-to-https" + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + compress = true + + cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6" + origin_request_policy_id = "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf" + response_headers_policy_id = "60669652-455b-4ae9-85a4-c4c02393f86c" + + function_association { + event_type = "viewer-request" + function_arn = aws_cloudfront_function.team9_files_dev_strip_bucket_prefix.arn + } + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + acm_certificate_arn = aws_acm_certificate_validation.team9_files_dev_cdn.certificate_arn + ssl_support_method = "sni-only" + minimum_protocol_version = "TLSv1.2_2021" + } +} + +resource "aws_s3_bucket_policy" "team9_files_dev_cloudfront_read" { + bucket = aws_s3_bucket.service["team9_files_dev"].id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "AllowCloudFrontReadTeam9FilesDev" + Effect = "Allow" + Principal = { + Service = "cloudfront.amazonaws.com" + } + Action = "s3:GetObject" + Resource = "${aws_s3_bucket.service["team9_files_dev"].arn}/*" + Condition = { + StringEquals = { + "AWS:SourceArn" = aws_cloudfront_distribution.team9_files_dev.arn + } + } + } + ] + }) +} + resource "aws_s3_bucket_lifecycle_configuration" "ahand_hub_dev" { bucket = aws_s3_bucket.service["ahand_hub_dev"].id diff --git a/infra/aws/t9-bootstrap/outputs.tf b/infra/aws/t9-bootstrap/outputs.tf index c90c1c0d..f8a9454b 100644 --- a/infra/aws/t9-bootstrap/outputs.tf +++ b/infra/aws/t9-bootstrap/outputs.tf @@ -17,3 +17,11 @@ output "ecr_repository_urls" { for name, repo in aws_ecr_repository.service : name => repo.repository_url } } + +output "team9_files_dev_cdn" { + value = { + alias = local.team9_files_dev_cdn_alias + id = aws_cloudfront_distribution.team9_files_dev.id + domain_name = aws_cloudfront_distribution.team9_files_dev.domain_name + } +} diff --git a/infra/aws/t9-dev-core/iam.tf b/infra/aws/t9-dev-core/iam.tf index 6fc2b6b5..6c77d8ed 100644 --- a/infra/aws/t9-dev-core/iam.tf +++ b/infra/aws/t9-dev-core/iam.tf @@ -148,6 +148,26 @@ resource "aws_iam_role_policy_attachment" "ecs_task_execution" { policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" } +resource "aws_iam_role_policy" "ecs_task_execution_ssm_secrets" { + name = "SSMSecretRead" + role = aws_iam_role.ecs_task_execution.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "ReadSharedCloudflareToken" + Effect = "Allow" + Action = [ + "ssm:GetParameter", + "ssm:GetParameters", + ] + Resource = "arn:aws:ssm:us-east-1:149614785083:parameter/folder9/shared/cloudflare_dns_token" + }, + ] + }) +} + resource "aws_iam_role" "ecs_task" { name = "ecsTaskRole" assume_role_policy = data.aws_iam_policy_document.ecs_tasks_assume.json From decf556f9461d40732aec969328cf994057caf64 Mon Sep 17 00:00:00 2001 From: Winrey Date: Wed, 3 Jun 2026 17:07:59 +0800 Subject: [PATCH 13/13] docs: record ww dev decommission state --- docs/aws/team9-dev-aws-migration-runbook.md | 57 +++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/docs/aws/team9-dev-aws-migration-runbook.md b/docs/aws/team9-dev-aws-migration-runbook.md index ae149008..ec80ef57 100644 --- a/docs/aws/team9-dev-aws-migration-runbook.md +++ b/docs/aws/team9-dev-aws-migration-runbook.md @@ -60,6 +60,63 @@ Other Railway dev URLs point to migrated dev endpoints: OpenClaw runtime and file-keeper were intentionally not migrated. Keep them off unless the requirement changes. +## 2026-06-03 PRs and ww Dev Decommission + +Draft PRs created for the migration branches: + +- Team9: https://github.com/team9ai/team9/pull/124 +- folder9: https://github.com/team9ai/folder9/pull/9 +- aHand: https://github.com/team9ai/aHand/pull/42 +- openclaw-hive: https://github.com/weightwave/openclaw-hive/pull/2 + +Additional `ww` dev-only shutdown actions completed after the cutover: + +- Removed Cloudflare DNS records that still pointed OpenClaw/file-keeper dev + hostnames at the old `ww` Traefik dev NLB: + - `file-keeper.claw.dev.team9.ai` + - `files-explorer.claw.dev.team9.ai` + - `*.fk.claw.dev.team9.ai` + - `*.instance.claw.dev.team9.ai` + - `plane.claw.dev.team9.ai` +- Disabled the old `ww` `files.dev.team9.ai` CloudFront distribution + `E1TFNLFUI3702I`. It now has no aliases and `Enabled=false`. +- Stopped the old `ww` dev RDS instance `openclaw-hive-dev`. + +Verified after decommission: + +- Cloudflare has no records pointing to the old `ww` dev NLBs or old + `files.dev` CloudFront domain. +- `ww/openclaw-hive-dev` ECS services all have desired/running `0/0`. +- `ww/folder9-dev` ECS services all have desired/running `0/0`. +- `ww/openclaw-hive-dev` RDS status is `stopped`. + +Remaining `ww` dev resources that still exist for rollback/retention and can +be deleted after the observation window: + +- S3 buckets: `t9-development`, `ahand-hub-dev`, `capability-hub` +- ElastiCache Redis: `ahand-hub-dev` +- NLBs: `traefik-dev-nlb-8cda97ce6b37e5e1`, `folder9-dev-nlb-c5c54878a223a778` +- ECS clusters/services/task definitions for `openclaw-hive-dev` and + `folder9-dev` +- Disabled CloudFront distribution `E1TFNLFUI3702I` +- Stopped RDS instance `openclaw-hive-dev` and its old snapshots + +`ww` cannot be fully closed yet if production still depends on it. Current +production blockers observed in `ww`: + +- `folder9-prod` ECS cluster is still running `folder9-prod`, + `folder9-dashboard-prod`, and `folder9-traefik-prod`. +- `openclaw-hive` ECS cluster is still running `control-plane-prod`, + `file-keeper-prod`, `ahand-hub-prod`, Traefik, and a file-keeper instance. +- RDS `openclaw-hive-prod` is still available. +- ElastiCache Redis `ahand-hub-prod` is still available. +- CloudFront `E210V6XIJ0X5JL` still serves `files.team9.ai`. +- Cloudflare production records still point at `ww` prod NLBs: + `folder.team9.ai`, `git.folder.team9.ai`, `admin.folder.team9.ai`, + `plane.claw.team9.ai`, `*.instance.claw.team9.ai`, + `file-keeper.claw.team9.ai`, `files-explorer.claw.team9.ai`, + `*.fk.claw.team9.ai`, `ahand-hub.team9.ai`, and `*.claw.team9.ai`. + ## S3 Pre-Copy Run before write-freeze. The 2026-06-02 source inventory is under 1 GiB: