Skip to content

ADR 0013: Prove profile extensibility with a bounded static site

  • Status: Accepted for the second concrete deployment profile implementation
  • Related issue: Task #31

Context

One deployment profile cannot demonstrate that generic lifecycle and provider contracts compose independently of an SSH application gateway. The second profile must have a real distinct outcome without embedding a new provider assumption into the core or creating a general-purpose guest shell executor.

The existing Yandex lifecycle adapter already implements six independently owned resources, explicit identity and placement, immutable initialization digests, and a private metadata upload copy. The existing artifact store already implements exclusive bounded publication and verified local cleanup. Those contracts can support a concrete static website without changing either layer.

Decision

Add a version-one static-site profile for an explicitly selected ubuntu-24.04-cloud-init fresh image and Yandex placement. It generates one nginx HTTP page from a bounded plain title and body, escaped into fixed HTML markup. Fixed cloud-init configuration contains only public keys, publishable page text, and fixed guest operations. The profile offers no arbitrary shell, template, package, file path, or web-server configuration input.

The schema requires explicit provider, folder, owner, zone, image, private subnet, administrator username/public key/sources, and HTTP sources. HTTP port 80 permits explicit IPv4 /0 through /32 sources; administrator SSH port 22 requires separate /24 through /32 sources. Each set contains at most sixteen nonoverlapping canonical networks. An exact root-owned authorized key path, disabled alternative key/CA sources, full SSH configuration, and persistent SSH service activation preserve the fresh guest authorization boundary.

CompileStaticSite() validates immutable intent. BuildStaticSiteBundle() uses the existing owned server artifact store. RenderStaticSitePlan() verifies the entire unchanged owned bundle and exact generated bytes, then emits the existing network, subnet, security group, address, disk, and instance resource graph. The actual lifecycle parser and engine consume that plan without new semantics. The existing provider validates and dispatches its digest-bound cloud-init.

PrepareStaticSite() and a module CLI publish the cloud-init and lifecycle TOML in separate private owned bundles inside the explicit artifact root. Neither replaces preexisting files. Failed plan publication binds cleanup to the original generated bundle through an additive expected_bundle argument on the existing artifact removal API. The store checks the complete expected file bytes and manifest metadata under its operation lock before any removal. This preserves valid same-identity different-content replacements that ownership alone would reauthorize. Default artifact cleanup callers keep their existing behavior. An exact already published plan preserves its referenced server; detected replacements remain untouched for explicit recovery. Byte-identical bundles with identical manifest metadata remain equivalent expected content; this argument does not claim a historical inode receipt or hostile same-user isolation.

Public parsing/compilation/artifact/rendering hooks allow an extension adapter to compose the profile later. This profile does not depend on extension discovery, an unmerged extension package, or provider mutation imports. The existing secure gateway implementation, core CLI, and provider contracts remain unchanged.

Consequences

The second profile proves a web-serving outcome can reuse ownership, lifecycle, provider translation, and generated artifact contracts without being a renamed gateway. It creates no tunnel/device accounts, performs no packet routing, and accepts no credentials. HTTP and SSH source permissions stay independent.

The fixed bootstrap checks SSH, nftables, and nginx configuration before its explicit service activation. Distribution package installation can start services separately. Local preparation and provider existence do not prove successful guest execution or readiness. No readiness marker or claim is emitted. Actual guest behavior requires separately authorized validation.

The profile intentionally has no TLS, domain automation, multiple pages, uploaded assets, dynamic applications, IPv6, Windows artifact storage, or arbitrary server configuration. Public page text is not a secret transport. Generated plans have an absolute local initialization reference and must not become tracked portable configuration. Operators retain both bundles while lifecycle recovery needs them.

The existing store retains its cooperative same-user race limitation and partial filesystem failure behavior. Local cleanup never substitutes for cloud destroy.

Alternatives considered

A renamed gateway with optional HTTP ingress would not prove a new concrete outcome because it would not install or configure a web listener. A generic cloud-init or shell pass-through would expose arbitrary guest execution and obscure the bounded profile contract. A new cloud provider would expand this task into provider translation and permission work instead of proving profile reuse.

Extracting a shared renderer from the gateway now would couple independent implementation streams and change a working profile for cosmetic deduplication. The small static-site renderer targets the existing stable resource subset; any later shared serializer can be justified by the observed two-profile behavior.

Compatibility and migration

Existing configurations, gateway APIs, state, provider identities, lifecycle commands, and resource semantics are unchanged. Static-site TOML is a separate explicit schema and has no automatic migration. Both profiles work independently without requiring extension discovery.

Validation

Deterministic tests compose the actual lifecycle parser, engine, and Yandex adapter with a fake CLI runner for create, status, repeated create, reverse dependency destroy, repeated destroy, initialization digest rejection, rollback, and prepared artifact cleanup. Strict rejection, bounded UTF-8 text, HTML escaping, private path constraints, collisions, and failed publication are tested. Shell/bootstrap checks use isolated command fakes; available guest syntax tools run in offline test modes only. No cloud resource or host service is mutated.

See the static-site contract for preparation, operator use, cleanup, and the exact implemented limits.

Repository source