Skip to content

feat(app-yaml): adopt an existing RDS instance and Route53 record - #155

Open
Alexanderamiri wants to merge 2 commits into
mainfrom
feat/moresleep-adopt
Open

Alexanderamiri wants to merge 2 commits into
mainfrom
feat/moresleep-adopt

Conversation

@Alexanderamiri

@Alexanderamiri Alexanderamiri commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Why

Today app.yaml can only create resources. javaBin/moresleep (sleepingpill.javazone.no) is moving onto the platform with no architectural change, so it has to keep two existing things:

  • RDS dbmoresleep (Postgres 17, public endpoint, serves the program API since 2021). Not in any Terraform state.
  • The hand-made Route53 A record sleepingpill.javazone.no. routing.host would fail on apply because the record already exists.

moresleep also reads DBHOST and DBPORT separately, but the RDS modules only expose endpoint (host:port).

What

app.yaml (validated in scripts/generate-modules.py):

routing:
  host: sleepingpill.javazone.no
  priority: 120
  adopt_dns: true              # take over the existing record
resources:
  databases:
    - name: main
      engine: postgres
      adopt: dbmoresleep       # import, never create
      host_env: DBHOST         # new, postgres only (created or adopted)
      port_env: DBPORT
      deletion_protection: true          # default true
      backup_retention_period: 14        # optional; unset = keep the live value
  • databases[].adopt generates module "rds_<name>" from the new terraform/modules/service-rds-adopted, plus:
    import {
      to = module.rds_main.aws_db_instance.this
      id = "dbmoresleep"
    }
    • The module manages only tags, deletion_protection, an optional backup_retention_period, and the Terraform-only final-snapshot settings: skip_final_snapshot=false, a final snapshot id, delete_automated_backups=false.
    • instance_class (required by the provider) is read from the live instance. Every other argument is in ignore_changes, and prevent_destroy is set.
    • instance_class, allocated_storage, engine_version and multi_az are rejected together with adopt. adopt is rejected for dynamodb or an invalid identifier.
    • No task-role policy is added, because the app keeps its own password auth.
    • Once the resource is in state, Terraform skips the import block, so it stays in the generated file.
  • host_env / port_env map to address / port outputs. service-rds gains an address output, and the adopted module's port is a string.
  • routing.adopt_dns: true passes allow_dns_overwrite = true to service-routing, which sets allow_overwrite on the record, so Route53 UPSERTs the existing record instead of failing on CREATE. The value is null when unset, so existing apps' records and generated files do not change.
    • I chose this over an import block on purpose. moresleep runs in parallel on moresleep.javazone.no first and then changes host. At that point the routing record address is already in state, so Terraform would skip an import and the replacement CREATE would fail. allow_overwrite covers both the fresh case and the host-change case.
  • Each adoption is approved in a platform PR (review fix). adopt and adopt_dns only validate when the identifier or host is listed for the app's team in ADOPTIONS in scripts/registry.py. This PR lists moresleep: rds dbmoresleep, dns sleepingpill.javazone.no. Why: Route53 records in the shared zones have no team ABAC, and DenyXTeam only fires on resources tagged with another team. Without the allowlist, any team could take over another app's hostname (host: submit.javazone.no + adopt_dns: true), or adopt, tag away and later delete an untagged legacy database with one app.yaml line. The generator always runs from platform main, so a team can't edit the list itself.
  • The adopted instance's final snapshot is <identifier>-final-<random_id hex> (stable per state), so a later delete/re-adopt cycle can't collide with an old snapshot. The module pins hashicorp/aws >= 5.40, < 6.0 because dedicated_log_volume is in ignore_changes.
  • Docs: app-yaml-reference.md (an "Adopting an existing PostgreSQL instance" section, adopt_dns, host_env/port_env), reusable-modules.md, terraform/modules/README.md and the module table in CLAUDE.md.
  • scripts/registry.py: MODULE_SOURCES gains service-rds-adopted.

