RFC 0005: Stages and deployment identity

Chiusa
#52 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
5/5
Tempo stimato
Più di una settimana
Idoneità per principianti
35/100
Tipo di issue
Funzionalità
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
aws, nodejs, typescript

Direzione di ricerca

Start by reading the linked RFCs 0010 and 0007 and tracing the current deploy command, deployment state key, and runtimeModules configuration. The payload names no source files or tests, so locate their entry points before estimating the implementation. Done means deployments are keyed by app, stage, and entry, generated names and ownership tags distinguish stages, and the new required config and validation behave as described.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

enhancement

Abstract

Notation identifies a deployment by the path of its entry file, such as infra/api.ts. The same code can therefore only be deployed one way: there is no way to deploy it as both staging and prod, and moving the entry file turns every deployed resource into an orphan. A deployment needs an identity that separates deployments of the same code and stays the same when files move, and the current one doesn't. AWS names come straight from resource ids, so two deployments of the same code in one account also clash.

We will solve this by identifying a deployment by its app, its stage and the name of its entry. The app and the entry names come from a required project config file, the stage from the deploy command, and none of them depend on where files live.

https://github.com/user-attachments/assets/cc02de84-9969-4f3f-8370-7a1abd83c502

Video transcript

When you run notation deploy, you give it the path of an entry file: the file that declares your infra. Take an orders app, whose entry file is infra slash api dot ts. Notation uses this path as the deployment ID. The deployment ID is the key for the state of the deployment: Notation's record of each resource that this deployment created, like the IAM role for the orders API.

Problem 1. Say you want a staging copy of the orders app, next to prod. You deploy the same entry file a second time. The path is the same, so the deployment ID is the same, and Notation finds the state of prod. So the second deploy does not create a staging copy. It updates prod. Today, to get a second stage, you must copy the entry file to a new path, and keep two copies of the same infra.

Problem 2. RFC 0010 will make the deployment ID matter outside of state. Under RFC 0010, the engine will make the name of each resource, and the ownership tags on each resource, from the deployment ID. The tags tell the engine which deployment made a resource. Now say you keep the state of staging in a separate store, so that staging does not update prod. Both deployments still have the ID infra slash api dot ts. Today, the staging deploy asks AWS to create a role that prod already made, and AWS refuses: the role already exists. The deploy fails loudly. But under RFC 0010, both deployments make the same role name and the same tags. The staging deploy finds the role of prod, reads the tags, and decides that the role is its own. So staging takes over the role of prod, and changes it, without any error.

Problem 3. Say you rename the entry file from infra slash api dot ts to infra slash orders dot ts. The deployment ID changes with the path, so the next deploy finds no state. Notation treats every resource as new, and creates all of them again. The old resources stay in AWS, with nothing to manage them.

All three problems have one cause, and this is the key point of the RFC: the deployment ID describes where a file lives, not what you deploy and where you deploy it. You can deploy the same code many times. And you can move the entry file, but the deployment should stay the same. So the deployment ID needs parts that do not come from the file layout.

The fix starts with a config file. Each project will have one file called notation config dot ts, and it will be required. The app field names the app, here orders-app. The entries field maps entry names to entry files. Here the entry called orders points to infra slash api dot ts. The runtime modules setting moves here from package dot json. And the default stage field is optional. Without it, every deploy must name its stage.

The deploy command will take an entry name, never a path, and a stage. So you will run notation deploy orders, with the stage flag set to prod. Notation will key the state with three parts: the app, the stage and the entry. Here, that is orders-app, prod, orders. Now, if you rename the entry file, you change one path in the config. The entry name stays orders, so the state key stays the same, and the next deploy finds its state.

Now deploy the same entry with the stage flag set to staging. The state key will be orders-app, staging, orders: a different key, so staging gets its own state, and does not update prod. Generated names and ownership tags will use the app and the stage too. The role of prod will get a generated name (orders-app-prod-orders-api-role-7f3a) that starts with orders-app and prod, and a tag for the app and a tag for the stage. The role of staging will get a name (orders-app-staging-orders-api-role-c21e) that starts with orders-app and staging, and a stage tag of staging. So the staging deploy cannot find the role of prod, and if it did, the tags would show that the role belongs to prod.

The app, the entry names and the stage end up in resource names and tags, so they must be name-safe. A stage name may use only lowercase letters, digits and hyphens, with at most 32 characters, and no hyphen at the start or end. Because the config is TypeScript, a type checks the app, the entry names and the default stage while you edit. For example, an app called Orders underscore App fails in the editor, before any deploy. A check at runtime does the same, and also checks the stage that you give on the command line.

This change has costs. Every project will need a config file, with an app and named entries, even a project with one entry. Notation will not migrate old state. State under the old path key stays where it is, so the first deploy after the change treats an existing deployment as new. And infra code cannot read the stage, so infra that differs between stages is out of scope for this RFC.

Before, the path of the entry file was the deployment ID. One entry file could have only one deployment, two deployments with the same ID could take over each other's resources, and moving the entry file lost its state. With this RFC, the app, the stage and the entry name will form the deployment ID. You will deploy one codebase as many stages, each stage will get its own names and tags, and you can move files without losing state.

Problem

The deployment ID is the entry file's project-relative path. It keys the deployment's state, and AWS names come from resource ids alone. This has three costs.

  • No stages. Deploying the same code twice produces the same deployment ID, so the second deploy updates the first. Separate stages need separate entry files that duplicate the infra.
  • Stages can't share an account. A Lambda's function name is its id and its role is <id>-role, so a second stage in the same account fails with "already exists".
  • Moving a file loses state. Renaming infra/api.ts changes the deployment ID, so the next deploy finds no state, creates everything again, and leaves the old resources behind.

Proposed solution

Each project will have one config file, notation.config.ts. It is required.

// notation.config.ts
import { defineConfig } from "@notation/core"

export default defineConfig({
  app: "orders-app",
  entries: {
    orders: "infra/api.ts",
  },
  runtimeModules: "src/**/*.fn.ts",
  defaultStage: "dev",
})
  • app is required. It is never derived from the package.json name.
  • entries maps entry names to entry files. It is required, even for a project with a single entry.
  • runtimeModules moves here from package.json (see 0007).
  • defaultStage is optional. There is no built-in default: without it, --stage is required.

The CLI takes an entry name, never a path, and the stage:

# before
notation deploy infra/api.ts             # deploymentId: "infra/api.ts"

# after
notation deploy orders --stage prod      # state key: "orders-app:prod:orders"
  • State is keyed by <app>:<stage>:<entry>, so one project can have several entries, and moving an entry file only changes its path in the config.
  • AWS names are scoped by app and stage, e.g. orders-app-prod-todos for a Lambda with id todos, so stages and apps can share an account.
Names

app, entry names and stage names are name-safe, because they form the state key and appear in AWS names. A stage name is [a-z0-9-], at most 32 characters, with no leading or trailing hyphen.

Because the config is TypeScript, the NameSafe template-literal type checks app, the entry names and defaultStage at compile time:

defineConfig({ app: "Orders_App", entries: { orders: "infra/api.ts" } })
// Type error: "Orders_App" is not name-safe

A runtime check backs this up, and also checks the --stage given on the command line.

AWS names

The engine names each account-unique AWS resource <app>-<stage>-<id>, plus any suffix such as -role. This covers Lambda functions, IAM roles, SQS queues, EventBridge rules and API Gateway APIs. Names that only need to be unique within a parent, such as an inline policy, are not scoped. A name written directly into an @notation/aws.iac resource config is kept as written.

When a name is longer than the service allows, the engine cuts it and appends - and 8 hex characters of a hash of the full name. The name stays unique and is the same on every deploy.

Infra code still cannot read the stage. The engine passes app and stage to resource collection, and only the naming function reads them.

Rationale

The identity of a deployment is a fact about what is being deployed and where, not about how the source is laid out. Separating the two lets the same code be deployed many times, and gives an identity that can go into cloud resource names, where a file path would leak project structure.

A config file, rather than fields in package.json, can be type-checked, so a bad name fails in the editor instead of at deploy.

Consequences

  • One codebase can be deployed as several stages, into one account or several.
  • AWS names change, so existing resources are replaced. Because old state is not migrated, an upgraded project creates its resources under new names and leaves the old ones unmanaged: destroy old deployments before upgrading.
  • Every project needs a notation.config.ts, with an app and named entries.
  • There is no backwards compatibility. State under the old path-based key is not migrated, so a deploy treats an existing deployment as new.
  • Infra code cannot read the stage. Stage-specific infra is out of scope.
Lingua principale
TypeScript
Stelle
54
Fork
1
Merge medio
15h 22m
PR unite (30g)
6

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di notationlabs/notation

Tutte le issue di notationlabs/notation

Issue simili

Altre issue su TypeScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.