How verified

  • python3 -m unittest discover -s scripts/tests: 59 tests OK, 16 of them new in AdoptTest (3 cover the approval list: another team's database, another team adopting dbmoresleep, an unapproved host). The existing fmt test now also runs on the moresleep fixture. terraform/lambda-src/tests: 64 OK.
  • terraform fmt -check -recursive terraform/modules is clean. terraform validate passes for service-rds-adopted, service-routing and service-rds (init -backend=false).
  • A local, read-only terraform plan against the real account (local backend, admin profile, never applied):
    • Minimal root with the module plus the import block:
      Plan: 1 to import, 0 to add, 1 to change, 0 to destroy (before the review fix; the random_id suffix now adds 1 to add). The only diffs are deletion_protection false -> true, tags_all (+5 default tags), and the Terraform-only skip_final_snapshot, final_snapshot_identifier and delete_automated_backups. Engine, class, storage, security group, publicity, PI and monitoring are untouched.
    • The generated moresleep phase-1 root (moresleep.javazone.no, all modules, local module sources):
      Plan: 1 to import, 22 to add, 1 to change, 0 to destroy. It stops only at reading IAM Role (javabin-ecs-exec-moresleep), which is created when the moresleep team is registered (javaBin/registry#40).
  • This PR's own Terraform Plan / Unit Tests checks are below.

Follow-ups

  • Companion PR feat(ci): deploy from the default branch and offer Postgres to JVM tests #154: deploy from the default branch (moresleep uses master) and Postgres for JVM tests.
  • Security workstream: dbmoresleep sits in the default VPC with the default SG open to 0.0.0.0/0. This PR deliberately leaves the SG alone.
  • An adopted instance is only reachable if its network allows the platform NAT. This is documented, not automated. The docs now say to allow only the NAT address (plus legacy clients) on a public instance's security group, never 0.0.0.0/0. All traffic to such an instance goes through the NAT and pays data processing.
  • Tagging the remaining untagged legacy resources (so DenyXTeam protects them) is still a security-workstream item. The approval list closes the app.yaml path, not hand-written Terraform.

🤖 Generated with Claude Code

Apps moving onto the platform could only get new resources. moresleep
must keep its RDS instance and its hand-made sleepingpill DNS record.

- databases[].adopt: <identifier> (postgres) generates the new
  service-rds-adopted module plus an import block. The module manages only
  tags, deletion_protection and an optional backup_retention_period;
  everything else is ignored, and prevent_destroy is set.
- databases[].host_env / port_env wire the hostname and port separately
  (created and adopted postgres); service-rds gains an address output.
- routing.adopt_dns: true sets allow_overwrite on the routing record so an
  existing record is taken over (Route53 UPSERT) instead of failing.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@Alexanderamiri
Alexanderamiri requested a review from a team as a code owner October 24, 2026 13:02
@github-actions

Copy link
Copy Markdown

Terraform Plan

✅ No changes — infrastructure is up to date.

Plan output
Acquiring state lock. This may take a few moments...
aws_ssm_parameter.atlassian_org_id: Refreshing state... [id=/javabin/platform/atlassian-org-id]
aws_ssm_parameter.atlassian_api_key: Refreshing state... [id=/javabin/platform/atlassian-api-key]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration
and found no differences, so no changes are needed.

…ecord

adopt and adopt_dns let a team take over resources the team boundary does not
protect (untagged legacy RDS instances, Route53 records in shared zones). Each
adoption now has to be listed for the team in ADOPTIONS in scripts/registry.py,
so it is approved in a platform PR. Also give the adopted instance's final
snapshot a stable random suffix and pin the module to AWS provider >= 5.40,
and tell adopters to keep a public instance's security group narrow.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@github-actions

Copy link
Copy Markdown

Terraform Plan

✅ No changes — infrastructure is up to date.

Plan output
Acquiring state lock. This may take a few moments...
aws_ssm_parameter.atlassian_org_id: Refreshing state... [id=/javabin/platform/atlassian-org-id]
aws_ssm_parameter.atlassian_api_key: Refreshing state... [id=/javabin/platform/atlassian-api-key]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration
and found no differences, so no changes are needed.
Releasing state lock. This may take a few moments...

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant