# Contributor Context Kyku provides an `AGENTS.md` at the repository root following the agent context file convention. This file contains the patterns, rules, and gotchas learned during development — structured specifically for AI agents and contributors. ## What is AGENTS.md? `AGENTS.md` is a convention where projects place context files at the repository root that AI coding agents can read to understand: - Project conventions and patterns - Rules for making changes - Provider-specific gotchas - Implementation checklists ## Kyku's AGENTS.md Structure The file at `/AGENTS.md` covers these sections: | Section | What It Covers | |---------|----------------| | 1. Adding a New Resource Type | All files that must be touched (checklist) | | 1.1 Adding a New Provider | Step-by-step provider creation checklist | | 2. Dependency Graph | Edge detection rules, graph verification | | 3. State vs Cloud — Planner Behavior | When the planner queries cloud APIs | | 4. Provider ID Management | Kyku UUIDs vs cloud-native IDs | | 5. Config Key Alignment | `read()` must match `resourceToConfig()` | | 6. Idempotent Create | Checking existence before creating | | 7. Destroy: Retry + Verify | Destroy retry logic with backoff | | 8. Use Label Selectors | Preferring labels over server IDs | | 9. Module Resolution | Dynamic import rules | | 10. Progress Display | ANSI cursor control conventions | | 11. Provider-Specific Gotchas | Per-provider pitfalls and fixes | | 12. TypeScript Patterns | Resource classes, generics, DOM lib | | 13. Testing & Verification | Build order, typecheck, real-API testing | | 14. Deploy Prefix | Auto-generated resource name prefixing | | 15. Plan Save/Load | Plan serialization to JSON | | 16. State Locking | File-based lock mechanics | | 17. Import Command | Resource adoption workflow | | 18. Hetzner DNS + S3 / DO Spaces | Separate API credentials | ## Key Patterns from AGENTS.md ### Adding a New Provider Checklist | Step | Location | Action | |------|----------|--------| | 1 | `packages/types/src/config.ts` | Add provider column to `INSTANCE_TYPE_MAP` and `IMAGE_MAP` | | 2 | `packages/core/src/auth/index.ts` | Add to `AuthConfig.provider` union | | 3 | `packages/cli/src/commands/provider-factory.ts` | Register `providerName: '@kykucloud/'` | | 4 | `packages/cli/package.json` | Add `"@kykucloud/": "workspace:*"` | | 5 | Create `packages//` | Copy `package.json` + `tsconfig.json` | | 6 | `src/client.ts` | REST or SDK client | | 7 | `src/mappings.ts` | Instance/image/region mapping functions | | 8 | `src/utils.ts` | Name normalization, tag helpers | | 9 | `src/resources/*.ts` | One manager per resource type | | 10 | `src/provider.ts` | Provider class implementing `Provider` | | 11 | `src/index.ts` | Export provider and public APIs | | 12 | Root `tsconfig.json` | Ensure `"lib": ["ES2022", "DOM"]` | | 13 | Build order | Rebuild `types` → `core` → `cli` | ### Critical Contract: Config Key Alignment The most common source of bugs is `read()` and `create()` returning different config shapes. Both must return config objects with **identical property names and types**: ```typescript // read() returns: config: { cidr: '10.0.0.0/16', region: 'us-east', distributeAcrossAzs: 2 } // create() must return SAME keys: config: { cidr: '10.0.0.0/16', region: 'us-east', distributeAcrossAzs: 2 } ``` Mismatches cause spurious update plans after apply. ### Provider Naming Convention Class name must be `PascalCaseProvider` (e.g., `DigitaloceanProvider`, `HetznerProvider`). The CLI factory constructs the class name as `${providerName.charAt(0).toUpperCase() + providerName.slice(1)}Provider`. ### Dependency Rule Use **object references** (not string IDs) for dependencies: ```typescript // ✅ Creates dependency edge vm.network = myVpc; // ❌ No dependency created vm.network = "vpc-abc123"; ``` ## Reading AGENTS.md When contributing to Kyku: ``` Read AGENTS.md before making changes to understand conventions. - For new providers: use section 1.1 checklist - For new resources: use section 1 checklist - For provider-specific code: check section 11 gotchas ``` ## How This Page is Generated This page mirrors the root `AGENTS.md` file in the repository. The source of truth is the file at the repository root — this page is a reference summary for contributors working through the docs site. --- # AI Agents Overview Kyku is designed to work well with AI coding agents (Claude Code, GitHub Copilot, Cursor, etc.). The TypeScript-based configuration, deterministic plan output, and structured state format make it easy for agents to understand and modify infrastructure. ## Why Kyku is Agent-Friendly | Feature | Benefit for AI Agents | |---------|----------------------| | **TypeScript config** | Strong types guide agents toward correct API usage | | **Deterministic plan/apply** | Agents can preview changes before applying | | **Object references** | Auto-dependency inference reduces reasoning load | | **Abstract types** | `'small'` → `t3.small` — agents don't need provider-specific knowledge | | **JSON state files** | Machine-readable, easy to parse and analyze | | **Simple CLI** | `kyku plan`, `kyku apply` — clear command semantics | | **llms.txt** | Discoverable documentation for agent context | ## Typical Agent Workflow ``` 1. Agent reads llms.txt and relevant docs 2. Agent writes infrastructure.ts with resource definitions 3. Agent runs: kyku plan → reads output → iterates 4. Agent runs: kyku apply → reads output → confirms 5. Agent runs: kyku output → reads results ``` ## Agent-Optimized Prompts ### Creating Infrastructure ``` Write an Kyku config that creates: - A VPC in us-east with 2 AZs - A web server (small, ubuntu-24.04) with SSH access - A PostgreSQL database (medium, version 16) - A load balancer on port 80 targeting the web server Use object references for dependencies. ``` ### Fixing Drift ``` Run kyku plan and check for unexpected changes. If the plan shows drift, identify which resource changed and what caused it (manual console change, config update). ``` ### Cleanup ``` Read the state file and identify all resources. Run kyku destroy --auto-approve to clean up. Verify with kyku plan (should show no resources). ``` ## Agent Resources | Resource | Description | |----------|-------------| | [llms.txt](/agents/llms-txt/) | Discoverable documentation standard | | [Contributor Context](/agents/contributor-context/) | AGENTS.md conventions for provider development | | [Docs MCP Server](/agents/mcp-server/) | MCP server for runtime agent queries | ## Design Principles for Agent UX 1. **Config over CLI flags** — Complex operations go in TypeScript, not CLI args. 2. **Predictable output** — Plan output is always parseable; use `--json` for machine reading. 3. **Safe defaults** — `plan` never modifies resources; `apply` always requires confirmation. 4. **Error messages** — Include the resource ID and expected fix in every error. 5. **Idempotent** — Running `apply` twice produces the same result. --- # llms.txt Kyku provides a `llms.txt` file following the [llms.txt](https://llmstxt.org) standard, giving AI coding agents discoverable, structured documentation about the project. ## What is llms.txt? The `llms.txt` standard defines a convention where projects place a text file at their root URL that AI agents can fetch to learn about the project. It's like `robots.txt` but for LLMs — it tells agents where to find the most relevant documentation. ## Kyku's llms.txt Kyku exposes its `llms.txt` at `https://kyku.cloud/llms.txt` with the following structure: ``` # Kyku > Universal infrastructure provisioning tool. Write TypeScript, deploy anywhere. ## Docs - Getting Started: https://kyku.cloud/getting-started/overview/ - Quickstart: https://kyku.cloud/getting-started/quickstart/ - Concepts: https://kyku.cloud/concepts/resources/ - Config Reference: https://kyku.cloud/config/overview/ - Provider Index: https://kyku.cloud/providers/ - CLI Reference: https://kyku.cloud/cli/commands/ - Guide: Deploy Web App: https://kyku.cloud/guides/deploy-web-app/ - Guide: CI/CD: https://kyku.cloud/guides/ci-cd/ - Guide: Environments: https://kyku.cloud/guides/environments/ - Internals: Engine: https://kyku.cloud/internals/engine/ - Internals: Dependency Graph: https://kyku.cloud/internals/dependency-graph/ ## Optional - Architecture Overview: https://kyku.cloud/internals/decisions/ - Full API: https://kyku.cloud/llms-full.txt ``` ## How to Use When working with Kyku via an AI coding agent, tell the agent to read `https://kyku.cloud/llms.txt` first: ``` Before working with Kyku, read https://kyku.cloud/llms.txt to understand the project structure and find relevant documentation. ``` Or with tools like `curl`: ```bash curl https://kyku.cloud/llms.txt ``` ## Benefits for AI Agents - **Single entry point**: One URL to fetch for project context - **Prioritized links**: Most important docs listed first - **Minimal tokens**: Concise summaries, not full pages - **Always current**: Auto-generated from the docs site - **Discoverable**: Standard location that agents can check by convention ## Fallback Content For offline use or when the docs site is unavailable, the `llms.txt` content is also included in the repository at `docs/llms.txt` with relative links. --- # Docs MCP Server Kyku provides a Model Context Protocol (MCP) server that exposes documentation as resources and tools for AI coding agents. This allows agents to query documentation at runtime without needing to crawl the docs site. ## What is MCP? The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) is an open standard that lets AI applications connect to external tools and data sources. An MCP server exposes: - **Resources**: Readable data (files, docs, schemas) - **Tools**: Callable functions for actions - **Prompts**: Reusable prompt templates ## Kyku's Docs MCP Server The Kyku docs MCP server provides: ### Resources | Resource URI | Description | |-------------|-------------| | `docs://providers/index` | Provider parity matrix | | `docs://providers/aws` | AWS provider details | | `docs://providers/gcp` | GCP provider details | | `docs://providers/hetzner` | Hetzner provider details | | `docs://providers/digitalocean` | DigitalOcean provider details | | `docs://providers/kubernetes` | Kubernetes provider details | | `docs://internals/engine` | Engine lifecycle | | `docs://internals/dependency-graph` | Graph builder internals | | `docs://internals/diff-engine` | Diff engine internals | | `docs://internals/planner` | Planner internals | | `docs://internals/crypto` | Encryption architecture | | `docs://internals/auth` | Auth modules | | `docs://internals/decisions` | Architecture decision log | | `docs://guides/deploy-web-app` | Deploy web app guide | | `docs://guides/ci-cd` | CI/CD guide | | `docs://guides/environments` | Environment management | | `docs://guides/state-encryption` | State encryption deep dive | | `docs://guides/import-resources` | Resource import guide | | `docs://agents/index` | AI agents overview | | `docs://agents/llms-txt` | llms.txt standard | | `docs://agents/contributor-context` | AGENTS.md conventions | | `docs://roadmap` | Feature roadmap | ### Tools | Tool | Description | |------|-------------| | `search-docs` | Search documentation by query | | `get-provider-details` | Get details for a specific provider | | `get-resource-types` | List all supported resource types | | `get-credential-requirements` | Get credential requirements for a provider | ### Prompts | Prompt | Description | |--------|-------------| | `create-kyku-config` | Generate an Kyku config for common patterns | | `troubleshoot-provider` | Diagnose provider configuration issues | | `migrate-existing-infra` | Plan migration from other IaC tools | ## Usage ### With Claude Desktop Add to your `claude_desktop_config.json`: ```json { "mcpServers": { "kyku-docs": { "command": "bun", "args": ["run", "packages/docs-mcp-server/src/index.ts"] } } } ``` ### With Other MCP Clients The server runs on stdio transport by default: ```bash bun run packages/docs-mcp-server/src/index.ts ``` Or as an HTTP server: ```bash KYKU_MCP_HTTP=true bun run packages/docs-mcp-server/src/index.ts # Listens on http://localhost:3100 ``` ## How Agents Use It When an AI agent needs Kyku documentation during a task: 1. The agent discovers the MCP server via its configuration 2. For specific docs, it reads the relevant resource URI 3. For open-ended questions, it uses `search-docs` 4. For code generation, it uses `create-kyku-config` prompt This avoids the need to: - Crawl the docs site - Hardcode documentation URLs - Parse HTML/markdown manually ## Implementation The MCP server is built with the official `@modelcontextprotocol/sdk` and reads markdown files from the docs site build output. It's maintained in `packages/docs-mcp-server/`. --- # apply Apply the planned changes to your cloud provider. Creates, updates, replaces, and destroys resources as needed. ## Usage ```bash kyku apply [options] ``` ## Options | Flag | Default | Description | |---|---|---| | `-e, --env ` | `default` | Environment workspace | | `-c, --config ` | `./infrastructure.ts` | Path to config file | | `--auto-approve` | `false` | Skip confirmation prompt | | `--dry-run` | `false` | Show plan without making changes | | `-p, --passphrase ` | — | Passphrase for encrypted state | | `--plan ` | — | Load plan from a file | | `--force-stale-plan` | `false` | Apply a saved plan even if state serial or config hash no longer match | | `--target ` | — | Apply matching resources **and their dependency ancestors**. Repeatable. Selector is an exact id, an id glob (`vm-*`), or `tags.key=value` on `resource.tags`. | | `--exclude ` | — | Omit matching resources. Errors if anything left in the graph still depends on them. Same selector forms as `--target`. | | `--no-auto-rollback` | `false` | Leave partial state in place if apply fails. Default is to restore the pre-apply backup (state only — leftover cloud resources are not destroyed). | | `--rollback-destroy` | `false` | After a failed apply restores state, destroy leftover cloud resources that have a verified `providerId`. Prompt unless `--auto-approve`. Unverified ids stay `ORPHAN`. | ## Examples ```bash # Apply with confirmation prompt kyku apply # Apply automatically (skip prompt) kyku apply --auto-approve # Apply to production kyku apply --env=prod --auto-approve # Dry run — show plan without applying kyku apply --dry-run # Apply from a saved plan file kyku plan --out plan.json kyku apply --plan plan.json # Override a stale plan file (serial or config hash mismatch) kyku apply --plan plan.json --force-stale-plan --auto-approve # Apply one resource and the things it depends on kyku apply --target vm-web --auto-approve # Apply every id matching a glob (each match still pulls in ancestors) kyku apply --target 'vm-*' --auto-approve # Apply resources tagged env=prod on resource.tags kyku apply --target tags.env=prod --auto-approve # Skip a resource (fails if something else still depends on it) kyku apply --exclude bucket-old --auto-approve # Apply with a passphrase for state decryption kyku apply --passphrase "your-secure-passphrase" ``` ## Behavior - Resources are created/updated in dependency order (roots first) - Independent resources are created concurrently (controlled by `--parallelism`) - Replace is destroy-then-create - Destroy order is reverse of create order (leaves first) - State is encrypted and saved after each change - If any planned replace is caused by a create-only [`customConfig`](/concepts/custom-config/#create-only-vs-updatable-keys) field rather than a portable field change, the confirmation prompt calls it out separately and requires typing `replace` to proceed, in addition to the normal `[y/N]` prompt — a config value causing a destroy-and-recreate is easy to miss in a longer plan otherwise. Skipped entirely under `--auto-approve`, same as the rest of the confirmation flow. - A saved `--plan` file is schema-validated on load. Invalid JSON or a missing `stateSerial` / `stateLineage` / `configHash` prints a clear error (exit `1`), never a stack trace. - After a successful apply the state serial increments, so applying the same file again is refused unless you pass `--force-stale-plan`. A config edit after `plan --out` is also refused (hash mismatch). - `--target` / `--exclude` scope the change set the same way as `kyku plan` (exact id, id glob, or `tags.key=value`). Applying a targeted plan leaves state for omitted resources unchanged and prints a drift warning. `kyku destroy --target` uses the same selectors but expands dependents (the reverse of plan/apply). - A failed apply restores the pre-apply state backup when at least one mutation was persisted (SIGINT after persist included). Leftover cloud resources are printed as `ORPHAN` and are not destroyed unless you pass `--rollback-destroy` (verified `providerId` only, reverse dependency order, same retry/verify as destroy). `--auto-approve` skips the leftover-destroy prompt. Pass `--no-auto-rollback` to leave partial state in place; [`kyku rollback`](/cli/rollback/) can still restore the backup later. ## Progress Display Kyku shows real-time progress with resource name, action, and status (running/done/failed). Failed resources don't block other independent resources but the overall apply is marked as failed. ## Exit Codes | Code | Meaning | |---|---| | 0 | Success / no changes | | 1 | General error | | 3 | Apply failed | | 4 | Canceled by user | --- # completion Print a completion script for the current CLI. Source it yourself — `kyku completion` never writes your shell rc files. Completes top-level commands and their flags. It does not complete resource IDs from state. ## Usage ```bash kyku completion ``` ## Install (one-liner per shell) ```bash # bash echo 'source <(kyku completion bash)' >> ~/.bashrc ``` ```zsh # zsh echo 'source <(kyku completion zsh)' >> ~/.zshrc ``` ```fish # fish kyku completion fish > ~/.config/fish/completions/kyku.fish ``` Reload the shell (or `source` the rc file) after adding the line. ## Examples ```bash kyku completion bash kyku completion zsh kyku completion fish ``` ## Exit Codes | Code | Meaning | |---|---| | 0 | Script printed | | 1 | Unknown shell | --- # destroy Destroy infrastructure resources tracked in the state file. Resources are destroyed in reverse dependency order (leaves first). State is loaded with the same backend as `plan` / `apply` (`backend` in the config file, otherwise local `.kyku/`). ## Usage ```bash kyku destroy [options] ``` ## Options | Flag | Default | Description | |---|---|---| | `-e, --env ` | `default` | Environment workspace | | `--auto-approve` | `false` | Skip confirmation prompt | | `-p, --passphrase ` | — | Passphrase for encrypted state | | `-c, --config ` | `./infrastructure.ts` | Path to config file | | `--target ` | — | Destroy matching resources **and their dependents** (reverse of plan). Repeatable. Selector is an exact id, an id glob (`vm-*`), or `tags.key=value` on `resource.tags`. | | `--exclude ` | — | Keep matching resources. Errors if the selected set still depends on them. Same selector forms as `--target`. | ## Examples ```bash # Destroy with confirmation prompt kyku destroy # Destroy automatically kyku destroy --auto-approve # Destroy production environment kyku destroy --env=prod --auto-approve # Destroy one leaf (nothing depends on it) kyku destroy --target vm-web --auto-approve # Destroy every id matching a glob (each match still pulls in dependents) kyku destroy --target 'vm-*' --auto-approve # Destroy resources tagged env=prod on resource.tags kyku destroy --target tags.env=prod --auto-approve # Destroy a VPC and everything that depends on it kyku destroy --target vpc-main --auto-approve # Destroy everything except a standalone bucket kyku destroy --exclude bucket-assets --auto-approve ``` ## Behavior - Destroy is routed through `KykuEngine.destroyAll()` - Resources are destroyed in reverse dependency order (dependents first) - `--target` expands **dependents** (the reverse of `plan` / `apply`, which expand ancestors) - `--exclude` errors if anything still selected depends on the excluded resource - Selectors are an exact resource **id**, an id glob (`*` / `?`, not a regex), or `tags.key=value` against `resource.tags` (from the config file when present, otherwise tags stored on the state config). A selector that matches nothing is an error. - Destroy includes retry with backoff (5 attempts, exponential) - After destroy, polls `readState` to confirm resource is gone (up to 10 retries) - Orphaned resources (in state but not in config) are destroyed - Custom resources must be removed from config first (their destroy handler needs the resource instance) ## Exit Codes | Code | Meaning | |---|---| | 0 | All selected resources destroyed | | 1 | General error | | 3 | Destroy failed | | 4 | Canceled by user | --- # doctor Run a pass/fail table of local environment checks. `kyku doctor` never writes state, never touches `.kyku.lock`, and never mutates cloud resources. ## Usage ```bash kyku doctor [options] ``` ## Checks | Check | Pass when | Fail when | |---|---|---| | Bun | Runtime is present and `>= 1.0.0` | Missing or too old | | config | `infrastructure.ts` loads, or no config file (providers skipped) | Config exists but cannot be loaded | | ` credentials` | Required env var is set **and** one cheap authenticated read succeeds | Named variable missing, file missing, or credentials rejected | | state | No state file, or `.kyku/state..json` parses | JSON / schema error | | lock | No lock, or lock is fresh with a live PID on this host | Stale lock (age or dead PID). Prints `pid`, `host`, and `age` | Missing tokens are named (`HCLOUD_TOKEN is not set`) instead of showing up later as a 403. Provider probes (one GET / STS / token fetch each): | Provider | Presence | Validity | |---|---|---| | Hetzner | `HCLOUD_TOKEN` | `GET /v1/locations` | | DigitalOcean | `DIGITALOCEAN_TOKEN` | `GET /v2/account` | | AWS | `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY`, or `INFRAM_AWS_ROLE_ARN` | STS `GetCallerIdentity` | | GCP | `GOOGLE_APPLICATION_CREDENTIALS` (file must exist), or workload-identity env vars | Obtain an access token | | Kubernetes | `KUBECONFIG` (or `~/.kube/config`) | File exists and looks like a kubeconfig | Doctor does not check quotas, auto-fix anything, or send telemetry. ## Options | Flag | Default | Description | |---|---|---| | `-c, --config ` | `./infrastructure.ts` | Config used to decide which providers to probe | | `-e, --env ` | `default` | Environment workspace (state + lock) | | `--state-dir ` | `.kyku` | State directory | ## Exit Codes | Code | Meaning | |---|---| | 0 | Every check passed (typically well under 5 seconds) | | 1 | At least one check failed | ## Examples ```bash kyku doctor kyku doctor --config=./staging.ts --env=staging ``` --- # Global Flags Global flags apply to all `kyku` commands and can be specified before or after the subcommand. ## Global Flags | Flag | Description | |---|---| | `--verbose` | Resource lifecycle events (`running` / `done` / `failed` per change) | | `--debug` | Provider HTTP method, redacted path, status, and duration. Request/response bodies are never logged. | | `--parallelism ` | Limit concurrent operations (default: unlimited) | ## Usage ```bash # Verbose logging kyku --verbose plan # Debug mode kyku --debug apply # Limit parallelism to 3 concurrent operations kyku --parallelism 3 apply # Combine flags kyku --verbose --parallelism 5 plan ``` ## Plan/Apply Options These flags apply to `plan`, `apply`, `destroy`, and related commands: | Flag | Default | Description | |---|---|---| | `--env ` | `default` | Environment workspace (dev, staging, prod) | | `--config ` | `./infrastructure.ts` | Config file path | | `--auto-approve` | `false` | Skip confirmation prompt | | `--dry-run` | `false` | Show plan without applying | | `--passphrase ` | — | Passphrase for encrypted state | | `--json` | `false` | `plan`: print the redacted plan as JSON. Also used by `output` / `schema`. | | `--out ` | — | Save plan to a file (plan command) | | `--plan ` | — | Load plan from a file (apply command) | | `--force-stale-plan` | `false` | Apply a saved plan even if state serial or config hash no longer match | | `--target ` | — | `plan`/`apply`: matching resources plus ancestors. `destroy`: matching resources plus dependents. Selector is an exact id, an id glob (`vm-*`), or `tags.key=value`. Repeatable. | | `--exclude ` | — | Omit matching resources (id, glob, or `tags.key=value`). Errors if live dependents remain. | ## Exit Codes | Code | Meaning | |---|---| | 0 | Success / no changes | | 1 | General error | | 2 | Validation error | | 3 | Apply failed | | 4 | Canceled by user | --- # graph Display the dependency graph of your infrastructure config as an ASCII tree or Graphviz DOT output. ## Usage ```bash kyku graph [options] ``` ## Options | Flag | Default | Description | |---|---|---| | `-c, --config ` | `./infrastructure.ts` | Path to config file | | `--dot` | `false` | Output in Graphviz DOT format | ## Examples ```bash # Show ASCII dependency tree kyku graph # Output DOT format for external rendering kyku graph --dot > graph.dot dot -Tsvg graph.dot > graph.svg # Graph from a specific config kyku graph -c ./my-config.ts ``` ## ASCII Output ``` Vpc (vpc-main) [level 0] └── depended on by: SecurityGroup (sg-web), Vm (vm-web) SecurityGroup (sg-web) [level 1] └── depends on: Vpc (vpc-main) └── depended on by: Vm (vm-web) Vm (vm-web) [level 2] └── depends on: Vpc (vpc-main), SecurityGroup (sg-web) ``` ## DOT Output The DOT format assigns distinct colors and shapes per resource type: | Type | Shape | Color | |---|---|---| | Vpc | folder | blue | | Vm | box | green | | SecurityGroup | hexagon | orange | | LoadBalancer | parallelogram | purple | | Database | cylinder | red | ## Verification Use the graph to verify: - No dangling references — every `depends on` ID appears as a node - No orphan resources (unless truly standalone) - Create order is correct (level 0 = roots created first) - Destroy order is the reverse of creation - No cycles (throws `CycleError`) --- # import Adopt an existing cloud resource into Kyku management without recreating it. Creates a state entry for the resource so Kyku can manage it going forward. ## Usage ```bash kyku import [options] ``` ## Arguments | Argument | Description | |---|---| | `resourceType` | Resource type (e.g., `Vpc`, `Vm`, `Bucket`) | | `cloudId` | Cloud provider resource ID | ## Options | Flag | Default | Description | |---|---|---| | `-n, --name ` | — | Name for the resource in Kyku state | | `-c, --config ` | `./infrastructure.ts` | Path to config file | | `-e, --env ` | `default` | Environment workspace | | `-p, --passphrase ` | — | Passphrase for encrypted state | ## Examples ```bash # Import an existing VPC kyku import Vpc vpc-0abc123def456 # Import a VM with a custom name kyku import Vm i-0abc123456 --name=web-server # Import into a specific environment kyku import Vpc vpc-0abc123def456 --env=prod # Import from a custom config kyku import Bucket my-s3-bucket -c ./infrastructure.ts ``` ## Behavior 1. Creates a stub resource with the given type and name 2. Calls `provider.readState(stub, cloudId)` to fetch current state 3. Writes the cloud state to the state file 4. Resource is now tracked and will appear in `kyku plan` and `kyku apply` ## Supported Types All resource types are supported: `Vpc`, `Vm`, `Bucket`, `DnsZone`, `SecurityGroup`, `LoadBalancer`, `Database`, `Identity`, `Role`, `SshKey`, `Volume`, `Queue`, `Cache`, `KubernetesCluster`, and more. --- # init Scaffold a new `infrastructure.ts` file with a starter template. ## Usage ```bash kyku init [options] ``` ## Options | Flag | Default | Description | |---|---|---| | `-p, --provider ` | `aws` | Cloud provider (`aws`, `gcp`, `hetzner`, `digitalocean`) | | `-o, --output ` | `./infrastructure.ts` | Output file path | ## Examples ```bash # Scaffold an AWS project kyku init --provider=aws # Scaffold a GCP project to a specific path kyku init --provider=gcp --output=./gcp-config.ts # Scaffold a Hetzner project kyku init --provider=hetzner ``` The generated file includes a basic VPC + VM example with the chosen provider. Edit it to match your infrastructure needs, then run `kyku plan`. --- # output Display computed outputs from provisioned resources, such as IP addresses, endpoints, and DNS names. ## Usage ```bash kyku output [options] ``` ## Options | Flag | Default | Description | |---|---|---| | `-e, --env ` | `default` | Environment workspace | | `--show-secrets` | `false` | Decrypt and show sensitive values | | `--json` | `false` | Output as JSON | | `-p, --passphrase ` | — | Passphrase for encrypted state | ## Examples ```bash # Show all outputs kyku output # Show in JSON format kyku output --json # Show decrypted secrets kyku output --show-secrets ``` ## JSON Output ```json { "vm-web": { "instanceId": "i-0abc123456", "publicIp": "203.0.113.42", "privateIp": "10.0.1.5" }, "db-main": { "endpoint": "app-db.c9abc123456.us-east-1.rds.amazonaws.com", "port": 5432, "databaseName": "appdb" }, "lb-web": { "dnsName": "web-lb-1234567890.us-east-1.elb.amazonaws.com", "listenerPorts": [80, 443] } } ``` ## Secrets Without `--show-secrets`, sensitive outputs are shown as `[encrypted]`. Pass `--passphrase` (or set `KYKU_PASSPHRASE`) and `--show-secrets` to decrypt them. --- # plan Show a diff of what will be created, updated, replaced, or destroyed. The plan compares your config against the current state file. ## Usage ```bash kyku plan [options] ``` ## Options | Flag | Default | Description | |---|---|---| | `-e, --env ` | `default` | Environment workspace | | `-c, --config ` | `./infrastructure.ts` | Path to config file | | `-p, --passphrase ` | — | Passphrase for encrypted state | | `--out ` | — | Save plan to a file for later application | | `--json` | `false` | Print the redacted plan as JSON (same change set as the human renderer) | | `--prefix

` | — | Opt-in deploy prefix for new cloud names | | `--detailed-exitcode` | `false` | `0` no changes, `1` error, `2` changes (CI drift gate). Validation also exits `1` when this flag is set. Honored together with `--json`. | | `--target ` | — | Plan matching resources **and their dependency ancestors**. Repeatable. Selector is an exact id, an id glob (`vm-*`), or `tags.key=value` on `resource.tags`. | | `--exclude ` | — | Omit matching resources. Errors if anything left in the graph still depends on them. Same selector forms as `--target`. | | `--migrate-state` | `false` | Upload local `.kyku` state to the configured remote backend, then plan from remote. Refuses if remote state already exists. | | `--force-migrate` | `false` | Same as `--migrate-state` but overwrites remote state if it exists. | ## Examples ```bash # Preview changes for default environment kyku plan # Preview changes for production kyku plan --env=prod # Save plan to a file kyku plan --out plan.json # Machine-readable plan (secrets redacted) kyku plan --json kyku plan --json --detailed-exitcode # Load config from a custom path kyku plan --config=./staging.ts # Fail CI when the plan is not a no-op kyku plan --detailed-exitcode # exit 0 = clean, 2 = drift, 1 = error # Plan a leaf and the resources it depends on kyku plan --target vm-web # Plan every id matching a glob (each match still pulls in ancestors) kyku plan --target 'vm-*' # Plan resources tagged env=prod on resource.tags (not provider labels) kyku plan --target tags.env=prod # Skip a resource (fails if something else still depends on it) kyku plan --exclude bucket-old # Show what would change without a passphrase (secrets shown as [encrypted]) kyku plan # Opt-in: upload local state to the remote backend, then plan from remote kyku plan --migrate-state kyku plan --force-migrate ``` ## Plan Output ``` Plan: 3 to create, 1 to update, 0 to replace, 1 to destroy + Vpc (vpc-main) region: "us-east" cidr: "10.0.0.0/16" + SecurityGroup (sg-web) ingress: [...] + Vm (vm-web) image: "ubuntu-22.04" instanceType: "small" - SecurityGroup (sg-old) (removed from config) ~ Vm (vm-existing) instanceType: "small" → "medium" ``` ## Plan File Use `--out` to serialize the plan to JSON for review or later application. `--json` prints the same redacted document to stdout. Saved plans embed `stateSerial`, `stateLineage`, and a `configHash` of the desired graph. `kyku apply --plan` refuses the file if the state serial no longer matches or the config has changed. Re-run `kyku plan`, or pass `--force-stale-plan`. ```bash kyku plan --out plan.json kyku apply --plan plan.json ``` Sensitive property values are stored as `sensitive:` placeholders, never in plaintext. `--target` / `--exclude` accept an exact resource **id**, an id glob (`*` / `?`, not a regex), or `tags.key=value` against `resource.tags` only (not provider labels, not `name`). A selector that matches nothing is an error. `--target` always includes ancestors (the dependency closure). A targeted `--out` file records the selector strings in `targets` so `kyku apply --plan` cannot treat a partial plan as a full one. Resources left out print a drift warning on stderr. `kyku destroy --target` uses the same selectors but expands dependents instead of ancestors. --- # refresh Update Kyku state to match the current state of cloud resources. This detects drift caused by manual changes or out-of-band modifications. ## Usage ```bash kyku refresh [options] ``` ## Options | Flag | Default | Description | |---|---|---| | `-e, --env ` | `default` | Environment workspace | | `-c, --config ` | `./infrastructure.ts` | Path to config file | | `-p, --passphrase ` | — | Passphrase for encrypted state | ## Examples ```bash # Refresh default environment kyku refresh # Refresh production environment kyku refresh --env=prod ``` ## Behavior 1. Loads the current state file 2. For each resource, calls `provider.readState()` with the stored provider ID 3. Updates the state with the current cloud config and outputs 4. Does not create, modify, or destroy any resources Use `kyku plan` after refresh to see if the config now differs from the updated state. --- # rollback Restore the environment's state file from the newest `state..json.bak.` written before a save. Rollback is a **local state-file operation** — it never calls a cloud API and never destroys leftover resources. ## Usage ```bash kyku rollback [options] ``` ## Options | Flag | Default | Description | |---|---|---| | `-e, --env ` | `default` | Environment workspace | | `--state-dir

` | `.kyku` | State directory | | `--yes` | `false` | Skip confirmation prompt | ## Examples ```bash # Preview orphans, then confirm kyku rollback # After a failed apply, restore the pre-apply backup without prompting kyku rollback --yes # Production workspace kyku rollback --env=prod --yes ``` ## Behavior - Picks the newest `.bak.` for the environment. If none exists, the command exits 1 with a clear error. - Writes the backup over the live state file without bumping `serial`, so the restored file matches the backup. - Resources present in the discarded state but absent from the backup are printed as `ORPHAN` with their provider ID. They stay in the cloud unless the failed apply used [`--rollback-destroy`](/cli/apply/). Import them with [`kyku import`](/cli/import/) or delete them in the cloud console. - Confirmation is required unless `--yes` is set. - There is no automatic rollback on apply failure, no multi-step undo, and no remote-backend rollback. ## After a failed apply `kyku apply` copies the current state to a timestamped backup before mutating it. If the apply fails partway, `kyku rollback` puts the state file back to that pre-apply backup. Cloud resources created during the failed apply become orphans. ## Exit Codes | Code | Meaning | |---|---| | 0 | Restored, or the user aborted the confirmation | | 1 | No backup, invalid backup, or other error | --- # schema Show what Kyku knows about a resource type's [`customConfig`](/concepts/custom-config/) on a given provider: which keys are lifecycle-aware (safely updatable in place) vs. create-only, which keys are reserved (owned by a first-class field), and where to find the typed alias for IDE autocomplete. This reads the same `CustomConfigSchema` metadata the planner uses to decide `update` vs. `replace` — it doesn't call any cloud API and needs no credentials beyond what the provider's constructor requires. ## Usage ```bash kyku schema --provider [options] ``` ## Options | Flag | Default | Description | |---|---|---| | `-p, --provider ` | — | Cloud provider (`aws`, `gcp`, `hetzner`, `digitalocean`, `kubernetes`) — required | | `--json` | `false` | Output as JSON instead of a formatted summary | ## Examples ```bash kyku schema Vpc --provider aws ``` ``` customConfig schema: Vpc on aws Typed alias (if exported): AwsVpcCustomConfig — import from @kykucloud/aws Lifecycle-aware keys: InstanceTenancy: string, lifecycle=create read behavior: reflects live cloud state EnableDnsHostnames: boolean, lifecycle=both read behavior: reflects live cloud state EnableDnsSupport: boolean, lifecycle=both read behavior: reflects live cloud state EnableNetworkAddressUsageMetrics: boolean, lifecycle=both read behavior: reflects live cloud state subnet: object, lifecycle=create read behavior: reflects live cloud state natGateway: object, lifecycle=create read behavior: reflects live cloud state routeTable: object, lifecycle=create read behavior: reflects live cloud state internetGateway: object, lifecycle=create read behavior: reflects live cloud state Any other key defaults to lifecycle="create": a change plans a replace (destroy + recreate). Unknown keys are never rejected outright — they pass through with a plan-time warning. ``` Every lifecycle-aware key also prints a `read behavior` line. Most reflect live cloud state, but a field the cloud's read response never returns at all (write-only) instead echoes back whatever you set — the diff engine can't otherwise tell an applied write-only change from a pending one: ```bash kyku schema Vm --provider hetzner ``` ``` customConfig schema: Vm on hetzner Typed alias (if exported): HetznerVmCustomConfig — import from @kykucloud/hetzner Lifecycle-aware keys: automount: boolean, lifecycle=create read behavior: echoes desired value (write-only — the cloud never returns this field, so drift can't be confirmed by reading) placement_group: number, lifecycle=both read behavior: reflects live cloud state volumes: array, lifecycle=create read behavior: reflects live cloud state backups: boolean, lifecycle=both read behavior: reflects live cloud state protection: object, lifecycle=both read behavior: reflects live cloud state dns_ptr: string, lifecycle=both read behavior: reflects live cloud state Any other key defaults to lifecycle="create": a change plans a replace (destroy + recreate). Unknown keys are never rejected outright — they pass through with a plan-time warning. ``` ```bash kyku schema Vm --provider aws --json ``` ```json { "provider": "aws", "resource": "Vm", "schema": { "keys": { "DisableApiTermination": { "type": "boolean", "lifecycle": "both" }, "DisableApiStop": { "type": "boolean", "lifecycle": "both" }, "InstanceInitiatedShutdownBehavior": { "type": "string", "lifecycle": "both" }, "SourceDestCheck": { "type": "boolean", "lifecycle": "both" }, "Monitoring": { "type": "object", "lifecycle": "both" }, "MetadataOptions": { "type": "object", "lifecycle": "both" } } } } ``` ## Notes - A resource type with no registered schema entries still accepts `customConfig` — it's an untyped passthrough, not an unsupported feature. The command says so explicitly rather than erroring. - `reserved` keys shown here (if any) are a hard plan-time error to set, since they collide with a first-class field Kyku already manages — see [Custom Config](/concepts/custom-config/#unknown-keys) for how collisions and unknown keys are handled. - The default lifecycle Kubernetes reports is `both`, not `create` — every typed Kubernetes resource applies via server-side apply, which is always safe to re-run in place, so `customConfig` changes there plan an update rather than a replace by default. --- # state Subcommands for inspecting and managing resources in the Kyku state file. ## Usage ```bash kyku state [options] ``` ## Subcommands ### state list List all resources tracked in state. ```bash kyku state list [options] ``` | Flag | Default | Description | |---|---|---| | `-e, --env ` | `default` | Environment workspace | Example output: ``` Resources in state (env: default): vpc-main Vpc providerId: vpc-0abc123 vm-web Vm providerId: i-0def456 sg-web SecurityGroup providerId: sg-0ghi789 ``` ### state show Show details of a specific resource in state. ```bash kyku state show [options] ``` | Flag | Default | Description | |---|---|---| | `-e, --env ` | `default` | Environment workspace | | `-p, --passphrase ` | — | Passphrase for encrypted state | Example: ```bash kyku state show vm-web ``` Output shows the full state entry: type, provider, providerId, config, dependencies, outputs, timestamps. ### state rm Remove a resource from state. Does **not** destroy the cloud resource. ```bash kyku state rm [options] ``` | Flag | Default | Description | |---|---|---| | `-e, --env ` | `default` | Environment workspace | Example: ```bash kyku state rm vm-web ``` **Warning:** This removes the resource from Kyku's tracking only. The cloud resource continues to exist and will need to be cleaned up manually. ### state mv Rename a resource id in the state file. Does **not** call the cloud. Dependent `dependencies` entries are rewritten to the new id. Stored configs are not walked except for a top-level `config.id` that matches the old id. ```bash kyku state mv [options] ``` | Flag | Default | Description | |---|---|---| | `-e, --env ` | `default` | Environment workspace | | `-p, --passphrase ` | — | Passphrase for encrypted state | Example: ```bash # Config id changed from vpc-old to vpc-new; keep state aligned kyku state mv vpc-old vpc-new ``` After this, `kyku plan` against the renamed config is a no-op (zero changes). Missing source or a target that already exists exits 1. A timestamped backup is written under `.kyku/` before the file is mutated. Encrypted state works with `--passphrase`. There is no cross-file move and no bulk/glob rename. ### state push Upload local `.kyku` state into the remote backend declared in the config file (S3, GCS, or Spaces). Verifies a round-trip, then renames the local file to `.migrated` (it is not deleted). ```bash kyku state push [options] ``` | Flag | Default | Description | |---|---|---| | `-e, --env ` | `default` | Environment workspace | | `-c, --config ` | `./infrastructure.ts` | Config file (must declare `backend`) | | `--force` | `false` | Overwrite remote state if it already exists | Refuses when the remote object already exists unless `--force` is set. There is no `state pull` / `state sync`, and `plan`/`apply` do not auto-migrate. ## Examples ```bash # List all resources kyku state list # List resources in production kyku state list --env=prod # Show a specific resource with decrypted secrets kyku state show db-main --passphrase "your-passphrase" # Remove a resource from state (NOT destroy) kyku state rm vm-web # Rename a resource id after a config id change kyku state mv vpc-old vpc-new # Move local state to the configured remote backend kyku state push ``` --- # taint / untaint `kyku taint ` marks a resource already in state so the next `kyku plan` shows a **replace** (destroy + create), even when config matches the cloud. `kyku untaint ` clears that marker. The flag is stored on the state resource and survives save/load. There is no `--replace` plan flag, no taint-by-tag/glob, and no cascade-taint. ## Usage ```bash kyku taint [options] kyku untaint [options] ``` | Flag | Default | Description | |---|---|---| | `-e, --env ` | `default` | Environment workspace | ## Examples ```bash # Force the next apply to rebuild this VM kyku taint vm-web # Change your mind kyku untaint vm-web ``` Unknown ids exit 1 and list the ids currently in state. A timestamped state backup is written under `.kyku/` before the file is mutated. A successful apply of the replace writes the resource back without the taint marker. --- # test Run policy assertions against the plan Kyku would generate from your config and local state. `kyku test` never calls the cloud. See [ADR-001](/internals/adr-001-policy-tests/) for why this is a typed TypeScript API rather than Gherkin. ## Usage ```bash kyku test [options] ``` ## Policy file Default path: `./infrastructure.test.ts`. Default-export a `Policy` or `Policy[]`: ```typescript import { definePolicy, findOpenIngress } from '@kykucloud/core' export default [ definePolicy('ssh-not-world-open', ({ resources, fail }) => { for (const resource of resources) { for (const hit of findOpenIngress(resource, { port: 22 })) { fail(`${resource.id}: SSH open to ${hit.source}`, resource.id) } } }), ] ``` `ctx.plan` is the offline plan (creates/updates/destroys from local state, no `readState`). `ctx.resources` is the desired graph after auto-subnet injection. There is no OPA/Rego, no watch mode, and no coverage report. ## Options | Flag | Default | Description | |---|---|---| | `-c, --config ` | `./infrastructure.ts` | Config file | | `-t, --test ` | `./infrastructure.test.ts` | Policy file | | `-e, --env ` | `default` | Environment workspace | | `--state-dir ` | `.kyku` | State directory (read-only) | ## Exit Codes | Code | Meaning | |---|---| | 0 | Every policy passed | | 1 | A policy called `fail()`, or the policy/config file could not be loaded | ## Examples The repo sample in `examples/policy/`: ```bash # open SSH (0.0.0.0/0) fails kyku test -c examples/policy/open-ssh.ts -t examples/policy/ssh.test.ts # narrowed source passes kyku test -c examples/policy/narrow-ssh.ts -t examples/policy/ssh.test.ts ``` --- # validate Validate your infrastructure config file without making any cloud API calls. Catches structural errors, invalid resource types, and missing required fields. ## Usage ```bash kyku validate [options] ``` ## Options | Flag | Default | Description | |---|---|---| | `-c, --config ` | `./infrastructure.ts` | Path to config file | | `-e, --env ` | `default` | Environment workspace | | `-p, --passphrase ` | — | Passphrase for encrypted state | ## Examples ```bash # Validate the default config kyku validate # Validate a specific config file kyku validate --config=./staging.ts # Validate with environment kyku validate --env=prod ``` ## Exit Codes | Code | Meaning | |---|---| | 0 | Config is valid | | 1 | General error | | 2 | Validation error | --- # Abstract Types Kyku uses abstract types so your config works across providers without modification. Each provider maps abstract values to their cloud-native equivalents. ## Instance Types These are the portable sizes. `micro`–`xlarge` stay on the original burst/shared families. `2xlarge` and up step onto larger general-purpose machines. | Abstract | ~vCPU / RAM | AWS | GCP | Hetzner | DO | |---|---|---|---|---|---| | `micro` | 1 / 1 GB | `t3.micro` | `e2-micro` | `cax11` | `s-1vcpu-1gb` | | `small` | 2 / 2 GB | `t3.small` | `e2-small` | `cax21` | `s-1vcpu-2gb` | | `medium` | 2 / 4 GB | `t3.medium` | `e2-medium` | `cax31` | `s-2vcpu-4gb` | | `large` | 2 / 8 GB | `t3.large` | `e2-standard-4` | `cax41` | `s-4vcpu-8gb` | | `xlarge` | 4 / 16 GB | `t3.xlarge` | `e2-standard-8` | `ccx13` | `s-8vcpu-16gb` | | `2xlarge` | 8 / 32 GB | `t3.2xlarge` | `e2-standard-16` | `ccx33` | `g-8vcpu-32gb` | | `4xlarge` | 16 / 64 GB | `m6i.4xlarge` | `e2-standard-32` | `ccx43` | `g-16vcpu-64gb` | | `8xlarge` | 32 / 128 GB | `m6i.8xlarge` | `n2-standard-48` | `ccx53` | `g-32vcpu-128gb` | Exact vCPU and memory still follow the provider SKU. The ~ column is only a portable hint. ### Provider-Specific Overrides To pin a specific provider-native type, use a provider map: ```typescript instanceType: { aws: 'm5.large', gcp: 'n2-standard-2', hetzner: 'ccx13', } ``` Unlisted providers fall back to the abstract mapping. ## Images | Abstract | AWS | GCP | Hetzner | DO | |---|---|---|---|---| | `ubuntu-22.04` | `ami-0c55b…` (varies by region) | `ubuntu-os-cloud/ubuntu-2204-lts` | `ubuntu-22.04` | `ubuntu-22-04-x64` | | `ubuntu-24.04` | `ami-0e86e2…` (varies by region) | `ubuntu-os-cloud/ubuntu-2404-lts` | `ubuntu-24.04` | `ubuntu-24-04-x64` | | `debian-12` | `ami-0c…` (varies by region) | `debian-cloud/debian-12` | `debian-12` | `debian-12-x64` | Custom images use the provider-native identifier directly: ```typescript image: 'ami-0c55b159cbfafe1f0' // Passes through unmapped on AWS ``` ## Regions | Abstract | AWS | GCP | Hetzner | DO | |---|---|---|---|---| | `us-east` | `us-east-1` | `us-east1` | `us-east` | `nyc1` | | `us-west` | `us-west-2` | `us-west2` | `us-west` | `sfo3` | | `eu-central` | `eu-central-1` | `europe-west1` | `eu-central` | `fra1` | | `eu-west` | `eu-west-1` | `europe-west2` | `eu-west` | `lon1` | | `ap-southeast` | `ap-southeast-1` | `asia-southeast1` | — | `sgp1` | ### Region Overrides ```typescript region: { aws: 'eu-west-2', gcp: 'europe-west2-a', } ``` ### Database Instance Types Databases use a separate instance type map: | Abstract | AWS RDS | GCP Cloud SQL | |---|---|---| | `micro` | `db.t3.micro` | `db-f1-micro` | | `small` | `db.t3.small` | `db-g1-small` | | `medium` | `db.t3.medium` | `db-custom-2-8192` | | `large` | `db.t3.large` | `db-custom-4-16384` | | `xlarge` | `db.t3.xlarge` | `db-custom-8-32768` | | `2xlarge` | `db.t3.2xlarge` | `db-custom-16-65536` | | `4xlarge` | `db.m6i.4xlarge` | `db-custom-32-131072` | | `8xlarge` | `db.m6i.8xlarge` | `db-custom-48-196608` | --- # Custom Config Every Kyku resource accepts an optional `customConfig` field: a plain object of provider-native values that get merged directly into the underlying cloud API request. It's the escape hatch for the long tail of provider-specific options Kyku's portable resource shape doesn't (and won't) model as first-class fields. ```typescript import { Vpc } from '@kykucloud/types' import type { AwsVpcCustomConfig } from '@kykucloud/aws' const vpc = new Vpc({ id: 'main-vpc', name: 'main', cidr: '10.0.0.0/16', region: 'us-east', customConfig: { InstanceTenancy: 'dedicated', } satisfies AwsVpcCustomConfig, }) ``` ## Portable fields vs. customConfig A resource like `Vpc` or `Vm` has a small set of **portable, first-class fields** — `name`, `cidr`, `instanceType`, `image`, `network`, and so on — that work identically across every provider. `customConfig` is the opposite: **provider-native fields**, shaped exactly like that provider's own create/update request, that only make sense for the provider you're deploying to. Kyku merges `customConfig` directly into the request it sends to the cloud API. It doesn't reinterpret or rename anything: values use the provider's own field names and casing (`CidrBlock` for AWS, `ip_range` for Hetzner/DigitalOcean, `ipCidrRange` for GCP), because the whole point is to reach fields Kyku's abstraction doesn't cover. ## Typed aliases Each provider package exports typed aliases so you get autocomplete and a type error instead of a typo silently doing nothing: | Provider | Source | Example | |---|---|---| | AWS | `@aws-sdk/client-*` command input types | `AwsVpcCustomConfig`, `AwsVmCustomConfig` (from `@kykucloud/aws`) | | GCP | `@google-cloud/compute` request/resource types | `GcpVpcCustomConfig`, `GcpVmCustomConfig` (from `@kykucloud/gcp`) | | Hetzner | Generated from the official `cloud.spec.json` OpenAPI document | `HetznerVpcCustomConfig`, `HetznerVmCustomConfig` (from `@kykucloud/hetzner`) | | DigitalOcean | Generated from the official `digitalocean/openapi` spec | `DigitaloceanVpcCustomConfig`, `DigitaloceanVmCustomConfig` (from `@kykucloud/digitalocean`) | Each alias is the provider's real request type minus the fields Kyku's portable contract already owns (`name`, `cidr`, `instanceType`, network/security-group identity, tags, SSH keys, and so on) — so it grows and shrinks with the underlying SDK/API automatically, with no hand-maintained field list to go stale. Apply an alias with `satisfies` rather than a type annotation, so the object literal itself is still checked structurally: ```typescript customConfig: { EbsOptimized: true } satisfies AwsVmCustomConfig ``` Coverage is broadest for AWS (every resource manager has an alias) and GCP (every Compute-backed resource plus most of the rest, via SDK dependencies added purely for their request types — `Identity`/`Role` are the one documented gap, since no clean IAM Admin client library exists). Hetzner and DigitalOcean cover their most commonly customized resources (Vpc, Vm, SecurityGroup, LoadBalancer, plus SshKey for Hetzner and Database/DnsRecord/KubernetesCluster for DigitalOcean); a handful of resources on each (object storage, legacy DNS APIs, synthetic tag-based resources) don't have — and structurally can't usefully have — a generated alias, which is documented per-resource rather than silently missing. Untyped doesn't mean unsupported: the merge/read/update passthrough exists for every resource on every provider regardless of whether a typed alias exists. Run `kyku schema --provider ` to check current coverage for a specific pair rather than relying on this description staying up to date. ## Unknown keys A key with no typed alias, or a key a generated alias doesn't yet know the lifecycle of, is still accepted — Kyku warns and passes it through rather than hard-rejecting it, so a brand-new field the cloud API just shipped isn't blocked by a stale catalog. If the key is a near-miss for a known one (a likely typo), the warning suggests the closest match ("did you mean 'description'?"). Run `kyku validate` to see these warnings — they're informational, not errors, so they don't fail validation. The one thing that *is* a hard error is a `customConfig` key that collides with a portable field Kyku already manages (for example, setting AWS's `CidrBlock` when you also set `cidr` — Kyku doesn't know which one should win, so it refuses to guess). ## Create-only vs. updatable keys Most `customConfig` keys are **create-only**: change one after the resource exists, and the next `kyku plan` shows a `replace` (destroy + recreate), because most cloud request fields aren't safely re-postable in place. A small, explicitly curated set of keys per provider/resource is marked **lifecycle-aware** and plans an in-place `update` instead once the provider manager implements the corresponding update/action API call: - Hetzner Vpc: `expose_routes_to_vswitch` - DigitalOcean Vpc: `description`, `default` - GCP Vpc: `description`, `mtu`, `routingConfig` Everything else defaults to create-only until a manager grows the matching update path. Kubernetes is the one exception to the "unlisted = create-only" default: every typed Kubernetes resource applies via server-side apply (an idempotent PATCH), so there's no field that genuinely requires tearing the object down — `customConfig` changes on Kubernetes resources always plan as an update. Run `kyku schema --provider ` to see exactly which keys are lifecycle-aware for a given resource/provider pair right now, rather than relying on this list staying current — see the [`schema` CLI reference](/cli/schema/). ## Irreversible and one-way changes A lifecycle-aware key means Kyku *can* apply the change in place — it doesn't mean the underlying cloud operation is itself reversible. A few routed keys are one-way on the provider's side; Kyku lets you apply them but doesn't pretend the cloud will let you undo them: - **DigitalOcean Vm `ipv6`**: DigitalOcean supports enabling IPv6 on a running Droplet but not disabling it again. Setting `ipv6: false` after it's already `true` throws rather than silently no-op'ing or attempting an API call DigitalOcean would reject anyway. - **DigitalOcean Vm `resize.disk`**: growing the disk via a resize action is permanent — a Droplet's disk can never be shrunk again afterward. The resize also requires the Droplet to be powered off first; Kyku does not power-cycle it for you, so DigitalOcean rejects the action if the Droplet is running when the plan applies. - **AWS Vpc `InstanceTenancy`**: changing `'dedicated'` to `'default'` only affects instances launched *after* the change — existing dedicated instances stay dedicated. Left create-only rather than modeled as a true in-place toggle, since "in place" would be misleading here. ## Stopped-instance preconditions Some EC2 instance attributes can only change while the instance is stopped (`InstanceType`, enhanced-networking flags, kernel/ramdisk, and similar). Kyku's AWS Vm routing deliberately does **not** implement an automatic stop → modify → restart cycle for these — doing so would mean silently taking a running instance offline mid-plan, which is a materially different (and riskier) operation than every other lifecycle-aware key documented here, all of which apply without any downtime. Attributes in this category are left create-only (a `replace`, not an `update`) rather than wrapped in an implicit reboot; if you need one of them changed in place, stop the instance yourself first. Only attributes AWS supports changing on a *running* instance are routed to `update()` — see `packages/aws/src/resources/vm.ts` for the current list. ## Kubernetes Kubernetes resources accept `customConfig` too. It merges into the same target the pre-existing raw `spec` field already used on `K8sDeployment`/`K8sStatefulSet`/`K8sIngress` — applied *after* `spec`, so `customConfig` wins on any overlapping key. Resources without a `spec` of their own (`K8sConfigMap`, `K8sSecret`) merge `customConfig` into the whole object instead, with identity fields and anything already owned by a first-class field (`data`, `stringData`, `secretType`, …) protected from being overwritten. `K8sManifest` and `K8sHelmRelease` don't accept `customConfig` — the raw manifest or Helm values you hand them *are* already the full provider-native escape hatch, so there's no separate merge target that would mean anything. ## Secrets `customConfig` values are stored in Kyku's state file like any other config — including in plaintext if you put a secret directly in it. Use an Kyku `Secret` reference (or your provider's native secret-reference mechanism) for anything sensitive rather than inlining it into `customConfig`. --- # Dependency Graph Kyku builds a directed acyclic graph (DAG) from your config automatically. The graph determines create and destroy order, enabling safe parallel execution. ## How Edges Are Created The `GraphBuilder` scans every property of every resource for `BaseResource` references: | Reference Type | Edge Created? | Example | |---|---|---| | Object reference | ✅ Yes | `vm.network = myVpc` → `Vm → Vpc` | | Array of objects | ✅ Yes (per element) | `vm.securityGroups = [sg1, sg2]` | | String reference | ❌ No | `vm.network = "vpc-main"` | | Tags / labels | ❌ No | `vm.tags = { env: "prod" }` | | Output values | ✅ Yes | `lbTargets: [{ vm: server.outputs.instanceId }]` | **Always use object references** for properties that represent cloud dependencies. String references bypass the dependency graph and may cause ordering issues. ## Ignored Properties The following properties never create edges: - `type`, `id`, `name`, `provider`, `tags`, `outputs` ## Create Order Root nodes (level 0) are created first. Each level waits for all lower levels to complete: ``` Level 0: Vpc Level 1: SecurityGroup (depends on Vpc) Level 2: Vm (depends on Vpc, SecurityGroup) Level 3: LoadBalancer (depends on Vpc, Vm) ``` Resources at the same level with no mutual dependencies are created in parallel. ## Destroy Order Destroy order is the reverse of create order — leaves first: ``` Level 3: LoadBalancer Level 2: Vm Level 1: SecurityGroup Level 0: Vpc ``` ## Viewing the Graph ### ASCII Tree ```bash kyku graph ``` Output: ``` Vpc (vpc-main) [level 0] └── depends on: └── depended on by: SecurityGroup (sg-web), Vm (vm-web) SecurityGroup (sg-web) [level 1] └── depends on: Vpc (vpc-main) └── depended on by: Vm (vm-web) Vm (vm-web) [level 2] └── depends on: Vpc (vpc-main), SecurityGroup (sg-web) ``` ### Graphviz DOT ```bash kyku graph --dot > graph.dot dot -Tsvg graph.dot > graph.svg ``` The DOT output assigns distinct shapes and colors per resource type for visual inspection. ### Verify Your Config ```bash kyku graph -c ./my-config.ts ``` Check that: - Every `depends on` ID appears as a node in the graph - No orphan resources (unless truly standalone like `SshKey`) - Create order is correct (roots appear first) - Destroy order matches the reverse of levels - No cycles (cycles are detected and throw a `CycleError`) --- # Deploy Prefix Every deployment gets a unique 8-character hex prefix (e.g., `a3f27b1d`) that is prepended to new cloud resource names. This prevents name collisions between deployments sharing the same cloud account. ## How It Works 1. On first `plan` or `apply`, Kyku generates a random 8-character hex prefix. 2. The prefix is stored in `StateFile.metadata.prefix`. 3. All new resources have the prefix prepended to their name: `a3f27b1d-web-server`. 4. The prefix persists across subsequent runs — it's reused from state. ## What Gets Prefixed Resource `name` properties are proxied automatically: ```typescript const vm = new Vm({ id: 'vm-web', name: 'web-server' }); // In provider code: resource.name === "a3f27b1d-web-server" ``` The prefix is applied via a **deep proxy** — nested references also get correct names: ```typescript const vm = new Vm({ network: vpc, // vpc.name is also proxied securityGroups: [sg], // sg.name is also proxied }); ``` ## Existing Resources Resources already tracked in state keep their original cloud names (whether prefixed or not). Only resources **not yet in state** get the prefix applied. This ensures backward compatibility with existing deployments. ## Custom Prefix You can set a custom prefix via `EngineOptions.deployPrefix`: ```typescript const engine = new KykuEngine({ deployPrefix: 'myteam', }); ``` Or use the `--deploy-prefix` flag if exposed by the CLI (otherwise, custom prefixes require using the engine API directly). ## Impact on Providers Providers don't need changes — they receive resources with already-prefixed names via the proxy. All `instanceof` checks work correctly (the proxy preserves the prototype chain). ## Name Length The prefix adds 9 characters (8 hex + dash). Account for this when naming resources: | Provider | Name Limit | Safe Name Length | |---|---|---| | Hetzner | 63 chars | 54 chars | | AWS VPC | varies | ~50 chars | | AWS ALB | 32 chars | 23 chars | | GCP | 62 chars | 53 chars | | DigitalOcean | 255 chars | 246 chars | --- # Environments Kyku supports isolated environments with separate state files, letting you manage dev, staging, and production from the same config. ## How It Works The `--env` flag selects a state file: ``` .kyku/ ├── state.json (default — no --env flag) ├── state.dev.json (--env=dev) ├── state.staging.json (--env=staging) └── state.prod.json (--env=prod) ``` ## Usage ```bash # Development kyku plan --env=dev kyku apply --env=dev # Staging kyku plan --env=staging kyku apply --env=staging # Production kyku plan --env=prod kyku apply --env=prod --auto-approve ``` Each environment maintains its own provider IDs, outputs, and encryption meta. ## State Isolation Resources in different environments are completely isolated: - Different provider credentials per env via the CI/CD pipeline - Different deployment prefixes (each env gets its own prefix) - Independent plan/apply lifecycle ## Promoting Between Environments To promote from dev to staging, apply the same config with a different `--env` flag. Since each environment has its own state, resources are created fresh in the target environment. For incremental promotion, save a plan file and apply it to the target: ```bash # Generate plan for dev kyku plan --env=dev --out plan-dev.json # Apply same plan to staging kyku apply --env=staging --plan plan-dev.json ``` ## CI/CD Environments ```yaml # GitHub Actions jobs: deploy-dev: steps: - run: kyku apply --env=dev --auto-approve deploy-prod: needs: deploy-dev steps: - run: kyku apply --env=prod --auto-approve ``` --- # Outputs Kyku provides type-safe output proxies that resolve at apply time to cloud-computed values. ## How Outputs Work Every `BaseResource` has an `outputs` property — a `Proxy` that returns `OutputValue` placeholder objects: ```typescript const vm = new Vm({ ... }); const placeholder = vm.outputs.instanceId; // → OutputValue { resourceId: "vm-web", key: "instanceId" } // → toString() → "${output:vm-web:instanceId}" ``` These placeholders are resolved to real cloud values when the resource is created (during `apply`). ## Using Outputs Outputs are commonly used in load balancer targets: ```typescript const webServer = new Vm({ id: 'vm-web', name: 'web-server', // ... }); new LoadBalancer({ id: 'lb-web', name: 'web-lb', vpc: vpc, listeners: [{ port: 80, protocol: 'http', targets: [{ vm: webServer.outputs.instanceId, port: 80 }], }], }); ``` The engine resolves `webServer.outputs.instanceId` to the actual instance ID after the VM is provisioned. ## Per-Resource Outputs ### VpcOutputs | Output | Type | Description | |---|---|---| | `vpcId` | `string` | Cloud provider VPC ID | | `cidr` | `string` | VPC CIDR block | | `subnetIds` | `string[]` | Auto-created subnet IDs | ### VmOutputs | Output | Type | Description | |---|---|---| | `instanceId` | `string` | Cloud provider instance ID | | `publicIp` | `string` | Public IP address | | `privateIp` | `string` | Private IP address | | `availabilityZone` | `string` | AZ where instance is deployed | ### DatabaseOutputs | Output | Type | Description | |---|---|---| | `endpoint` | `string` | Database connection endpoint | | `port` | `number` | Database listener port | | `databaseName` | `string` | Initial database name | ### LoadBalancerOutputs | Output | Type | Description | |---|---|---| | `dnsName` | `string` | LB DNS name for CNAME records | | `listenerPorts` | `number[]` | Active listener ports | ### BucketOutputs | Output | Type | Description | |---|---|---| | `bucketName` | `string` | Bucket name | | `endpoint` | `string` | S3-compatible endpoint | | `arn` | `string` | AWS ARN | ### DnsZoneOutputs | Output | Type | Description | |---|---|---| | `zoneId` | `string` | Zone ID | | `nameServers` | `string[]` | Delegation name servers | ## Output Propagation The `OutputPropagator` resolves output references before resources are created: ``` Parse config → Extract output placeholders ({output:id:key}) → Look up placeholder in state → If found, substitute real value → If not found yet, resolve after resource creation ``` ## Custom Resources Custom resources can return arbitrary outputs from their handler: ```typescript handler.apply(async (ctx) => ({ providerId: '...', outputs: { apiKey: ctx.setSecret('sk-...'), endpoint: 'https://api.example.com', }, })); ``` --- # Remote State By default Kyku writes state to `.kyku/state..json`. To share state across machines or CI jobs, set a remote backend in the config file. ## Configure ### S3 ```typescript export default { provider: 'aws', backend: { type: 's3', bucket: 'kyku-state', region: 'us-east-1', key: 'prod/state.json', }, resources: [/* ... */], }; ``` ### GCS ```typescript export default { provider: 'gcp', backend: { type: 'gcs', bucket: 'kyku-state', key: 'prod/state.json', }, resources: [/* ... */], }; ``` ### DigitalOcean Spaces ```typescript export default { provider: 'digitalocean', backend: { type: 'spaces', bucket: 'kyku-state', region: 'nyc3', key: 'prod/state.json', }, resources: [/* ... */], }; ``` Spaces reuses the S3 manager with endpoint `https://.digitaloceanspaces.com`. ### HTTP ```typescript export default { provider: 'hetzner', backend: { type: 'http', address: 'https://state.example.com/prod/state.json', // lockAddress: 'https://state.example.com/prod/state.lock', }, resources: [/* ... */], }; ``` GET retrieves state (404 = empty). PUT writes with `If-Match` / `If-None-Match`. The lock is a second JSON object at `lockAddress` (default `${address}.lock`). Auth is optional until the server returns 401: `KYKU_HTTP_TOKEN` (Bearer) or `KYKU_HTTP_USERNAME` + `KYKU_HTTP_PASSWORD` (Basic). There are no CLI flags for backend settings. `plan`, `apply`, and `destroy` pick the block up from `infrastructure.ts` (or `-c`). ## Behaviour - Load and save target that single object. Writes are conditional (`If-Match` / `If-None-Match` on S3 and HTTP, `ifGenerationMatch` on GCS) so two writers cannot clobber each other. - Each successful save increments `serial`. A write whose in-memory serial no longer matches the remote object is refused. - The lock lives at `${key}.lock` in the same bucket (`s3://…`, `gs://…`, or `spaces://…`) or at `lockAddress` for HTTP, with the same 60s heartbeat and token-conditional stale break as the local lock. - Missing S3/GCS/Spaces credentials are a hard error. HTTP allows unauthenticated calls until the server returns 401. Kyku does **not** write local state instead. ## Migrating existing local state `kyku state push` uploads `.kyku/state..json` to the configured object, verifies the round-trip, and renames the local file to `.migrated`. It refuses if remote state already exists unless you pass `--force`. The next `kyku plan` reads from the remote backend. Plan and apply never auto-migrate. Opt in on plan with `--migrate-state` (error if remote exists) or `--force-migrate` (overwrite). There is no `backend.autoMigrate` and no bare `--force`. A leftover `.migrated` file is never re-uploaded. ## Out of scope There is no `state pull` / `state sync`, no backend-to-backend move, no bucket create, and no state version browsing. ## Credentials ### S3 ```bash export AWS_ACCESS_KEY_ID=... export AWS_SECRET_ACCESS_KEY=... export AWS_SESSION_TOKEN=... # temporary / STS creds # or: AWS_PROFILE, instance role, or AWS_WEB_IDENTITY_TOKEN_FILE ``` ### GCS ```bash export GOOGLE_CLOUD_PROJECT=... export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json # or: gcloud auth application-default login # or: KYKU_GCP_WORKLOAD_PROVIDER + KYKU_GCP_SERVICE_ACCOUNT ``` ### DigitalOcean Spaces ```bash export DO_SPACES_REGION=nyc3 export DO_SPACES_ACCESS_KEY=... export DO_SPACES_SECRET_KEY=... ``` ### HTTP ```bash export KYKU_HTTP_TOKEN=... # or: export KYKU_HTTP_USERNAME=... export KYKU_HTTP_PASSWORD=... ``` --- # Resources Every piece of infrastructure in Kyku is a resource — an instance of a class extending `BaseResource`. ## BaseResource ```typescript abstract class BaseResource { abstract readonly type: ResourceType readonly id: string readonly name: string readonly provider?: string readonly tags?: Record readonly outputs: OutputAccessor } ``` | Property | Description | |---|---| | `id` | Logical identifier used for state tracking and dependency resolution. Auto-generated UUID if omitted. | | `name` | Cloud resource name. May be prefixed automatically (see Deploy Prefix). | | `type` | Discriminated union tag — `'Vpc'`, `'Vm'`, `'SecurityGroup'`, etc. | | `provider` | Override the default provider for multi-provider configs. | | `tags` | Key-value metadata mapped to provider-native labels/tags. | | `outputs` | Proxy for resolved cloud outputs (IPs, ARNs, endpoints). | ## Resource Type Union All supported resource types: `Vpc` · `Subnet` · `Vm` · `SecurityGroup` · `LoadBalancer` · `TargetGroup` · `Database` · `Identity` · `Role` · `SshKey` · `Bucket` · `DnsZone` · `DnsRecord` · `Secret` · `Certificate` · `KmsKey` · `Volume` · `AutoScalingGroup` · `Queue` · `Cache` · `KubernetesCluster` · `Custom` · `K8sNamespace` · `K8sConfigMap` · `K8sSecret` · `K8sPersistentVolumeClaim` · `K8sDeployment` · `K8sStatefulSet` · `K8sService` · `K8sIngress` · `K8sManifest` · `K8sHelmRelease` ## Config Interface Pattern Every resource has a matching `Config` interface: ```typescript interface VmConfig extends ResourceConfig { readonly instanceType: InstanceType readonly image: ImageType readonly network: Vpc | string readonly subnet?: Subnet | string readonly securityGroups?: (SecurityGroup | string)[] // ... } ``` The config interface defines all user-settable properties. Provider-specific configs are never needed — use the abstract types. ## Abstract Types Concept Resources are defined with abstract types instead of provider-specific values: ```typescript instanceType: 'small' // NOT 't3.small' or 'e2-small' image: 'ubuntu-22.04' // NOT 'ami-0c55b159cbfafe1f0' region: 'us-east' // NOT 'us-east-1' or 'us-central1' ``` The provider maps these to native values during validation and creation. ## Resource Lifecycle ``` validate → plan → create / read / update / destroy ``` 1. **validate** — Provider checks the config: does the instance type exist? Is the CIDR valid? Throws `UnsupportedFeatureError` for unsupported resource types. 2. **plan** — Engine diffs config against state to determine create/update/replace/destroy/no-op. 3. **create** — Provider provisions the resource in the cloud. 4. **read** — Provider fetches current state from the cloud (for drift detection). 5. **update** — Provider modifies an existing resource (in-place or replace). 6. **destroy** — Provider removes the resource from the cloud. ## Resource Type Catalog See the [Resources](/resources/) section for detailed config options per resource type. --- # State & Encryption Kyku stores infrastructure state in JSON files under the `.kyku/` directory. Sensitive fields are automatically encrypted. ## State Files State is stored per environment: ``` .kyku/ ├── state.json (default environment) ├── state.dev.json (--env=dev) ├── state.prod.json (--env=prod) └── .kyku.lock (process lock) ``` Each state file contains: ```json { "version": "1", "environment": "default", "encrypted": true, "encryptionMeta": { ... }, "metadata": { "prefix": "a3f27b1d" }, "resources": { "vm-web": { "type": "Vm", "provider": "aws", "providerId": "i-0abc123456", "config": { ... }, "dependencies": ["vpc-main"], "outputs": { "instanceId": "i-0abc123456" }, "createdAt": "...", "updatedAt": "..." } } } ``` ## Encryption State encryption uses **AES-256-GCM** with **PBKDF2** key derivation via the Web Crypto API. ``` KYKU_PASSPHRASE → PBKDF2 (600k iterations, SHA-256) → AES-256-GCM key ``` ### What Gets Encrypted The encryption engine detects sensitive fields by name (`password`, `secret`, `token`, `key`, `private`, `sshPrivateKey`, `certificate`, `apiKey`, `accessKey`, `secretKey`, `signingKey`, `connectionString`) and encrypts their values. Non-sensitive fields (instance types, CIDRs, names) remain in plaintext. ### Setting the Passphrase ```bash # Environment variable export KYKU_PASSPHRASE="your-secure-passphrase" # CLI flag kyku plan --passphrase "your-secure-passphrase" # File (env var or CLI flag) export KYKU_PASSPHRASE_FILE=/path/to/passphrase.txt kyku plan --passphrase-file /path/to/passphrase.txt # Interactive (TTY only) # If no passphrase is provided, Kyku prompts for one ``` ### Without a Passphrase The `plan` command works without a passphrase — secrets are shown as `[encrypted]`: ``` password: [encrypted] ``` The `apply` command requires a passphrase to decrypt secrets before sending them to the cloud provider. ### Safe to Commit Because encryption is field-level and non-sensitive fields remain readable, state files are safe to commit to version control. Only secrets are opaque. ### Viewing Secrets ```bash kyku output --show-secrets ``` This decrypts and displays all outputs, including sensitive ones. ## State Locking Kyku uses a file-based lock (`.kyku/.kyku.lock`) with PID and timestamp to prevent concurrent operations. Stale locks older than 5 minutes are automatically broken. ## Manual State Management See the [state CLI commands](/cli/state/) for listing, showing, and removing resources from state. --- # Config File The config file is a TypeScript module (default: `./infrastructure.ts`) that defines your infrastructure and exports resources for the engine. ## Export Format ### Single Provider ```typescript export default { provider: 'aws', resources: [vpc, vm, db], }; ``` ### Multi-Provider ```typescript export default { provider: 'aws', providers: { aws: { region: 'us-east-1' }, gcp: { region: 'us-central1' }, }, resources: [awsVm, gcpVm], }; ``` ## Remote State Backend Optional. Config-file only — there are no CLI flags for backend settings. ```typescript export default { provider: 'aws', backend: { type: 's3', bucket: 'kyku-state', region: 'us-east-1', key: 'prod/state.json', }, resources: [vpc, vm, db], }; ``` ```typescript backend: { type: 'gcs', bucket: 'kyku-state', key: 'prod/state.json', } ``` ```typescript backend: { type: 'spaces', bucket: 'kyku-state', region: 'nyc3', key: 'prod/state.json', } ``` ```typescript backend: { type: 'http', address: 'https://state.example.com/prod/state.json', } ``` State load/save is atomic on that object. The lock is `${key}.lock` in the same bucket, or `${address}.lock` for HTTP. S3 uses the standard AWS chain; GCS uses Application Default Credentials; Spaces uses `DO_SPACES_ACCESS_KEY` / `DO_SPACES_SECRET_KEY` (endpoint `https://.digitaloceanspaces.com`); HTTP uses `KYKU_HTTP_TOKEN` or `KYKU_HTTP_USERNAME` + `KYKU_HTTP_PASSWORD`. Missing object-store credentials fail with an actionable error; Kyku never falls back to local `.kyku/`. Include the environment in `key` (or `address`) if you use `--env`. Supported types: `s3`, `gcs`, `spaces`, `http`. Individual resources can override the default provider: ```typescript const gcpVm = new Vm({ id: 'vm-gcp', name: 'gcp-server', provider: 'gcp', // ... }); ``` ## Tags and Labels All resources accept a `tags` record that maps to provider-native labels: ```typescript new Vm({ tags: { environment: 'production', 'managed-by': 'kyku', }, }); ``` Tags never create dependency edges in the graph. ## Object vs String References Referencing a resource object creates a dependency edge. String references by logical ID do not: ```typescript // ✅ Object reference — creates Vm → Vpc dependency network: myVpc, // ❌ String reference — NO dependency created network: 'vpc-main', ``` Always use object references for proper dependency ordering. ## Provider-Specific Config Options ### Common ```typescript new Vpc({ region: 'us-east', // Abstract region distributeAcrossAzs: 3, // Auto-subnet count }); ``` ### VM | Property | Type | Description | |---|---|---| | `instanceType` | `InstanceType` | Abstract size or provider map | | `image` | `ImageType` | Abstract image or provider map | | `network` | `Vpc \| string` | VPC reference | | `subnet` | `Subnet \| string` | Subnet reference | | `securityGroups` | `(SecurityGroup \| string)[]` | Security groups | | `sshPublicKey` | `string` | Public key content | | `sshKey` | `SshKey \| string` | SSH key reference | | `userData` | `string` | Cloud-init script | | `distributeAcrossAzs` | `boolean` | Auto-placement across AZs | ### Database | Property | Type | Description | |---|---|---| | `engine` | `'postgresql' \| 'mysql' \| 'mariadb'` | Database engine | | `version` | `string` | Engine version | | `instanceType` | `InstanceType` | Abstract size | | `storage` | `number` | Storage in GB | | `username` | `string` | Master username | | `password` | `Secret` | Master password (auto-encrypted) | | `iamAuth` | `boolean` | IAM authentication | | `vpc` | `Vpc \| string` | VPC placement | | `securityGroups` | `(SecurityGroup \| string)[]` | Security groups | | `backupRetention` | `number` | Backup retention days | ### Load Balancer | Property | Type | Description | |---|---|---| | `lbType` | `'application' \| 'network'` | LB type | | `vpc` | `Vpc \| string` | VPC reference | | `listeners` | `LoadBalancerListener[]` | Listener configs | | `healthCheck` | `HealthCheck` | Health check config | | `spanAcrossAzs` | `boolean` | Cross-AZ balancing | | `securityGroups` | `(SecurityGroup \| string)[]` | Attached SGs | | `size` | `string` | Abstract size | --- # TypeScript modules A Kyku module is a TypeScript function that returns `BaseResource[]`. It does not call `plan` or `apply`. The config file (`infrastructure.ts`) is the only place that exports `{ provider, resources }`. This is a convention, not a registry. There is no `kyku init --from` and no codegen. ## Factory Install the types package and return resource objects. Use **object references** for dependencies (not string IDs): ```typescript import { type BaseResource, SecurityGroup, Vm, Vpc } from '@kykucloud/types' export function webStack(opts: { prefix: string cidr: string region: string sshSources: string[] }): BaseResource[] { const vpc = new Vpc({ id: `${opts.prefix}-vpc`, name: `${opts.prefix}-vpc`, cidr: opts.cidr, region: opts.region, distributeAcrossAzs: 1, }) const sg = new SecurityGroup({ id: `${opts.prefix}-sg`, name: `${opts.prefix}-sg`, vpc, // object ref → SecurityGroup depends on Vpc ingress: [ { protocol: 'tcp', fromPort: 22, toPort: 22, sources: opts.sshSources }, ], }) const vm = new Vm({ id: `${opts.prefix}-vm`, name: `${opts.prefix}-web`, instanceType: 'small', image: 'ubuntu-22.04', network: vpc, securityGroups: [sg], }) return [vpc, sg, vm] } ``` From an npm install the import path is `@kykucloud/types` — the same path this repo uses. ## Compose in `infrastructure.ts` ```typescript import { webStack } from './modules/web-stack' export default { provider: 'hetzner', resources: [ ...webStack({ prefix: 'app', cidr: '10.0.0.0/16', region: 'eu-central', sshSources: ['203.0.113.10/32'], }), ], } ``` `kyku graph` walks the composed array. Object refs show up as edges (`app-vm` depends on `app-vpc` and `app-sg`). See `examples/modules/web-stack.ts` and `examples/composed.ts` in the repo. ## Rules - Factories are pure: construct objects, return them. No `engine.apply()`, no network, no writing `.kyku/`. - Give every resource a stable `id`. Colliding IDs across modules will fail validation. - Spread multiple factories into `resources` to compose stacks. - String IDs (`network: 'app-vpc'`) do **not** create graph edges. --- # First Config The config file (`infrastructure.ts` by default) is a TypeScript module that defines your infrastructure using Kyku resource classes. ## Imports Every resource class is imported from `@kykucloud/types`: ```typescript import { Vpc, Vm, SecurityGroup, LoadBalancer, Database } from '@kykucloud/types'; ``` ## Creating Resources Each resource takes a config object matching its resource-specific interface: ```typescript const vpc = new Vpc({ id: 'vpc-main', // Unique logical ID (used in state) name: 'main-vpc', // Cloud resource name cidr: '10.0.0.0/16', // Provider-native CIDR region: 'us-east', // Abstract region (maps per provider) }); ``` The `id` field is used internally for state tracking and dependency resolution. If omitted, a UUID is generated automatically. The `name` field becomes the cloud resource name (optionally prefixed — see [Deploy Prefix](/concepts/deploy-prefix/)). ## Object References Create Dependencies When you assign a resource object to another resource's property, Kyku creates a dependency edge: ```typescript const vm = new Vm({ id: 'vm-web', name: 'web-server', instanceType: 'small', image: 'ubuntu-22.04', network: vpc, // Object ref → Vm depends on Vpc securityGroups: [webSG], // Array of objects → Vm depends on SecurityGroup }); ``` This means the VPC is created before the VM, and the VM is destroyed before the VPC. **String references** (e.g. `network: 'vpc-main'`) do **not** create dependency edges — use object references to ensure correct ordering. ## The Export Reusable stacks are plain TypeScript factories that return `BaseResource[]` — see [TypeScript modules](/config/modules/). The module must export a default object: ```typescript export default { provider: 'aws', // Default provider for all resources resources: [vpc, webSG, vm, db, lb], }; ``` ### Single Provider ```typescript export default { provider: 'hetzner', resources: [vpc, vm], }; ``` ### Multi-Provider Some resources can use a different provider by setting their `provider` field: ```typescript const awsVm = new Vm({ id: 'vm-aws', name: 'aws-server', provider: 'aws', // ... }); const gcpVm = new Vm({ id: 'vm-gcp', name: 'gcp-server', provider: 'gcp', // ... }); export default { provider: 'aws', // Default providers: { // Named provider configs aws: { region: 'us-east-1' }, gcp: { region: 'us-central1' }, }, resources: [awsVm, gcpVm], }; ``` ## Tags and Labels Tags are passed as key-value pairs and mapped to provider-native labels/tags: ```typescript new Vm({ id: 'vm-web', name: 'web-server', tags: { environment: 'production', team: 'platform', }, // ... }); ``` ## Full Multi-Resource Example ```typescript import { Vpc, Vm, SecurityGroup, LoadBalancer, Database } from '@kykucloud/types'; const vpc = new Vpc({ id: 'vpc-main', name: 'main-vpc', cidr: '10.0.0.0/16', region: 'us-east', }); const webSG = new SecurityGroup({ id: 'sg-web', name: 'web-sg', ingress: [ { protocol: 'tcp', fromPort: 80, toPort: 80, sources: ['0.0.0.0/0'] }, { protocol: 'tcp', fromPort: 443, toPort: 443, sources: ['0.0.0.0/0'] }, ], }); const webServer = new Vm({ id: 'vm-web', name: 'web-server', instanceType: 'small', image: 'ubuntu-22.04', network: vpc, securityGroups: [webSG], userData: `#!/bin/bash apt-get update apt-get install -y nginx`, }); const db = new Database({ id: 'db-main', name: 'app-db', engine: 'postgresql', version: '15', instanceType: 'small', storage: 20, username: 'appuser', vpc: vpc, securityGroups: [webSG], }); const lb = new LoadBalancer({ id: 'lb-web', name: 'web-lb', lbType: 'application', vpc: vpc, listeners: [ { port: 80, protocol: 'http', targets: [{ vm: webServer.outputs.instanceId, port: 80 }], }, ], }); export default { provider: 'aws', resources: [vpc, webSG, webServer, db, lb] }; ``` --- # Install ## Prerequisites - **Bun** — Runtime and package manager. Install from [bun.sh](https://bun.sh). - **Mise** (optional) — Tool version management from [mise.jdx.dev](https://mise.jdx.dev). ## Quick Install Kyku is Bun-only. The CLI publishes as `@kykucloud/cli`. ```bash # https://bun.sh curl -fsSL https://bun.sh/install | bash bun add -g @kykucloud/cli kyku version kyku init --provider=hetzner ``` ## Manual Install (from repo) Until 0.1.0 is on the registry, or when hacking on Kyku itself: ```bash git clone https://github.com/pmdroid/kyku.git cd kyku bun install bun run build ``` This builds all packages in order: `types` → `core` → `aws` → `gcp` → `hetzner` → `cli`. Run the CLI directly: ```bash kyku --help ``` ## Mise (optional) If using Mise for tool version management: ```bash mise install ``` This reads `.mise.toml` and installs the correct Bun version automatically. ## Credentials Set provider credentials as environment variables: ```bash # AWS — standard env vars export AWS_REGION=us-east-1 export AWS_ACCESS_KEY_ID=... export AWS_SECRET_ACCESS_KEY=... # GCP export GOOGLE_CLOUD_PROJECT=my-project export GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json # Hetzner export HCLOUD_TOKEN=your-token # DigitalOcean export DIGITALOCEAN_TOKEN=your-token ``` AWS and GCP can use OIDC in GitHub Actions / GitLab / CircleCI (`KYKU_AWS_ROLE_ARN`, or `KYKU_GCP_WORKLOAD_PROVIDER` + `KYKU_GCP_SERVICE_ACCOUNT`). Hetzner and DigitalOcean have no OIDC — use their API tokens. ## Verify Installation ```bash kyku version kyku doctor ``` `kyku version` prints `@kykucloud/cli 0.1.0`. `kyku doctor` is a read-only pass/fail table (Bun, named credentials, state parse, lock). It does not write state, lock, or cloud. --- # Overview Kyku is a universal infrastructure provisioning tool. Write TypeScript once, deploy anywhere — AWS, GCP, Hetzner, or DigitalOcean. ## Why Kyku Provisioning infrastructure typically means learning a DSL (HCL for Terraform, Bicep for Azure) or managing verbose SDK calls. Kyku abstracts the cloud provider away entirely: - **Same config, any provider** — Your `infrastructure.ts` works on AWS, GCP, and Hetzner without modification. Switch providers by changing one string. - **Abstract types** — `instanceType: 'small'` maps to `t3.small` (AWS), `e2-small` (GCP), or `cpx11` (Hetzner) transparently. - **Auto-inferred dependencies** — Reference a `Vpc` object from a `Vm` and the dependency graph is built automatically. No explicit `depends_on`. - **Encrypted state by default** — Secrets (passwords, tokens) are encrypted with AES-256-GCM. Safe to commit to version control. - **Auto-subnets** — `distributeAcrossAzs: 3` creates public and private subnets across availability zones in one line. ## Comparison | | Kyku | Terraform | |---|---|---| | Language | TypeScript | HCL | | Abstraction | Abstract types → provider-native | Provider-native only | | Dependencies | Auto-inferred from object refs | Explicit `depends_on` | | State encryption | Built-in (AES-256-GCM) | Manual or paid | | Multi-provider | Single config, single run | Separate configs per provider | ## Features - **Unified API** — Same resource classes for every provider. One import, any cloud. - **Abstract types** — `'medium'` resolves to `t3.medium`, `e2-medium`, or `cpx21` per provider. - **Auto-inferred dependencies** — `vm.network = myVpc` creates a `Vm → Vpc` edge automatically. - **Auto-subnets** — `distributeAcrossAzs: 3` injects 3 public + 3 private subnets. - **Plan/Apply** — Preview every change with human-readable diffs before committing. - **Encrypted state** — AES-256-GCM with PBKDF2 key derivation. Safe in git. - **Parallel execution** — Independent resources provision concurrently. - **Multi-provider** — Mix AWS and GCP resources in a single config file. - **Type-safe outputs** — `db.outputs.endpoint` resolves at apply time with full type safety. - **Import** — Adopt existing cloud resources into Kyku management without recreating. ## Architecture ``` ┌──────────────────────────────────────────────────────────┐ │ infrastructure.ts (your config — Vpc, Vm, Database…) │ └──────────────────┬───────────────────────────────────────┘ │ ┌──────────────────▼───────────────────────────────────────┐ │ @kykucloud/types — Resource classes, config interfaces │ │ @kykucloud/core — Engine, DAG builder, diff, crypto │ │ — Plan/apply lifecycle, state manager │ └──────────────────┬───────────────────────────────────────┘ │ ┌──────────────────▼───────────────────────────────────────┐ │ @kykucloud/aws │ @kykucloud/gcp │ @kykucloud/hetzner │ │ @kykucloud/digitalocean │ │ └──────────────────┬───────────────────────────────────────┘ │ ┌──────────────────▼───────────────────────────────────────┐ │ @kykucloud/cli — Commander.js CLI (kyku plan/apply…) │ └──────────────────────────────────────────────────────────┘ ``` ## Monorepo Structure ``` packages/ ├── types/ @kykucloud/types — Shared interfaces & resource classes ├── core/ @kykucloud/core — State engine, DAG, diff, crypto, auth ├── aws/ @kykucloud/aws — AWS SDK v3 provider ├── gcp/ @kykucloud/gcp — Google Cloud provider ├── hetzner/ @kykucloud/hetzner — Hetzner Cloud provider ├── digitalocean/@kykucloud/digitalocean — DigitalOcean provider └── cli/ @kykucloud/cli — Commander.js CLI ``` --- # Quickstart This walkthrough creates a VPC and a VM on AWS, then cleans up. ## 1. Scaffold a Project ```bash kyku init --provider=aws ``` This creates `./infrastructure.ts` with a starter template. ## 2. Write a Config Replace `infrastructure.ts` with: ```typescript import { Vpc, Vm, SecurityGroup } from '@kykucloud/types'; const vpc = new Vpc({ id: 'vpc-main', name: 'main-vpc', cidr: '10.0.0.0/16', region: 'us-east', }); const sg = new SecurityGroup({ id: 'sg-web', name: 'web-sg', ingress: [ { protocol: 'tcp', fromPort: 22, toPort: 22, sources: ['0.0.0.0/0'] }, { protocol: 'tcp', fromPort: 80, toPort: 80, sources: ['0.0.0.0/0'] }, ], }); const vm = new Vm({ id: 'vm-web', name: 'web-server', instanceType: 'small', image: 'ubuntu-22.04', network: vpc, securityGroups: [sg], }); export default { provider: 'aws', resources: [vpc, sg, vm] }; ``` ## 3. Preview Changes ```bash kyku plan ``` The plan output shows what will be created, updated, or destroyed. Expected output: ``` Plan: 3 to create, 0 to update, 0 to destroy ``` ## 4. Apply ```bash kyku apply --auto-approve ``` This provisions the VPC, security group, and VM on AWS. Progress is displayed in real-time. ## 5. Verify Check outputs: ```bash kyku output ``` ## 6. Clean Up ```bash kyku destroy --auto-approve ``` This destroys all resources tracked in state. Resources are destroyed in reverse dependency order (VM → SecurityGroup → VPC). ## Next Steps - Read the [First Config](/getting-started/first-config/) guide for a deeper walkthrough - Explore [Concepts](/concepts/resources/) to understand the resource model - Check provider-specific guides for advanced configuration --- # CI/CD with OIDC Kyku supports OIDC-based authentication for CI/CD platforms. No long-lived access keys needed — temporary credentials are exchanged for each deployment. ## Supported Platforms | Platform | AWS | GCP | Hetzner | DO | |----------|-----|-----|---------|----| | GitHub Actions | ✅ STS AssumeRoleWithWebIdentity | ✅ Workload Identity Federation | Token env var | Token env var | | GitLab CI | ✅ | ✅ | Token env var | Token env var | | CircleCI | ✅ | ✅ | Token env var | Token env var | > Hetzner and DigitalOcean do not support OIDC federation. Use their API tokens as env vars in CI secrets. ## GitHub Actions ### AWS ```yaml # .github/workflows/deploy.yml name: Deploy on: push: branches: [main] permissions: id-token: write contents: read jobs: deploy: runs-on: ubuntu-latest steps: - uses: oven-sh/setup-bun@v2 - uses: actions/checkout@v4 - name: Drift check run: kyku plan --env=prod --detailed-exitcode env: KYKU_AWS_ROLE_ARN: arn:aws:iam::123456789012:role/KykuDeployRole KYKU_PASSPHRASE: ${{ secrets.KYKU_PASSPHRASE }} - name: Deploy run: kyku apply --env=prod --auto-approve env: KYKU_AWS_ROLE_ARN: arn:aws:iam::123456789012:role/KykuDeployRole KYKU_PASSPHRASE: ${{ secrets.KYKU_PASSPHRASE }} ``` Kyku auto-detects the GitHub Actions OIDC token at `ACTIONS_ID_TOKEN_REQUEST_URL` and exchanges it via `STS AssumeRoleWithWebIdentity`. **Prerequisites:** 1. Create an IAM OIDC identity provider for GitHub (`token.actions.githubusercontent.com`) 2. Create a role with a trust policy for your repo 3. Set `KYKU_AWS_ROLE_ARN` as a GitHub Actions secret ### GCP ```yaml name: Deploy on: push: branches: [main] permissions: id-token: write contents: read jobs: deploy: runs-on: ubuntu-latest steps: - uses: google-github-actions/auth@v2 with: workload_identity_provider: projects/123/locations/global/workloadIdentityPools/my-pool/providers/github service_account: deployer@my-project.iam.gserviceaccount.com - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 - name: Deploy run: kyku apply --env=prod --auto-approve env: KYKU_PASSPHRASE: ${{ secrets.KYKU_PASSPHRASE }} ``` Or let Kyku handle OIDC directly by setting env vars: ```yaml env: KYKU_GCP_WORKLOAD_PROVIDER: projects/123/... KYKU_GCP_SERVICE_ACCOUNT: deployer@my-project.iam.gserviceaccount.com KYKU_GCP_PROJECT_ID: my-project ``` ## GitLab CI ```yaml # .gitlab-ci.yml deploy: stage: deploy image: oven/bun:latest id_tokens: INFRA_OIDC_TOKEN: aud: https://gitlab.com variables: KYKU_AWS_ROLE_ARN: arn:aws:iam::123456789012:role/KykuDeployRole script: - kyku plan --env=prod - kyku apply --env=prod --auto-approve ``` ## CircleCI ```yaml version: 2.1 orbs: aws-cli: circleci/aws-cli@5.0 jobs: deploy: docker: - image: oven/bun:latest steps: - checkout - aws-cli/setup: role-arn: arn:aws:iam::123456789012:role/KykuDeployRole - run: kyku apply --env=prod --auto-approve ``` ## Plan Review in CI Use plan files for review gates: ```yaml - name: Generate Plan run: kyku plan --env=prod --out=plan.json - name: Upload Plan Artifact uses: actions/upload-artifact@v4 with: name: plan path: plan.json - name: Apply (manual trigger) if: github.event_name == 'workflow_dispatch' run: kyku apply --env=prod --auto-approve --plan=plan.json ``` ## Env-Specific Deployments ```yaml name: Deploy on: push: branches: - main # → prod - dev # → dev jobs: deploy: runs-on: ubuntu-latest environment: ${{ github.ref_name == 'main' && 'prod' || 'dev' }} steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 - run: kyku apply --env=${{ github.ref_name == 'main' && 'prod' || 'dev' }} --auto-approve env: KYKU_AWS_ROLE_ARN: ${{ secrets.AWS_ROLE_ARN }} KYKU_PASSPHRASE: ${{ secrets.KYKU_PASSPHRASE }} ``` ## Secrets in CI Set `KYKU_PASSPHRASE` as a secret in your CI platform to enable state encryption: ```bash # During apply, passphrase decrypts secrets for provider operations export KYKU_PASSPHRASE="your-secure-passphrase" ``` Without the passphrase, `plan` works but shows `[encrypted]` for secret fields. --- # Deploy a Web App This guide walks through deploying a three-tier web application using Kyku. The same config works across AWS, GCP, and Hetzner. ## Architecture ``` Internet → LoadBalancer (port 80/443) → Web VM (port 8080) ↓ Database (port 5432, private subnet) ``` ## Config ```typescript import { Vpc, Vm, SecurityGroup, LoadBalancer, Database } from '@kykucloud/types'; const vpc = new Vpc({ id: 'vpc-app', name: 'app-vpc', cidr: '10.0.0.0/16', region: 'us-east', distributeAcrossAzs: 2, }); // Public-facing web SG const webSg = new SecurityGroup({ id: 'sg-web', name: 'web-sg', ingress: [ { protocol: 'tcp', fromPort: 80, toPort: 80, sources: ['0.0.0.0/0'] }, { protocol: 'tcp', fromPort: 443, toPort: 443, sources: ['0.0.0.0/0'] }, ], }); // Internal DB SG const dbSg = new SecurityGroup({ id: 'sg-db', name: 'db-sg', ingress: [ { protocol: 'tcp', fromPort: 5432, toPort: 5432, sources: [webSg] }, ], }); const webServer = new Vm({ id: 'vm-web-1', name: 'web-server-1', instanceType: 'small', image: 'ubuntu-24.04', network: vpc, securityGroups: [webSg], userData: `#!/bin/bash apt-get update apt-get install -y nginx systemctl enable nginx systemctl start nginx`, }); const db = new Database({ id: 'db-main', name: 'app-db', engine: 'postgresql', version: '16', instanceType: 'small', storage: 20, username: 'appuser', vpc: vpc, securityGroups: [dbSg], }); const lb = new LoadBalancer({ id: 'lb-web', name: 'web-lb', lbType: 'application', vpc: vpc, listeners: [{ port: 80, protocol: 'http', targets: [{ vm: webServer, port: 80 }], }], }); export default { provider: 'aws', resources: [vpc, webSg, dbSg, webServer, db, lb] }; ``` ## Switching Providers Change the provider and optionally override regions: ```typescript export default { provider: 'gcp', resources: [...] }; // Or multi-provider export default { providers: { aws: { region: 'us-east-1' }, gcp: { region: 'us-central1' }, }, resources: [...], }; ``` ## Deploy ```bash # Preview kyku plan # Apply kyku apply --auto-approve # Check outputs kyku output # Clean up kyku destroy --auto-approve ``` ## What Kyku Does Automatically - **Subnets**: `distributeAcrossAzs: 2` creates 2 public + 2 private subnets - **SG references**: `sources: [webSg]` auto-resolves to the correct security group ID - **Dependencies**: DAG ensures VPC → SGs → VM/DB → LB order - **DB credentials**: Auto-generates password, encrypted in state - **LB targets**: Auto-configures target group and health checks --- # Environments Kyku has built-in workspace support for managing multiple environments (dev, staging, prod) from a single config. ## How It Works ```bash kyku plan --env=dev # → .kyku/state.dev.json kyku apply --env=staging # → .kyku/state.staging.json kyku plan --env=prod # → .kyku/state.prod.json kyku plan # → .kyku/state.json (default) ``` Each environment has its own: - **State file** — separate `.kyku/state..json` - **Deploy prefix** — unique 8-char hex prefix per env (e.g., `a3f27b1d-web-server`) - **Cloud resources** — completely isolated from other environments ## Per-Environment Configuration Use TypeScript to parameterize your config: ```typescript // infrastructure.ts import { Vpc, Vm, SecurityGroup } from '@kykucloud/types'; const env = process.env.KYKU_ENV || 'dev'; const configs = { dev: { instanceType: 'micro' as const, instanceCount: 1, distAzs: 1, }, staging: { instanceType: 'small' as const, instanceCount: 2, distAzs: 2, }, prod: { instanceType: 'medium' as const, instanceCount: 3, distAzs: 3, }, }; const cfg = configs[env as keyof typeof configs]; const vpc = new Vpc({ id: 'vpc-main', name: `app-vpc-${env}`, cidr: '10.0.0.0/16', region: 'us-east', distributeAcrossAzs: cfg.distAzs, }); const sg = new SecurityGroup({ id: 'sg-web', name: `web-sg-${env}`, ingress: [ { protocol: 'tcp', fromPort: 80, toPort: 80, sources: ['0.0.0.0/0'] }, ], }); const servers = Array.from({ length: cfg.instanceCount }, (_, i) => new Vm({ id: `vm-web-${i}`, name: `web-server-${i}`, instanceType: cfg.instanceType, image: 'ubuntu-24.04', network: vpc, securityGroups: [sg], }) ); export default { provider: 'aws', resources: [vpc, sg, ...servers] }; ``` ## Deploy Prefix Each environment gets a unique 8-character hex deploy prefix, auto-generated on first `plan`/`apply` and stored in `StateFile.metadata.prefix`. | Environment | Prefix | Example Resource Name | |-------------|--------|----------------------| | dev | `a3f27b1d` | `a3f27b1d-web-server-0` | | staging | `c8e41f9a` | `c8e41f9a-web-server-0` | | prod | `b7d12e83` | `b7d12e83-web-server-0` | This prevents name collisions when managing multiple environments in the same cloud account. **Custom prefix:** ```typescript // In your config or engine init const engine = new KykuEngine({ deployPrefix: 'myapp', // Custom prefix instead of random hex }); ``` ## Branch-Based Environments (CI) ```yaml name: Deploy on: push: branches: - main # → prod - dev # → dev - staging # → staging jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 - run: | ENV=${{ github.ref_name }} kyku apply --env=$ENV --auto-approve env: KYKU_PASSPHRASE: ${{ secrets.KYKU_PASSPHRASE }} ``` ## State Isolation State files are fully isolated per environment: ``` .kyku/ ├── state.json # default ├── state.dev.json # dev ├── state.staging.json # staging └── state.prod.json # prod ``` Each state file contains its own: - Encryption metadata (salt, IV) - Resource tracking (provider IDs, outputs) - Deploy prefix ## Environment-Specific Config Files For larger differences between environments, use separate config files: ```bash kyku plan --config=./infrastructure.dev.ts --env=dev kyku plan --config=./infrastructure.prod.ts --env=prod ``` ## Best Practices - **Use `--env` consistently**: Always pass `--env` to avoid mixing environments in the default state file. - **CI matches env to branch**: Map branches to environments automatically. - **Separate credentials**: Use different cloud accounts or IAM roles per environment. - **Passphrase per env**: Consider different `KYKU_PASSPHRASE` values per environment. - **Destroy selectively**: `kyku destroy --env=dev` only destroys dev resources. --- # Import Resources Use `kyku import` to bring existing cloud resources under Kyku management without recreating them. ## Usage ```bash kyku import [--name ] [-c ./config.ts] ``` | Argument | Description | |----------|-------------| | `resourceType` | Resource type (Vpc, Vm, Bucket, DnsZone, etc.) | | `cloudId` | Provider-native ID (e.g., `vpc-0abc123`, `i-0xyz789`) | | `--name` | Optional name for the resource in state | | `-c` | Config file path (default: `./infrastructure.ts`) | ## Examples ```bash # Import an existing AWS VPC kyku import Vpc vpc-0abc123def456 --name=main-vpc # Import an existing EC2 instance kyku import Vm i-0xyz789abc123 --name=web-server # Import an existing GCP Cloud SQL instance kyku import Database my-instance --name=app-db # Import an existing Hetzner network kyku import Vpc 12224210 --name=main-network ``` ## How It Works 1. Kyku creates a **stub resource** in memory with the given type and cloud ID 2. Calls `provider.readState(stub, cloudId)` to read the current cloud state 3. Writes the resource into the state file with its full config and outputs 4. On subsequent `plan` runs, the resource shows as **no-op** (existing) ``` State before: empty State after: Vm "i-0xyz789" tracked (no changes needed) ``` ## Prerequisites - The resource must exist in your cloud account - Valid credentials must be configured for the provider - The config file (`infrastructure.ts`) must reference the imported resource if you want Kyku to manage it going forward ## Adding to Config After importing, add the resource to your config file so Kyku can detect future drift: ```typescript const vpc = new Vpc({ id: 'vpc-main', // Same id used during import name: 'main-vpc', cidr: '10.0.0.0/16', }); ``` You can omit properties that can't be changed (like `cidr` for VPCs) — Kyku will use the values from state. ## Supported Resource Types All resource types support import: Vpc, Vm, SecurityGroup, LoadBalancer, Database, Identity, Role, SshKey, Bucket, DnsZone, DnsRecord, TargetGroup, Custom. ## Unsupported Resources Resources that throw `UnsupportedFeatureError` on the provider (e.g., TargetGroup on GCP, Database on Hetzner) cannot be imported. ## State File After Import ```json { "vpc-abc": { "id": "vpc-main", "type": "Vpc", "provider": "aws", "providerId": "vpc-0abc123def456", "config": { "cidr": "10.0.0.0/16", "region": "us-east" }, "dependencies": [], "outputs": { "vpcId": "vpc-0abc123def456" } } } ``` ## Import vs Create | Aspect | Import | Create | |--------|--------|--------| | Cloud resource | Already exists | Does not exist | | Kyku action | Reads state, tracks | Creates new | | Config required | Optional (can add later) | Required | | First plan shows | No-op | Create | ## Caveats - **Config alignment**: The resource's config in your `infrastructure.ts` should match what's in the cloud. Otherwise Kyku will show drift on the next plan. - **Provider IDs**: Kyku stores the provider-native ID (e.g., `vpc-0abc123`). Do not modify this in the state file. - **Dependencies**: If the imported resource depends on other resources (e.g., a VM in a VPC), import the dependencies first. - **Name resolution**: If the cloud resource was created outside Kyku, ensure its name matches what your config expects, or set `--name` explicitly. --- # HAR record and replay Kyku can record Hetzner (and other `requestJson`) HTTP to a HAR file, strip tokens, and replay the same sequence in unit and BDD CI. This is not an auto-re-record loop and does not cover every provider. One Hetzner VPC lifecycle fixture is checked in. ## Environment variables | Variable | Meaning | |----------|---------| | `KYKU_RECORD=1` | Record every `requestJson` call to `KYKU_HAR_FILE` | | `KYKU_REPLAY=1` | Serve responses from `KYKU_HAR_FILE`; no network | | `KYKU_HAR_FILE` | Path to the HAR file (default `kyku.har` in the working directory) | `KYKU_RECORD` and `KYKU_REPLAY` cannot both be set. Do not rename `KYKU_*` env vars. Recording uses `KYKU_*` only. ## Record ```bash # Live Hetzner (human; writes a sanitized fixture) HCLOUD_TOKEN=… bun run packages/hetzner/scripts/record-vpc-lifecycle.ts # Same sequence against an in-memory mock (no token) bun run packages/hetzner/scripts/record-vpc-lifecycle.ts ``` Authorization, `Auth-API-Token`, cookies, and JSON fields whose names look like secrets are replaced with `[REDACTED]` before the file is written. ## Replay CI installs the harness in replay mode against `packages/hetzner/test/fixtures/vpc-lifecycle.har`. Tests call `VpcManager` with a dummy token; `fetch` is never used. If the next real request does not match the next recorded entry, replay throws `HarReplayError` and prints: ``` HAR replay diverged at request 3: expected: POST https://api.hetzner.cloud/v1/networks actual: GET https://api.hetzner.cloud/v1/networks/424242 ``` ## Token guard `bun run check:har` walks `**/*.har` and fails if a Bearer token, unsanitized `Authorization` header, or a configured `HCLOUD_TOKEN` / `DIGITALOCEAN_TOKEN` value is present. CI runs this on every PR. --- # State Encryption Kyku encrypts sensitive fields in state files using **AES-256-GCM** with **PBKDF2** key derivation via the Web Crypto API. No external dependencies required. ## Why Encryption State files contain sensitive data — database passwords, API tokens, secret keys. Kyku encrypts these fields so state files are safe to commit to version control or share with CI systems. ## Architecture ``` Passphrase (KYKU_PASSPHRASE) │ ▼ PBKDF2 (600,000 iterations, SHA-256, random salt) │ ▼ 256-bit AES key │ ▼ AES-256-GCM (random IV per encryption) │ ▼ Ciphertext + auth tag → stored in state as "ENC:base64-ciphertext:base64-tag" ``` ### Key Derivation | Parameter | Value | |-----------|-------| | Algorithm | PBKDF2 | | Hash | SHA-256 | | Iterations | 600,000 | | Output | 256-bit key | > **Why PBKDF2 over Argon2id?** Bun's `Bun.password.hash()` returns a formatted Argon2id hash string, not raw key bytes. PBKDF2 is a NIST-standard KDF natively available in Web Crypto, making it the correct tool for deriving AES-256-GCM keys from passphrases. ### Encryption | Parameter | Value | |-----------|-------| | Algorithm | AES-256-GCM | | Key size | 256 bits | | IV | 12 bytes (random, per encryption) | | Auth tag | 16 bytes | ## Usage ```bash # Set passphrase export KYKU_PASSPHRASE="your-secure-passphrase" # Plan (works without passphrase — secrets shown as [encrypted]) kyku plan # Apply (requires passphrase for decryption) kyku apply --auto-approve # Show decrypted outputs kyku output --show-secrets ``` ### Passphrase Sources | Source | Example | Priority | |--------|---------|----------| | Env var | `KYKU_PASSPHRASE=...` | Highest | | Docker secret file | `KYKU_PASSPHRASE_FILE=/run/secrets/kyku-passphrase` | Medium | | CLI flag | `--passphrase "..."` | Lowest | ### Interactive Mode If no passphrase source is found, Kyku prompts for it interactively during `apply`. ## State File Format ```json { "version": "2.0", "encrypted": true, "encryptionMeta": { "algorithm": "aes-256-gcm", "kdf": "pbkdf2", "kdfParams": { "iterations": 600000, "hash": "SHA-256", "salt": "base64-encoded-salt" }, "iv": "base64-encoded-iv" }, "resources": { "db-123": { "type": "Database", "config": { "name": "app-db", "password": "ENC:base64-ciphertext:base64-tag" } } } } ``` ### Encryption Metadata The `encryptionMeta` block is stored once per state file. All encrypted fields use the same key derivation (same salt). Each field gets its own random IV. ## Which Fields Are Encrypted Fields are encrypted if they match these name patterns (case-insensitive): - `*password` - `*secret` - `*token` - `*credential` Or if the field type is `Secret`: ```typescript import { Secret } from '@kykucloud/types'; const config = { apiKey: 'sk-...' as Secret, // Will be encrypted }; ``` ## Plan Mode vs Apply Mode | Mode | Passphrase Required | Secret Display | |------|-------------------|----------------| | `plan` | No | `[encrypted]` | | `apply` | Yes | Decrypted for provider calls | | `output` | No | `[redacted]` | | `output --show-secrets` | Yes | Decrypted | ## Security Best Practices - **Strong passphrase**: Use at least 16 characters with mixed case, numbers, and symbols - **Never hardcode**: Use env vars, Docker secrets, or CI secret stores - **Rotate periodically**: Change the passphrase by re-encrypting state - **SSH keys**: Only public keys in config; private keys never touch Kyku - **CI**: Store `KYKU_PASSPHRASE` in your CI platform's secret store ## FAQ **Can I commit state to git?** Yes. Encrypted state files are safe to commit. Only the passphrase holder can decrypt secrets. **What if I lose the passphrase?** Encrypted secrets are unrecoverable. You would need to recreate the resources with new credentials. **Does encryption affect plan diffs?** No. The diff engine compares config shapes, not encrypted values. Plan output shows `[encrypted]` for secret fields regardless of changes. **Can I use my own encryption?** Not currently. Kyku handles encryption internally. CustomResource handlers can use `setSecret()` to mark values for encryption. --- # ADR-001 Policy tests # ADR-001: Typed TypeScript policy API for `kyku test` **Status:** Accepted **Date:** 2026-08-15 **Tickets:** PAS-155 ## Context `kyku test` must assert against the generated plan with no cloud calls. Two assertion surfaces were on the table: 1. **Reuse the repo Gherkin suite** (`features/*.feature` + vitest-cucumber). Users would write `.feature` files and step definitions. 2. **A typed TypeScript policy API** that receives the desired `BaseResource[]` and the offline `Plan`. ## Decision Use a typed TypeScript policy API (`definePolicy` in `@kykucloud/core`). Do not reuse Gherkin as the user-facing assertion surface. ## Why Gherkin is not cheaper here - The in-repo Gherkin suite tests the *engine* (plan/apply/destroy/crypto). Its step definitions bind to Vitest world objects, not to a user's `infrastructure.ts`. - Shipping that harness as a product API would mean documenting Cucumber, a World object, and a second file format. Users already write TypeScript configs. - `Plan` and `BaseResource` are already typed. A `check(ctx)` function is a few lines; a `.feature` file still needs custom steps to reach security-group ingress. - Scope cut: no OPA/Rego, no watch mode, no coverage. Adding a Gherkin parser would be extra surface for no extra power. Gherkin stays as the project's own BDD suite. It is not the policy language. ## Assertion surface ```typescript import { definePolicy, findOpenIngress } from '@kykucloud/core' export default [ definePolicy('ssh-not-world-open', ({ resources, plan, fail }) => { for (const resource of resources) { for (const hit of findOpenIngress(resource, { port: 22 })) { fail(`${resource.id}: SSH open to ${hit.source}`, resource.id) } } void plan }), ] ``` `kyku test` loads the config, builds an **offline** plan from local state (never `provider.readState`), runs each policy, exits `1` on any `fail()`, `0` when all pass. ## Consequences - Default policy file is `./infrastructure.test.ts`. - Policies can inspect desired resources and the generated plan (creates/updates/destroys) without credentials. - Sample in `examples/policy/`: an open SSH rule fails; a narrowed source passes. --- # Auth Modules Kyku supports multiple authentication strategies, from environment variables to OIDC-based temporary credentials in CI/CD. ## Auth Architecture The `@kykucloud/core/auth` module abstracts credential resolution: ```typescript interface AuthConfig { provider: 'aws' | 'gcp' | 'hetzner' | 'digitalocean'; env: string; // OIDC-specific awsRoleArn?: string; gcpWorkloadProvider?: string; gcpServiceAccount?: string; } ``` The auth module resolves credentials in priority order: 1. OIDC (if CI environment detected and OIDC config present) 2. Env vars (standard cloud SDK credentials) 3. Config/SDK default chains (AWS profile, GCP ADC) ## AWS Authentication ### Standard Env Vars ```bash export AWS_REGION=us-east-1 export AWS_ACCESS_KEY_ID=... export AWS_SECRET_ACCESS_KEY=... ``` ### OIDC (CI/CD) Kyku auto-detects CI platforms and exchanges OIDC tokens for AWS credentials: ```yaml env: KYKU_AWS_ROLE_ARN: arn:aws:iam::123456789012:role/KykuDeployRole ``` The exchange uses `STS AssumeRoleWithWebIdentity`: 1. Reads OIDC token from CI platform (GitHub: `ACTIONS_ID_TOKEN_REQUEST_URL`) 2. Calls `STS AssumeRoleWithWebIdentity` with the token and role ARN 3. Injects temporary `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN` as env vars 4. Standard SDK picks them up automatically ### AWS SSO ```bash aws sso login --profile my-profile export AWS_PROFILE=my-profile ``` Kyku reads the SSO-sourced credentials from the shared credentials file via the standard SDK credential chain. ## GCP Authentication ### Application Default Credentials (ADC) ```bash gcloud auth application-default login ``` ### Service Account Key File ```bash export GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json export GOOGLE_PROJECT_ID=my-project ``` ### OIDC (CI/CD) Two approaches: **1. Using `google-github-actions/auth` (recommended for GitHub Actions):** ```yaml - uses: google-github-actions/auth@v2 with: workload_identity_provider: projects/123/... service_account: deployer@my-project.iam.gserviceaccount.com ``` **2. Direct OIDC via Kyku env vars:** ```yaml env: KYKU_GCP_WORKLOAD_PROVIDER: projects/123/locations/global/workloadIdentityPools/my-pool/providers/github KYKU_GCP_SERVICE_ACCOUNT: deployer@my-project.iam.gserviceaccount.com KYKU_GCP_PROJECT_ID: my-project ``` Kyku uses `google-auth-library` for ADC and OIDC token exchange. Tokens are cached with a 1-minute margin before expiry. ## Hetzner Authentication Hetzner Cloud API does not support OIDC. Use API tokens: ```bash export HCLOUD_TOKEN=your-cloud-token ``` Additional tokens for Hetzner DNS and S3: ```bash export HETZNER_DNS_TOKEN=your-dns-token export HETZNER_S3_ACCESS_KEY=your-s3-key export HETZNER_S3_SECRET_KEY=your-s3-secret ``` ## DigitalOcean Authentication ```bash export DIGITALOCEAN_TOKEN=your-api-token export DO_SPACES_ACCESS_KEY=your-spaces-key export DO_SPACES_SECRET_KEY=your-spaces-secret ``` ## Provider Detection The auth module returns early for token-based providers: ```typescript function resolveAuth(config: AuthConfig): AuthResult { if (config.provider === 'hetzner' || config.provider === 'digitalocean') { // Token-based — no OIDC exchange needed return { type: 'token' }; } if (isCI() && hasOidcConfig(config)) { return exchangeOidcToken(config); } // Rely on SDK credential chain (env vars, config file, etc.) return { type: 'sdk-default' }; } ``` ## CI Platform Detection Kyku detects CI platforms by checking environment variables: | Platform | Detection Env Var | |----------|------------------| | GitHub Actions | `GITHUB_ACTIONS` | | GitLab CI | `GITLAB_CI` | | CircleCI | `CIRCLECI` | | Jenkins | `JENKINS_HOME` | ## Token Caching For OIDC exchanges, the resulting credentials are cached to avoid repeated STS calls within a single plan/apply run: - AWS temp creds expire in 1 hour (refreshed if needed) - GCP access tokens cached with 1-minute margin before expiry - Hetzner/DO tokens are static and don't expire --- # Encryption Architecture Kyku encrypts sensitive fields in state files using native Web Crypto API — no external dependencies required. ## Architecture ``` Passphrase (user-provided) │ ▼ PBKDF2 (600,000 iterations, SHA-256, random 16-byte salt) │ ▼ 256-bit AES key │ ▼ AES-256-GCM (random 12-byte IV per encryption) │ ▼ Ciphertext + 16-byte auth tag │ ▼ Stored as "ENC:base64-ciphertext:base64-tag" ``` ## Key Derivation (PBKDF2) ```typescript const salt = crypto.getRandomValues(new Uint8Array(16)); const key = await crypto.subtle.importKey( 'raw', new TextEncoder().encode(passphrase), 'PBKDF2', false, ['deriveKey'] ); const aesKey = await crypto.subtle.deriveKey( { name: 'PBKDF2', salt, iterations: 600000, hash: 'SHA-256', }, key, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'] ); ``` ### Parameters | Parameter | Value | |-----------|-------| | Algorithm | PBKDF2 | | Hash | SHA-256 | | Iterations | 600,000 | | Derived key length | 256 bits | > **Why PBKDF2?** The original design used Argon2id via `Bun.password.hash()`, but that returns a formatted hash string (not raw key bytes) unsuitable for AES key derivation. PBKDF2 is a NIST-standard KDF natively available in Web Crypto, making it the correct tool for this purpose. ## Encryption (AES-256-GCM) ```typescript const iv = crypto.getRandomValues(new Uint8Array(12)); const plaintext = new TextEncoder().encode(value); const encrypted = await crypto.subtle.encrypt( { name: 'AES-GCM', iv, tagLength: 128 }, aesKey, plaintext ); // encrypted = ciphertext + auth tag (appended) const ciphertext = encrypted.slice(0, -16); const tag = encrypted.slice(-16); ``` ### Parameters | Parameter | Value | |-----------|-------| | Algorithm | AES-256-GCM | | Key size | 256 bits | | IV | 12 bytes (random, unique per encryption) | | Auth tag | 16 bytes | | Output format | `ENC:base64Ciphertext:base64Tag` | ## State File Metadata ```json { "version": "2.0", "encrypted": true, "encryptionMeta": { "algorithm": "aes-256-gcm", "kdf": "pbkdf2", "kdfParams": { "iterations": 600000, "hash": "SHA-256", "salt": "base64-salt" }, "iv": "base64-iv" }, "resources": { "db-123": { "config": { "password": "ENC:base64-ciphertext:base64-tag" } } } } ``` The `encryptionMeta` block is stored once per state file. All encrypted fields share the same KDF salt (one derivation per file). Each field gets its own random IV. ## Field Detection Fields are encrypted automatically if they match these name patterns (case-insensitive): - `*password` - `*secret` - `*token` - `*credential` Or if explicitly typed as `Secret`: ```typescript import { Secret } from '@kykucloud/types'; const config = { apiKey: 'sk-...' as Secret, }; ``` ## Decryption Flow ```typescript // During apply, when passphrase is available: function decryptValue(encoded: string, meta: EncryptionMeta): string { const [_, ciphertextB64, tagB64] = encoded.match(/^ENC:(.+):(.+)$/); const salt = base64ToBytes(meta.kdfParams.salt); const iv = base64ToBytes(meta.iv); const key = deriveKey(passphrase, salt, meta.kdfParams); const plaintext = await crypto.subtle.decrypt( { name: 'AES-GCM', iv }, key, concat(base64ToBytes(ciphertextB64), base64ToBytes(tagB64)) ); return new TextDecoder().decode(plaintext); } ``` ## Plan Mode vs Apply Mode | Mode | Passphrase | Secret Handling | |------|-----------|-----------------| | `plan` | Not required | Shows `[encrypted]` | | `apply` | Required | Decrypted for provider calls | | `output` | Not required | Shows `[redacted]` | | `output --show-secrets` | Required | Decrypted | ## Security Considerations - **Salt reuse**: Same salt for all fields in one state file is acceptable — each field has a unique IV - **Passphrase strength**: 600,000 PBKDF2 iterations provides reasonable brute-force resistance - **Auth tag verification**: GCM authentication tag prevents tampering - **No padding oracle**: GCM is an authenticated encryption mode, immune to padding oracle attacks - **Web Crypto API**: Uses the browser/JavaScript engine's native crypto implementation, not a JS library --- # Architecture Decision Log This document captures all architectural decisions organized by category. ## 1. Technology Stack | Decision | Choice | Rationale | |----------|--------|-----------| | Language | TypeScript (latest) | Type safety, modern JS features | | Runtime | BunJS | Fast, all-in-one (runtime, package manager, test runner) | | Testing | Bun test + MSW | Native Bun support, HTTP mocking | | Build | Bun bundler + tsc | Fast bundling + type checking | | CLI Framework | Commander.js | Mature, widely used | | Tool Management | Mise | Consistent tool versions across team | ## 2. Security | Decision | Choice | Rationale | |----------|--------|-----------| | Encryption Algorithm | AES-256-GCM | Industry standard, authenticated | | Key Derivation | PBKDF2 (Web Crypto) | NIST-standard KDF | | KDF Iterations | 600,000 | OWASP recommended minimum | | Passphrase Source | Env var / file / flag | Flexible for CI/CD | | Encrypted Fields | Sensitive only | Performance, readability | | Field Detection | Name patterns + Secret\ | Automatic + explicit | | SSH Keys | Public only | Private keys not managed by Kyku | ## 3. Resources (MVP Scope) | Resource | Status | Notes | |----------|--------|-------| | Vpc | ✅ Included | With region and AZ distribution | | Subnet | ✅ Included | First-class, public/private | | Vm | ✅ Included | With AZ distribution | | SecurityGroup | ✅ Included | With SG-to-SG references | | LoadBalancer | ✅ Included | With AZ spanning | | Database | ✅ Included | PostgreSQL, MySQL, MariaDB | | Identity | ✅ Included | IAM users, service accounts | | Role | ✅ Included | With hierarchical permissions | | Storage (S3/Cloud Storage) | ❌ Deferred | Post-MVP | | K8s Cluster | ❌ Deferred | Post-MVP | | K8s Deployment | ❌ Deferred | Post-MVP | ## 4. Regions & Networking | Decision | Choice | Rationale | |----------|--------|-----------| | Region Format | Abstract + override | Portable, flexible | | Abstract Regions | `us-east`, `eu-central`, etc. | User-friendly | | AZ Distribution | Automatic (default) | High availability | | Subnet Model | First-class resources | Explicit control when needed | | Subnet Types | Public + private | Security best practice | | DB Placement | Always private subnets | Security | ## 5. State Management | Decision | Choice | Rationale | |----------|--------|-----------| | State Format | JSON | Human-readable, debuggable | | State Location | `.kyku/state..json` | Per-environment | | Encryption | Field-level | Selective, performant | | Locking | File-based (future) | Prevent concurrent applies | ## 6. CLI Design | Decision | Choice | Rationale | |----------|--------|-----------| | Commands | plan, apply, output | Core workflow | | Plan Output | Human-readable text | Easy review | | Apply Confirmation | Interactive prompt | Prevent accidents | | Auto-approve | `--auto-approve` flag | CI/CD support | | Secrets in Output | Redacted by default | Security | | Exit Codes | Standard (0=success, 1=error) | Shell scripting | ## 7. Provider Support | Feature | AWS | GCP | Hetzner | |---------|-----|-----|---------| | VPC/Network | ✅ Full | ✅ Full | ✅ Full | | Subnet | ✅ Per-AZ | ✅ Regional | ✅ Simple | | VM | ✅ Full | ✅ Full | ✅ Full | | Security Group | ✅ Native | ✅ Firewall Rules | ✅ Firewall | | Load Balancer | ✅ ALB/NLB | ✅ Cloud LB | ✅ LB | | Database | ✅ RDS | ✅ Cloud SQL | ❌ | | Identity/User | ✅ IAM User | ✅ Service Account | ❌ | | Role | ✅ IAM Role | ✅ Custom Role | ❌ | ## Detailed Decision Log ### 2024-05-10: Initial Architecture **Decision:** Build universal infrastructure provisioning tool **Scope:** AWS, GCP, Hetzner | **Resources:** VM, DB, LB, VPC, Security Groups, IAM ### 2024-05-10: Technology Stack **Decision:** TypeScript + BunJS + Mise **Rationale:** Modern, fast, type-safe ### 2024-05-10: Security Groups **Decision:** SG-to-SG references in rules **Rationale:** Natural security model for infrastructure ### 2024-05-10: Database Auth **Decision:** IAM auth primary, password legacy **Rationale:** Security best practice ### 2024-05-10: Regions & Subnets **Decision:** Abstract regions, first-class subnets **Rationale:** Portability across providers, explicit control when needed ### 2024-05-10: Secret Encryption **Decision:** AES-256-GCM + Argon2id → **Superseded** by 2024-05-13 (PBKDF2) ### 2024-05-10: K8s Deferral **Decision:** Post-MVP **Rationale:** Focus on infrastructure primitives first ### 2024-05-11: CI Authentication Strategy **Decision:** Hybrid — OIDC automatic in CI, standard credential chains locally **Rationale:** CI platforms provide OIDC tokens natively; local dev uses SDK defaults ### 2024-05-11: AWS CI Auth **Decision:** `AssumeRoleWithWebIdentity` via STS, inject temp creds into env vars **Config:** `KYKU_AWS_ROLE_ARN`, `KYKU_AWS_ROLE_SESSION_NAME` ### 2024-05-11: GCP CI Auth **Decision:** Workload Identity Federation + service account impersonation **Config:** `KYKU_GCP_WORKLOAD_PROVIDER`, `KYKU_GCP_SERVICE_ACCOUNT`, `KYKU_GCP_PROJECT_ID` ### 2024-05-11: Hetzner Auth **Decision:** No OIDC support, continue with `HCLOUD_TOKEN` **Rationale:** Hetzner Cloud API does not support OIDC federation ### 2024-05-13: Encryption — PBKDF2 over Argon2id **Decision:** Use Web Crypto PBKDF2 (600,000 iterations, SHA-256) **Rationale:** `Bun.password.hash()` returns formatted hash strings, not raw key bytes. PBKDF2 is the correct tool for deriving AES-256-GCM keys. ### 2024-05-13: Output Propagation **Decision:** Output values persisted to state file after resource creation. Cross-resource output references create implicit dependency edges. ### 2024-05-13: State Backup **Decision:** Timestamped state backup before every `apply` execution. **Rationale:** Safety net for failed applies. ### 2024-05-13: AWS VPC Networking **Decision:** VPC includes IGW, NAT Gateways (per AZ), route tables (public + private). **Rationale:** VPCs without IGW/NAT are non-functional. Auto-generating avoids requiring users to understand AWS networking primitives. ### 2024-05-13: RDS Instance Types **Decision:** Separate instance type mapping (`db.t3.*` vs `t3.*`). **Rationale:** Cloud managed databases use different naming conventions than compute. ### 2024-05-13: GCP Client Libraries **Decision:** `@google-cloud/compute` and `@google-cloud/iam` (modular packages). **Rationale:** Smaller install, proper ESM support. Monolithic `googleapis` has CJS/ESM issues. ### 2024-05-13: IAM Instance Profiles **Decision:** EC2 instances automatically get IAM instance profiles for SGs with associated IAM roles. **Rationale:** Database IAM auth requires instance profiles. ### 2024-05-13: Resource Classes **Decision:** Resources are classes with constructors, not plain interfaces. **Rationale:** Auto-generated UUIDs, `outputs` proxy, natural user API (`new Vpc({...})`). ### 2024-05-13: Output References **Decision:** `resource.outputs.` returns `OutputValue` placeholder. Template literals stringify to `${output::}`. Engine resolves during apply. ### 2024-05-13: CustomResource Registry **Decision:** Handlers referenced by `handlerId` string via global `CustomResourceRegistry`. **Rationale:** Functions cannot be serialized to JSON state. Registry allows state to survive plan/apply runs. ### 2024-05-13: Auto-Subnet Visibility **Decision:** Auto-created subnets injected as full `Subnet` resources with deterministic IDs. **Rationale:** Users need to reference auto-subnets. First-class resources make them visible in graph and state. ### 2024-05-13: Dynamic Provider Imports **Decision:** CLI provider factory uses dynamic `import()`. **Rationale:** Avoids bundling all provider SDKs. Reduces CLI startup time. ### 2024-05-13: Destroy Uses State **Decision:** `kyku destroy` operates on state file, not config. **Rationale:** Resources removed from config would otherwise be orphaned. ### 2024-05-13: AWS ELB v2 Health Checks **Decision:** Target Groups for health checks (ELB v2 API). **Rationale:** `ConfigureHealthCheckCommand` does not exist in ELB v2 SDK. --- # Dependency Graph The dependency graph is the backbone of Kyku's execution ordering. It ensures resources are created, updated, and destroyed in the correct sequence. ## GraphBuilder The `GraphBuilder` class in `@kykucloud/core` scans every resource property for `BaseResource` instances and builds a Directed Acyclic Graph (DAG). ### Edge Detection ```typescript // Object reference → creates edge vm.network = myVpc; // Result: Vm → Vpc edge // Array of objects → creates edges vm.securityGroups = [sg1, sg2]; // Result: Vm → sg1, Vm → sg2 edges // String reference → NO edge vm.network = "vpc-abc123"; // Result: no dependency created (use object references!) ``` ### Rules | Reference Type | Edge Created? | Example | |---------------|--------------|---------| | `BaseResource` property | ✅ Yes | `vm.network = myVpc` | | `BaseResource[]` property | ✅ Yes | `vm.securityGroups = [sg1, sg2]` | | String (ID) | ❌ No | `vm.network = "vpc-id"` | | Tags/labels (`Record`) | ❌ No | `tags: { env: "prod" }` | | Output references | ✅ Yes | `vm.env.DATABASE_URL = db.outputs.endpoint` | > **Important**: Use object references (not string IDs) to ensure the dependency graph captures all edges. ## Level Assignment Resources are assigned levels via **topological sort**: ``` Level 0: Vpc, SshKey (no dependencies) Level 1: SecurityGroup, Subnet (depends on Vpc) Level 2: Vm, Database (depends on Vpc, SG, Subnet) Level 3: LoadBalancer (depends on Vm, Vpc, SG) ``` Resources at the same level execute **in parallel**. Level N waits for all levels < N to complete. ## Cycle Detection `GraphBuilder.hasCycles()` detects circular dependencies and throws a `CycleError`: ```typescript // Bad: circular reference sgA.ingress = [{ sources: [sgB] }]; sgB.ingress = [{ sources: [sgA] }]; // CycleError: Circular dependency detected ``` Circuits with `TargetGroup ↔ LoadBalancer` are avoided by making only one direction reference the other. ## Destroy Order Destroy follows the reverse of creation order. Resources at **higher levels** are destroyed first: ``` Destroy order: LoadBalancer → Vm → SecurityGroup → Vpc ``` The `destroyOrder` array in the CLI must list resources in this reverse order. ## Graph Verification Use `kyku graph` to inspect and verify resource connectivity: ```bash kyku graph # ASCII tree view with nodes, edges, and levels kyku graph --dot # Graphviz DOT output kyku graph -c ./my-config.ts # Inspect a specific config ``` ### Verification Checklist | Check | How | |-------|-----| | No dangling references | Every `depends on:` ID in the tree output refers to a node that appears elsewhere in the graph | | No orphan resources | Every node has at least one dependency OR at least one dependent (unless standalone like SshKey) | | Create order correct | Roots (level 0) appear first, leaves appear last | | Destroy order matches levels | Compare `destroyOrder` against graph output | | No unintended cycles | Detected automatically — `CycleError` thrown at validation time | | Auto-subnet injection | After `injectAutoSubnets()`, graph shows synthetic `Subnet` nodes under each `Vpc` | ### Visual Inspection with --dot ```bash kyku graph --dot > graph.dot dot -Tsvg graph.dot > graph.svg # Color-coded SVG dot -Tpng graph.dot > graph.png # PNG for docs ``` Dot output assigns distinct shapes and colors per resource type: - `Vpc` = folder shape, blue - `Vm` = box shape, green - `SecurityGroup` = hexagon, orange - `Subnet` = ellipse, purple - `LoadBalancer` = diamond, red - `Database` = cylinder, yellow ## Auto-Subnet Injection After the user's resources are collected, the engine injects synthetic `Subnet` resources for each Vpc with `distributeAcrossAzs > 0`: ```typescript // Vpc with distributeAcrossAzs: 2 // Creates 4 synthetic Subnet resources: // vpc-main-subnet-public-0 (level 1, depends on Vpc) // vpc-main-subnet-public-1 (level 1, depends on Vpc) // vpc-main-subnet-private-0 (level 1, depends on Vpc) // vpc-main-subnet-private-1 (level 1, depends on Vpc) ``` These subnets are first-class resources in the graph and state file. VMs are automatically wired to private subnets (or public if they have public SG rules). ## Implementation ```typescript class GraphBuilder { // Entry point build(resources: BaseResource[]): Graph // Edge detection private scanForReferences(resource: BaseResource): string[] private isBaseResource(value: unknown): boolean // Topological sort private assignLevels(edges: Edge[]): Map // Cycle detection hasCycles(): boolean // Output toAscii(): string toDot(): string } interface Graph { nodes: Node[] edges: Edge[] levels: Map } interface Node { id: string type: ResourceType name: string } interface Edge { from: string // dependent to: string // dependency } ``` --- # Diff Engine The `DiffEngine` compares the desired configuration (defined in your `infrastructure.ts`) against the actual cloud state (returned by `provider.readState()`). It produces a set of changes that the planner uses to build the execution plan. ## How It Works ### 1. resourceToConfig() The engine extracts a plain config object from each `BaseResource`, excluding "known" keys that are not part of the resource's configuration: ```typescript // KNOWN_RESOURCE_KEYS = {outputs, type, id, name, provider, tags, labels} // These are SKIPPED — don't bother returning them from read()/create() // For Vm, resourceToConfig() produces: config: { instanceType: vm.instanceType, // abstract string image: vm.image, // abstract string network: typeof vm.network === 'string' ? vm.network : vm.network.id, subnet: typeof vm.subnet === 'string' ? vm.subnet : vm.subnet?.id, securityGroups: vm.securityGroups?.map(sg => typeof sg === 'string' ? sg : sg.id ) ?? [], sshPublicKey: vm.sshPublicKey, sshKey: typeof vm.sshKey === 'string' ? vm.sshKey : vm.sshKey?.id, userData: vm.userData, distributeAcrossAzs: vm.distributeAcrossAzs, } ``` **Conversion rules:** - `BaseResource` objects → their `.id` string - `BaseResource[]` arrays → array of `.id` strings - `string` values → as-is - `undefined` values → as-is (both sides must agree on undefined) ### 2. diff() The `diff()` method calls `resourceToConfig()` on the desired resource and compares it via `JSON.stringify` against the actual config from `provider.readState()`. ```typescript diff(resource: BaseResource, actual: CloudState): Change[] { const desired = this.resourceToConfig(resource); const actualConfig = actual.config; if (JSON.stringify(desired) === JSON.stringify(actualConfig)) { return []; // no-op } // Detect property-level changes const changes: PropertyChange[] = []; for (const key of Object.keys(desired)) { if (JSON.stringify(desired[key]) !== JSON.stringify(actualConfig[key])) { changes.push({ property: key, oldValue: actualConfig[key], newValue: desired[key], action: this.isImmutable(resource.type, key) ? 'replace' : 'update', }); } } return changes; } ``` ### 3. Immutable Property Detection Some properties cannot be changed in-place. The diff engine detects these and marks them as `replace` (destroy + recreate): | Resource | Immutable Properties | |----------|---------------------| | Vpc | `cidr`, `region` | | Subnet | `cidr`, `vpc` | | Vm | `image`, `instanceType`, `network` | | SecurityGroup | `name` (some providers) | | LoadBalancer | `lbType`, `vpc` | | Database | `engine`, `version`, `vpc` | | Identity | `name` | | Role | `name` | ## Provider Integration ### Provider `read()` Must Match `resourceToConfig()` This is the most important contract in the provider interface: ```typescript // Provider.read() must return a config with identical keys to resourceToConfig() async read(resource: Vpc, providerId: string): Promise { return { providerId: 'vpc-0abc123', config: { cidr: '10.0.0.0/16', // matches region: 'us-east', // matches distributeAcrossAzs: 2, // matches name: 'main-vpc', // SKIPPED by diff (KNOWN_RESOURCE_KEYS) }, }; } ``` If `read()` returns fewer keys than `resourceToConfig()`, the next plan will show drift and trigger a spurious update. ### Provider `create()` Must Also Match Both `read()` and `create()` return `CloudState` with a `config` property. Both must use the **same config shape**: ```typescript async create(resource: Vpc): Promise { // ... create resource ... return { providerId: 'vpc-0abc123', config: { cidr: '10.0.0.0/16', // SAME keys as read() region: 'us-east', distributeAcrossAzs: 2, name: 'main-vpc', }, outputs: { vpcId: 'vpc-0abc123', cidr: '10.0.0.0/16' }, }; } ``` ### refId / refIds Helpers Every resource manager with BaseResource references needs the same "convert to string ID" logic. Use these lightweight helpers: ```typescript function refId(ref: unknown): string | undefined { if (typeof ref === 'object' && ref !== null && 'id' in ref) return (ref as { id: string }).id; if (typeof ref === 'string') return ref; return undefined; } function refIds(refs: unknown[] | undefined): string[] { return refs?.map(r => refId(r)!).filter(Boolean) ?? []; } ``` ### provider.plan() Fallback The planner does **not** call `provider.plan()` — it uses `readState()` + `DiffEngine.diff()`. Provider `plan()` methods should still be implemented following the same pattern as a fallback for direct provider testing. ## Change Types | Change | Description | Action | |--------|-------------|--------| | `create` | In config, not in state | Call `provider.createResource()` | | `update` | In both, config differs | Call `provider.updateResource()` | | `replace` | Immutable property changed | `destroy()` + `create()` | | `destroy` | In state, not in config | Call `provider.destroyResource()` | | `no-op` | Config matches state | Skip | ## Drift Detection When `kyku plan` runs: 1. Resources with `providerId` in state → query cloud via `readState()` 2. Resources without `providerId` → always show `create` 3. If cloud state differs from desired config → show as `update` **Never** query the cloud for resources not in state — this breaks idempotency and causes spurious "no-op" results. --- # Engine The `KykuEngine` is the core orchestrator that drives the entire infrastructure lifecycle. It coordinates the dependency graph, diff engine, planner, state manager, crypto, and provider calls. ## Lifecycle ``` Config → Inject Auto-Subnets → Build DAG → Load State → Query Cloud → Diff → Plan → Apply → Persist State ``` ### Phase 1: Load Config ```typescript const engine = new KykuEngine({ stateDir: './.kyku' }); const plan = await engine.plan(config, { provider: awsProvider }); ``` The config is a TypeScript module that exports resource instances. The engine receives an array of `BaseResource` objects. ### Phase 2: Inject Auto-Subnets Before building the graph, the engine scans for Vpc resources with `distributeAcrossAzs > 0` and injects synthetic `Subnet` resources: ```typescript // Vpc with distributeAcrossAzs: 2 // Injects: vpc-main-subnet-public-0, vpc-main-subnet-public-1 // vpc-main-subnet-private-0, vpc-main-subnet-private-1 ``` These subnets appear in the dependency graph, state file, and outputs like any other resource. ### Phase 3: Build Dependency Graph The `GraphBuilder` scans every resource property for `BaseResource` instances. A reference like `vm.network = myVpc` creates a `Vm → Vpc` edge. The graph assigns levels via topological sort. ### Phase 4: Load State Reads `.kyku/state..json` for last known cloud provider IDs and configs. If no state file exists, all resources are considered new. ### Phase 5: Discover Cloud State For resources with a `providerId` in state, calls `provider.readState(resource, providerId)` to detect drift. Resources without a `providerId` skip this step. ### Phase 6: Diff The `DiffEngine` compares desired config (from `resourceToConfig()`) vs actual config (from `readState`). Changes are categorized: | Change Type | Description | |-------------|-------------| | `create` | Resource exists in config but not in state | | `update` | Config differs from cloud state | | `replace` | Immutable property changed (destroy + recreate) | | `destroy` | Resource exists in state but not in config | | `no-op` | Config matches cloud state | ### Phase 7: Plan The planner produces an ordered list of changes grouped by graph level. Resources at the same level run in parallel. ### Phase 8: Apply Executes changes in dependency order, reporting progress in real-time. Each change calls the appropriate provider method: - `create` → `provider.createResource(resource)` - `update` → `provider.updateResource(resource)` - `replace` → `provider.destroyResource()` + `provider.createResource()` - `destroy` → `provider.destroyResource()` ### Phase 9: Persist State After applying, the new provider IDs, configs, and outputs are written to the state file. A timestamped backup of the previous state is saved before every apply. ## Engine API ```typescript class KykuEngine { constructor(options: EngineOptions); // Lifecycle methods plan(config: ResourceConfig[], options: PlanOptions): Promise; apply(config: ResourceConfig[], options: ApplyOptions, plan?: Plan): Promise; destroy(options: DestroyOptions): Promise; refresh(options: RefreshOptions): Promise; // State access readState(env?: string): Promise; getOutputs(options: OutputOptions): Promise>; // Graph buildGraph(config: ResourceConfig[]): Graph; } ``` ### EngineOptions | Option | Type | Default | Description | |--------|------|---------|-------------| | `stateDir` | `string` | `./.kyku` | Directory for local state files | | `backend` | `StateBackendConfig` | — | Optional remote backend (`s3`, `gcs`, `spaces`, or `http`) | | `deployPrefix` | `string` | random 8-char hex | Prefix for resource names | ### PlanOptions | Option | Type | Default | Description | |--------|------|---------|-------------| | `provider` | `Provider` | required | Provider implementation | | `env` | `string` | `''` | Environment workspace | ### ApplyOptions | Option | Type | Default | Description | |--------|------|---------|-------------| | `provider` | `Provider` | required | Provider implementation | | `autoApprove` | `boolean` | `false` | Skip confirmation prompt | | `parallelism` | `number` | `10` | Max concurrent operations | | `env` | `string` | `''` | Environment workspace | ## Deploy Prefix Every deployment gets a unique 8-char hex prefix, auto-generated on first `plan`/`apply` and stored in `StateFile.metadata.prefix`. Resource names are wrapped with a **deep Proxy** that prepends `prefix-` to `name` for new resources while keeping existing resource names unchanged. ## State Locking Local backend: file lock at `{stateDir}/.kyku.lock` with PID + timestamp. S3 backend: lock object at `${key}.lock` in the configured bucket. Locks are auto-released on success/error via try/finally. A 60s heartbeat refreshes the timestamp; stale locks (>5 minutes) are broken only when a re-read still shows the same token. Default 30s timeout for acquiring lock. ## Error Handling | Phase | Behavior | |-------|----------| | Validation | Config errors surface immediately with file/line info | | Plan | Cloud API errors during discovery abort the plan | | Apply | On failure, partial state is preserved. Resources that succeeded remain applied. | | Destroy | Retry with backoff (5 attempts, 2^n × 2s). Verify deletion with poll (10 attempts, 2s). | ## Progress Display Kyku uses ANSI cursor control for in-place terminal updates: - `\x1b[1A` (cursor up) + `\x1b[2K` (clear line) for redraw - Tracks exact `linesPrinted` count - Spinner animation at ~80ms via `setInterval` - No leading/trailing newlines in render to avoid scroll accumulation --- # Planner The `Planner` takes the diff output and the dependency graph and produces an ordered execution plan. It groups changes by graph level for parallel execution. ## generatePlan() ```typescript class Planner { generatePlan(config: Config, state: StateFile, graph: Graph, diffs: DiffResult): Plan } ``` ### Inputs | Input | Source | Description | |-------|--------|-------------| | `config` | User's `infrastructure.ts` | Desired resource definitions | | `state` | `.kyku/state..json` | Last known cloud state | | `graph` | `GraphBuilder.build()` | Dependency graph with levels | | `diffs` | `DiffEngine.diff()` | Changes per resource | ### Output ```typescript interface Plan { changes: Change[]; // Ordered by graph level counters: PlanCounters; // Summary: creates, updates, replaces, destroys graph: Graph; // Dependency graph for reference environment: string; // Workspace name } interface PlanCounters { create: number; update: number; replace: number; destroy: number; noop: number; } ``` ## State vs Cloud Logic The planner decides what's new vs existing based on state: - **Has `providerId` in state** → queries cloud via `provider.readState(id, providerId)` to detect drift - **No `providerId` in state** → always shows `create` (never queries cloud) This is critical for idempotency. Querying the cloud for resources not in state would cause spurious "no-op" results. ## Ordering Changes are ordered by graph level: ``` Level 0: [create Vpc, create SshKey] ← parallel Level 1: [create Subnet, create SecurityGroup] Level 2: [create Vm, create Database] Level 3: [create LoadBalancer] ``` Each level waits for all lower levels to complete. Resources within a level execute in parallel (up to `--parallelism` limit). ## Destroy Order Destroy follows reverse level order: ``` Level 3: [destroy LoadBalancer] Level 2: [destroy Vm, destroy Database] Level 1: [destroy SecurityGroup, destroy Subnet] Level 0: [destroy Vpc, destroy SshKey] ``` ## Plan Save/Load Plans can be serialized to JSON files and loaded later: ```bash # Save plan kyku plan --out plan.json # Apply from saved plan kyku apply --plan plan.json ``` The engine's `apply()` accepts an optional `plan?: Plan` 4th parameter — if provided, it uses pre-computed changes and skips the `plan()` call. ## Idempotent Create Every provider `create()` method checks if the resource already exists before creating: ```typescript async create(resource: Vpc): Promise { const existing = await client.get(`/resources?name=${resource.name}`); if (existing.resources?.length > 0) { return this.read(resource, String(existing.resources[0].id)); } // ... create new } ``` **Exception:** Purely logical resources (e.g., `TargetGroup` that just defines a label) don't need this check. ## Provider ID Management ### Critical Rule Never pass Kyku resource UUIDs to cloud APIs. | ID Type | Format | Example | |---------|--------|---------| | Kyku ID | UUID/string | `"hetzner-test"` or `"p1s9hutn"` | | Cloud ID | Provider-native | `12224210` (Hetzner), `vpc-0abc123` (AWS) | The `readState` by name is the fallback for resolving cloud IDs from Kyku references. ### Destroy Provider ID Resolution If `stateResource.providerId` is non-numeric (stale Kyku ID from early versions), look it up via `provider.readState(stub)` before passing to `provider.destroy()`. ## Plan Output Format Human-readable plan output: ``` Plan: 3 to create, 1 to update, 0 to replace, 1 to destroy, 2 no-op Changes: + create Vpc "main-vpc" (level 0) + create SecurityGroup "web-sg" (level 1) + create Vm "web-server" (level 2) ~ update LoadBalancer "web-lb" (level 3) port: 80 → 443 - destroy Vm "old-worker" (level 2) ✓ no-op Database "app-db" ``` JSON output with `--json` flag: ```json { "changes": [ { "action": "create", "resourceId": "vpc-main", "level": 0 }, { "action": "create", "resourceId": "web-sg", "level": 1 } ], "counters": { "create": 3, "update": 1, "destroy": 1, "replace": 0, "noop": 2 } } ``` --- # AWS The AWS provider maps Kyku abstract resources to native AWS services via the AWS SDK v3 (`@aws-sdk/client-ec2`, `@aws-sdk/client-rds`, `@aws-sdk/client-iam`, `@aws-sdk/client-elastic-load-balancing-v2`, `@aws-sdk/client-s3`, `@aws-sdk/client-route-53`). ## Credentials ```bash # Env vars (standard) export AWS_REGION=us-east-1 export AWS_ACCESS_KEY_ID=... export AWS_SECRET_ACCESS_KEY=... # Or via AWS SSO aws sso login --profile my-profile export AWS_PROFILE=my-profile # OIDC for CI/CD export KYKU_AWS_ROLE_ARN=arn:aws:iam::123456789012:role/KykuDeployRole ``` Kyku auto-detects CI platforms (GitHub Actions, GitLab CI, CircleCI) and exchanges OIDC tokens via `STS AssumeRoleWithWebIdentity` — no long-lived keys needed. ## Supported Resources | Resource | AWS Service | Status | |----------|-------------|--------| | **Vpc** | EC2-VPC | ✅ | | **Subnet** | EC2-VPC | ✅ | | **Vm** | EC2 | ✅ | | **SecurityGroup** | EC2-SG | ✅ | | **LoadBalancer** | ALB / NLB | ✅ | | **TargetGroup** | ELBv2 TG | ✅ | | **Database** | RDS | ✅ | | **Identity** | IAM User | ✅ | | **Role** | IAM Role | ✅ | | **SshKey** | Key Pair | ✅ | | **Bucket** | S3 | ✅ | | **DnsZone** | Route53 | ✅ | | **DnsRecord** | Route53 | ✅ | | **Custom** | wired | ✅ | ## Instance Type Mapping | Abstract | AWS | |----------|-----| | `micro` | t3.micro | | `small` | t3.small | | `medium` | t3.medium | | `large` | t3.large | | `xlarge` | t3.xlarge | | `2xlarge` | t3.2xlarge | | `4xlarge` | m6i.4xlarge | | `8xlarge` | m6i.8xlarge | RDS uses a separate mapping with `db.*` prefix: | Abstract | RDS | |----------|-----| | `micro` | db.t3.micro | | `small` | db.t3.small | | `medium` | db.t3.medium | | `large` | db.t3.large | | `xlarge` | db.t3.xlarge | | `2xlarge` | db.t3.2xlarge | | `4xlarge` | db.m6i.4xlarge | | `8xlarge` | db.m6i.8xlarge | Override with provider-specific instance types: ```typescript instanceType: { aws: 'm5.large', gcp: 'n2-standard-4' } ``` ## Image Mapping | Abstract | AWS AMI | |----------|---------| | `ubuntu-22.04` | ami-0c7217cdde317cfec | | `ubuntu-24.04` | ami-04a4fb15521f51d19 | | `debian-12` | ami-0c55b159cbfafe1f0 | Override with a custom AMI ID: ```typescript image: { aws: 'ami-0custom123' } ``` ## Region Mapping | Abstract | AWS | |----------|-----| | `us-east` | us-east-1 | | `us-west` | us-west-2 | | `eu-central` | eu-central-1 | | `eu-west` | eu-west-1 | | `ap-southeast` | ap-southeast-1 | | `ap-south` | ap-south-1 | | `ap-northeast` | ap-northeast-1 | | `ca-east` | ca-central-1 | Override with a provider-specific region: ```typescript region: { aws: 'eu-west-2', gcp: 'europe-west2' } ``` ## Gotchas ### Custom Config Every resource manager accepts a `customConfig` object merged directly into the underlying AWS SDK request — see [Custom Config](/concepts/custom-config/). AWS has the broadest typed-alias coverage of any provider: `@kykucloud/aws` exports an `Omit` alias (`AwsVpcCustomConfig`, `AwsVmCustomConfig`, `AwsSecurityGroupCustomConfig`, …) for every resource manager except `DnsRecord` (its request body is 100% Kyku-owned) and `TargetGroup` (a synthetic tag-based resource with no AWS create call of its own). Vpc and Vm also have real update routing, not just create-time merging: Vpc's `EnableDnsHostnames`/`EnableDnsSupport`/`EnableNetworkAddressUsageMetrics` route through `ModifyVpcAttribute`; Vm's termination/stop protection, monitoring, source/dest check, shutdown behavior, and instance metadata options route through their respective EC2 attribute APIs. `Vpc.customConfig` also exposes `subnet`/`natGateway`/`routeTable`/`internetGateway` namespaces (flat objects applying to every child object of that type Kyku creates) so you can configure the rest of the network stack Kyku provisions alongside the VPC — see [Custom Config](/concepts/custom-config/). `Vm.userData` is a first-class portable field now (base64-encoded into `RunInstances` automatically); it's immutable after launch, same as AWS itself requires. ### SDK v3 Error Names Each AWS service uses different error names. Kyku catches per-service errors: | Service | Error Name | |---------|-----------| | VPC | `InvalidVpcID.NotFound` | | SecurityGroup | `InvalidGroup.NotFound` | | LoadBalancer | `LoadBalancerNotFound` | | RDS | `DBInstanceNotFound` | | IAM | `NoSuchEntity` | | KeyPair | `InvalidKeyPair.NotFound` | ### Name-Based Lookups Each resource type has a different lookup pattern: | Resource | Lookup Method | |----------|---------------| | VPC | `DescribeVpcs` + `tag:Name` filter | | EC2 | `DescribeInstances` + `tag:Name` filter | | SecurityGroup | `DescribeSecurityGroups` + `group-name` filter | | ALB/NLB | `DescribeLoadBalancers` + `Names: [name]` | | IAM | `GetUser(UserName)` / `GetRole(RoleName)` | | RDS | `DescribeDBInstances` (list all, filter client-side) | | KeyPairs | `DescribeKeyPairs` + `KeyNames: [name]` | ### VPC Auto-Creates Full Network Stack When you create a Vpc, Kyku automatically provisions: - Internet Gateway - NAT Gateways (one per public subnet, incurring ongoing costs) - Elastic IPs (one per NAT Gateway) - Public + private route tables **Destroy** cleans up in reverse order: route tables → NATs + EIPs → IGWs → subnets → VPC. ### SSH Keys are Key Pairs AWS treats SSH keys as EC2 Key Pairs. Kyku uses `ImportKeyPairCommand` to upload the public key. The key name (not public key content) is passed to `RunInstances.KeyName`. Keys must be pre-imported before VM creation. ### ALB Name Limit ALB names are limited to **32 characters** (not 255 like other AWS resources). Kyku validates this in `validateLoadBalancer`. ### RDS Polling RDS creation is slow — Kyku polls up to **120 attempts × 5s = 600s (10 minutes)**. RDS instance types use a separate `RDS_INSTANCE_TYPE_MAP` (`db.t3.*` prefix), not the standard EC2 mapping. ### RDS DB Subnet Group RDS requires a DB subnet group for VPC placement. Kyku auto-creates this via `CreateDBSubnetGroupCommand` using the private subnets from the VPC. ### Security Group Rule Mapping AWS IpPermissions format differs from Kyku's rule model. Kyku implements `fromAwsIpPermissions()` to reverse-map AWS `IpPermissions` / `IpPermissionsEgress` back to the Kyku `SecurityGroupRule[]` format for config comparison. ### IAM Policy Cleanup Before deleting IAM users/roles, Kyku must detach managed policies (`DetachUserPolicy`) and delete inline policies (`DeleteUserPolicy`). Each step catches `NoSuchEntity`. ### Immutable Properties Changes to these properties trigger destroy + recreate: | Resource | Immutable | |----------|-----------| | Vm | `network` (can't move between VPCs) | | SecurityGroup | `name` (AWS can't rename SGs) | | Identity | `name` (IAM rename is separate) | | Role | `name` (IAM rename is separate) | ### Tags Format AWS uses `[{Key: string, Value: string}]` arrays. The `Name` tag is set automatically via `toAwsTags(tags, name)`. ### AZ Distribution `distributeAcrossAzs: N` creates N public subnets + N private subnets (one pair per AZ). The AWS provider always creates a paired public/private topology. --- # DigitalOcean The DigitalOcean provider maps Kyku abstract resources to DigitalOcean services via the DO REST API and AWS S3 SDK for Spaces. ## Credentials ```bash # DigitalOcean API (required) export DIGITALOCEAN_TOKEN=your-api-token # DO Spaces (optional, for Bucket) export DO_SPACES_REGION=nyc3 export DO_SPACES_ACCESS_KEY=your-access-key export DO_SPACES_SECRET_KEY=your-secret-key ``` | Service | Backend | Auth Method | |---------|---------|-------------| | Cloud API | REST at `api.digitalocean.com/v2` | `Authorization: Bearer DIGITALOCEAN_TOKEN` | | Spaces | `@aws-sdk/client-s3` w/ endpoint override | AWS SigV4 | ## Supported Resources | Resource | DO Service | Status | |----------|------------|--------| | **Vpc** | VPC | ✅ | | **Subnet** | VPC (synthetic) | ✅ | | **Vm** | Droplet | ✅ | | **SecurityGroup** | Cloud Firewall | ✅ | | **LoadBalancer** | Load Balancer | ✅ | | **TargetGroup** | LB Target Group | ✅ | | **Database** | Managed DB | ✅ | | **Identity** | — | ❌ | | **Role** | — | ❌ | | **SshKey** | SSH Key | ✅ | | **Bucket** | Spaces (S3-compat) | ✅ | | **DnsZone** | Domain | ✅ | | **DnsRecord** | Record | ✅ | | **Custom** | wired | ✅ | ## Instance Type Mapping | Abstract | DigitalOcean | |----------|--------------| | `micro` | s-1vcpu-1gb | | `small` | s-1vcpu-2gb | | `medium` | s-2vcpu-4gb | | `large` | s-4vcpu-8gb | | `xlarge` | s-8vcpu-16gb | | `2xlarge` | g-8vcpu-32gb | | `4xlarge` | g-16vcpu-64gb | | `8xlarge` | g-32vcpu-128gb | ## Image Mapping | Abstract | DigitalOcean | |----------|--------------| | `ubuntu-22.04` | ubuntu-22-04-x64 | | `ubuntu-24.04` | ubuntu-24-04-x64 | | `debian-12` | debian-12-x64 | ## Region Mapping | Abstract | DigitalOcean | |----------|--------------| | `us-east` | nyc3 | | `us-west` | sfo3 | | `eu-central` | fra1 | | `eu-west` | lon1 | | `ap-southeast` | sgp1 | | `ap-south` | blr1 | | `ap-northeast` | syd1 | | `ca-east` | tor1 | ## Gotchas ### Custom Config Every resource manager accepts a `customConfig` object merged directly into the underlying DigitalOcean API request — see [Custom Config](/concepts/custom-config/). `@kykucloud/digitalocean` exports `DigitaloceanVpcCustomConfig`, `DigitaloceanVmCustomConfig`, `DigitaloceanLoadBalancerCustomConfig`, `DigitaloceanDatabaseCustomConfig`, `DigitaloceanDnsRecordCustomConfig`, and `DigitaloceanKubernetesClusterCustomConfig`, generated at build time from a vendored slice of the official `digitalocean/openapi` spec (`bun run generate:custom-config` regenerates them; `bun run check:custom-config` verifies the committed output is current). Fields use the spec's own snake_case casing (`ip_range`, `vpc_uuid`, …), matching what the provider actually sends over the wire — there's no camelCase mapping layer. SecurityGroup, SshKey, and DnsZone reduce to an empty type once Kyku-owned fields are excluded (documented rather than typed); Bucket (Spaces, separate API) and TargetGroup (synthetic) don't have one either. Droplet updates now route through real Droplet Actions: `backups`/`backup_policy`/`ipv6`/`resize`. IPv6 enable is one-way (DO doesn't support disabling it again — attempting to throws); disk resize is grow-only and needs the droplet powered off first. Kernel changes are deliberately not wired — Kyku droplets use internal kernels, not the legacy externally-managed-kernel model. ### Tags are Flat Strings DigitalOcean tags are flat strings (`key:value` format), not key-value objects. Use the `tagValue('kyku-sg', id)` helper. Max tag length is 255 chars. ### Firewall Ports Format Firewall rules use a `ports` string field: | Port Spec | Example | |-----------|---------| | Single port | `"22"` | | Range | `"80-443"` | | All ports | `"all"` | | ICMP | Omit `ports` entirely | Kyku maps `fromPort`/`toPort` to the DO `ports` format automatically. ### Firewall Dynamic Attachment via Tags Droplets are tagged with `kyku-sg:` at creation. Firewall rules reference these tags in `sources.tags` / `tags` instead of individual droplet IDs. ### LB Sizing Uses `size_unit` DO Load Balancers use `size_unit` (integer, 1–100), not named sizes. Kyku maps abstract `size` config to `size_unit` with a default of 1. ### Managed DB Engine Names DO uses different engine names than the abstract names: | Abstract | DO API | |----------|--------| | `postgresql` | `pg` | | `mysql` | `mysql` | | `mariadb` | `mariadb` | | `redis` | `redis` | Kyku maps these in `create()` before POST to `/v2/databases`. ### DB Private Networking Managed Databases require `private_network_uuid` for VPC placement. Kyku resolves the VPC UUID and includes it in the create body. ### VPC CIDR Must Be Private Range DO VPCs only accept private CIDR blocks: `10.x`, `172.16-31.x`, or `192.168.x`. Kyku validates this before creation. ### Name Limit DigitalOcean names allow up to **255 chars** (not 63 like Hetzner). Use `slice(0, 255)` — no aggressive truncation needed. ### `process.env` in Bun DO provider uses `(globalThis as any).process?.env?.VAR` for env var access instead of `@types/node`. --- # GCP The GCP provider maps Kyku abstract resources to Google Cloud Platform services via REST APIs (Compute Engine, Cloud SQL, Cloud DNS, Cloud Storage, IAM, Cloud Load Balancing). ## Credentials ```bash # Application Default Credentials (recommended) gcloud auth application-default login # Service account key file export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-key.json export GOOGLE_PROJECT_ID=my-project # OIDC for CI/CD export KYKU_GCP_WORKLOAD_PROVIDER=projects/123/locations/global/workloadIdentityPools/my-pool/providers/github export KYKU_GCP_SERVICE_ACCOUNT=deployer@my-project.iam.gserviceaccount.com export KYKU_GCP_PROJECT_ID=my-project ``` Kyku uses `google-auth-library` for ADC and OIDC token exchange. Tokens are cached with a 1-minute margin before expiry. ## Supported Resources | Resource | GCP Service | Status | |----------|-------------|--------| | **Vpc** | VPC Network | ✅ | | **Subnet** | VPC Network (synthetic) | ✅ | | **Vm** | Compute Engine | ✅ | | **SecurityGroup** | Firewall Rules | ✅ | | **LoadBalancer** | Cloud LB stack (5+ resources) | ✅ | | **TargetGroup** | — | ❌ | | **Database** | Cloud SQL | ✅ | | **Identity** | Service Account | ✅ | | **Role** | Custom Role | ✅ | | **SshKey** | — | ❌ | | **Bucket** | Cloud Storage | ✅ | | **DnsZone** | Cloud DNS | ✅ | | **DnsRecord** | Cloud DNS | ✅ | | **Custom** | wired | ✅ | ## Instance Type Mapping | Abstract | GCP Compute | GCP Cloud SQL | |----------|-------------|---------------| | `micro` | e2-micro | db-f1-micro | | `small` | e2-small | db-g1-small | | `medium` | e2-medium | db-custom-2-8192 | | `large` | e2-standard-4 | db-custom-4-16384 | | `xlarge` | e2-standard-8 | db-custom-8-32768 | | `2xlarge` | e2-standard-16 | db-custom-16-65536 | | `4xlarge` | e2-standard-32 | db-custom-32-131072 | | `8xlarge` | n2-standard-48 | db-custom-48-196608 | Cloud SQL uses a separate `CLOUD_SQL_INSTANCE_MAP` (compute types vs DB tiers). ## Image Mapping | Abstract | GCP Image | |----------|-----------| | `ubuntu-22.04` | ubuntu-2204-jammy-v20240101 | | `ubuntu-24.04` | ubuntu-2404-noble-v20240101 | | `debian-12` | debian-12-bookworm-v20240101 | ## Region Mapping GCP uses provider-native region names directly (no abstract mapping layer). Abstract regions (`us-east`, `eu-central`) are passed through as-is because GCP region names like `us-central1`, `europe-west3` already follow the same pattern. Override with provider-specific region: ```typescript region: { gcp: 'us-west1' } ``` ## Gotchas ### Custom Config Every resource manager accepts a `customConfig` object merged directly into the underlying GCP request body — see [Custom Config](/concepts/custom-config/). `@kykucloud/gcp` exports typed aliases for nearly every resource: `GcpVpcCustomConfig`/`GcpVmCustomConfig`/`GcpSecurityGroupCustomConfig`/`GcpVolumeCustomConfig`/`GcpAutoScalingGroupCustomConfig` from `@google-cloud/compute`, plus `GcpBucketCustomConfig`/`GcpDnsZoneCustomConfig`/`GcpSecretCustomConfig`/`GcpKmsKeyCustomConfig`/`GcpQueueCustomConfig`/`GcpCacheCustomConfig`/`GcpKubernetesClusterCustomConfig`/`GcpCertificateCustomConfig`/`GcpDatabaseCustomConfig`/`GcpLoadBalancerCustomConfig` from dedicated `@google-cloud/*` SDK deps added purely for their request types (the GCP provider itself still calls the REST API directly, not through these SDKs). `Identity`/`Role` are the one gap — no clean client library covers IAM Admin's service-account/custom-role request shapes, only IAM policy APIs. Vpc/Vm also have real update routing now, not just create-time merging: VPC's `description`/`mtu`/`routingConfig`/`networkFirewallPolicyEnforcementOrder`/ULA-IPv6 fields route through the network PATCH; VM's `metadata` (including `startup-script` for `vm.userData`), `scheduling`, `deletionProtection`, and shielded-instance integrity policy route through their respective GCP setters (fetching the current fingerprint first, where GCP requires one). Fields that require a `TERMINATED` instance (service account, shielded config, display device, machine type) stay create-only. ### Multi-Resource LB Stack GCP Load Balancers require **5+ resources** to be created in order: 1. Health check 2. Backend service 3. URL map 4. Target HTTP/HTTPS proxy 5. Forwarding rule Destroy follows the reverse order. Kyku handles this transparently within the `LoadBalancerManager`. ### Cloud SQL is Slow Cloud SQL instance creation takes **10–15 minutes**. Kyku polls with **120 attempts × 5s = 600s timeout**. Use `--parallelism 1` to avoid overwhelming the sqladmin API. ### Cloud SQL Uses Separate API Cloud SQL uses `sqladmin.googleapis.com` (not the Compute Engine API). Kyku constructs URLs as `https://sqladmin.googleapis.com/v1/projects/{project}/instances`. ### Firewall Rules: One Per Ingress/Express Entry GCP does not support multiple rules in a single firewall resource. Kyku creates separate firewall resources named `${sg.name}-ingress-0`, `${sg.name}-ingress-1`, etc. Destroy removes all by name prefix. ### Firewall Rule Format GCP firewall rules use network URLs and target tags: ```typescript network: `${baseUrl}/global/networks/${name}` targetTags: [sg.name] direction: 'INGRESS' | 'EGRESS' ``` ### Unsupported Resources - **TargetGroup**: No GCP equivalent — use managed instance groups and throw `UnsupportedFeatureError`. - **SshKey**: GCP stores SSH keys in project-level metadata. Use `vm.sshPublicKey` directly (set in instance metadata via `ssh-keys` key). ### Naming Constraints | Constraint | Limit | |------------|-------| | Max length | 62 chars | | Allowed chars | `[a-z0-9-]` | | Normalizer | `normalizeGcpName()` strips invalid chars | ### Labels Format GCP labels use `{key: value}` with strict limits: - Keys: ≤ 62 chars, `[a-z0-9_-]` - Values: ≤ 63 chars, `[a-z0-9_-]` The `toGcpLabels()` helper normalizes all labels. Labels are separate from firewall `targetTags`. ### IAM Uses Separate API Base IAM operations use `iam.googleapis.com`: - Service Accounts: `POST /v1/projects/{project}/serviceAccounts` - Custom Roles: `POST /v1/projects/{project}/roles` ### Operations API Polling POST/PATCH/DELETE return a `selfLink`. Poll with GET every 2s until `status === 'DONE'`. ### Subnet is Synthetic GCP subnet resources are auto-injected by the planner (no standalone cloud resource). The `SubnetManager.readState` checks if the parent VPC exists. Create/destroy are no-ops handled by `VpcManager`. ### `process.env` in Bun GCP provider uses `(globalThis as any).process?.env?.VAR` for env var access instead of `@types/node`. --- # Hetzner The Hetzner provider maps Kyku abstract resources to Hetzner Cloud API services, plus Hetzner DNS and Hetzner S3-compatible object storage via separate APIs. ## Credentials ```bash # Hetzner Cloud API (required) export HCLOUD_TOKEN=your-cloud-token # Hetzner DNS (optional, for DnsZone/DnsRecord) export HETZNER_DNS_TOKEN=your-dns-token # Hetzner S3 (optional, for Bucket) export HETZNER_S3_REGION=fsn1 export HETZNER_S3_ACCESS_KEY=your-access-key export HETZNER_S3_SECRET_KEY=your-secret-key ``` Provider HTTP used by this client goes through `requestJson`. Set `KYKU_RECORD=1` / `KYKU_REPLAY=1` to record or replay it; see [HAR record and replay](/guides/record-replay/). | Service | Backend | Auth Method | |---------|---------|-------------| | Cloud API | REST at `api.hetzner.cloud/v1` | `Authorization: Bearer HCLOUD_TOKEN` | | DNS API | REST at `dns.hetzner.com/api/v1` | `Auth-API-Token` header | | S3-compat | `@aws-sdk/client-s3` w/ endpoint override | AWS SigV4 | ## Supported Resources | Resource | Hetzner Service | Status | |----------|-----------------|--------| | **Vpc** | Network | ✅ | | **Subnet** | Network Subnet (synthetic) | ✅ | | **Vm** | Cloud Server | ✅ | | **SecurityGroup** | Firewall | ✅ | | **LoadBalancer** | Load Balancer | ✅ | | **TargetGroup** | LB Target Group | ✅ | | **Database** | — | ❌ | | **Identity** | — | ❌ | | **Role** | — | ❌ | | **SshKey** | SSH Key | ✅ | | **Bucket** | S3-compat (Object Storage) | ✅ | | **DnsZone** | DNS API | ✅ | | **DnsRecord** | DNS API | ✅ | | **Custom** | wired | ✅ | ## Instance Type Mapping | Abstract | Hetzner | |----------|---------| | `micro` | cax11 | | `small` | cax21 | | `medium` | cax31 | | `large` | cax41 | | `xlarge` | ccx13 | | `2xlarge` | ccx33 | | `4xlarge` | ccx43 | | `8xlarge` | ccx53 | > **Note:** Older instance families (cx11, cx21, etc.) are deprecated in favor of Arm-based CAX and CCX series. The abstract mapping uses current-generation types. ## Image Mapping | Abstract | Hetzner | |----------|---------| | `ubuntu-22.04` | ubuntu-22.04 | | `ubuntu-24.04` | ubuntu-24.04 | | `debian-12` | debian-12 | ## Region Mapping Hetzner uses abstract location names. Regions map directly to locations: | Abstract | Hetzner Location | |----------|-----------------| | `us-east` | nbg1 | | `us-west` | hil1 | | `eu-central` | fsn1 | | `eu-west` | nbg1 | > Hetzner locations: nbg1 (Nuremberg), fsn1 (Falkenstein), hil1 (Helsinki). ## Gotchas ### Custom Config Every resource manager accepts a `customConfig` object merged directly into the underlying Hetzner API request — see [Custom Config](/concepts/custom-config/). `@kykucloud/hetzner` exports `HetznerVpcCustomConfig`, `HetznerVmCustomConfig`, `HetznerSecurityGroupCustomConfig`, `HetznerLoadBalancerCustomConfig`, and `HetznerSshKeyCustomConfig`, generated at build time from Hetzner's official `cloud.spec.json` OpenAPI document (`bun run generate:custom-config` regenerates them; `bun run check:custom-config` verifies the committed output is current). Bucket (separate Object Storage API), DnsZone/DnsRecord (legacy DNS API, not in the cloud spec), KubernetesCluster (no native managed K8s), and TargetGroup (synthetic) don't have — and can't usefully have — a generated alias. Vpc/Vm also route real updates through Hetzner's action endpoints now: Network's `subnet`/`route`/`delete_protection` via `add_subnet`/`add_route`/`change_protection`, Server's `backups`/`protection`/`placement_group`/`dns_ptr` via their respective actions. One-shot commands (reboot, rescue, reset password, image creation) are deliberately not wired to `customConfig` — they aren't declarative desired state. ### Network Zones are Abstract Hetzner network zones use abstract names (`eu-central`) — not location names (`fsn1`, `nbg1`). Always use abstract zone identifiers. ### Server Needs Subnet First Before attaching a server to a network, a subnet must exist in that network. Always set `distributeAcrossAzs >= 1` on Vpc resources to auto-create subnets. ### LB Network Attachment is Async After creating a Load Balancer with network attachment, poll the `private_net` field via GET before adding targets. This can take up to 120s. ### `use_private_ip` + Label Selectors Don't Mix Setting `use_private_ip: true` alongside `label_selector` targets causes an API error. Omit `use_private_ip` when using label selectors. ### Target Format ```typescript // Add target with label selector { type: "label_selector", label_selector: { selector: "kyku-tg=tg-web" } } // Remove from resources { remove_from: [{ type: "server", server: { id: 12345 } }] } ``` ### Firewall Destroy Fails with "Still In Use" Before deleting a firewall, PUT `{ applied_to: [] }` to clear all label selectors. Otherwise the API rejects deletion. ### Load Balancer POST Doesn't Return `private_net` The POST response for `/load_balancers` does not include the `private_net` field. Must poll GET to confirm network attachment. ### Name Uniqueness Hetzner enforces name uniqueness across all resources in a project. Kyku checks by name before creating. ### Name Length Max 63 chars (`[a-z0-9-]`). With the 9-char deploy prefix (8 hex + dash), effective limit is 54 chars for user-provided names. ### Deprecated Server Types The old cx11/cx21/cx31 series are deprecated in favor of cax11/cax21/cax31 (Arm). The abstract mapping already uses current-gen types. --- # Provider Overview Kyku supports four cloud providers with a unified resource API. Each provider supports a subset of resource types. Unsupported resources throw `UnsupportedFeatureError` at validation time. ## Parity Matrix | Resource | AWS | GCP | Hetzner | DigitalOcean | |----------|-----|-----|---------|-------------| | **Vpc** | ✅ EC2-VPC | ✅ VPC Network | ✅ Network | ✅ VPC | | **Subnet** | ✅ EC2-VPC | ✅ (synthetic) | ✅ (synthetic) | ✅ (synthetic) | | **Vm** | ✅ EC2 | ✅ Compute Engine | ✅ Cloud Server | ✅ Droplet | | **SecurityGroup** | ✅ EC2-SG | ✅ Firewall Rules | ✅ Firewall | ✅ Cloud Firewall | | **LoadBalancer** | ✅ ALB/NLB | ✅ LB stack | ✅ LB | ✅ LB | | **TargetGroup** | ✅ TG | ❌ | ✅ TG | ✅ TG | | **Database** | ✅ RDS | ✅ Cloud SQL | ❌ | ✅ Managed DB | | **Identity** | ✅ IAM User | ✅ Service Account | ❌ | ❌ | | **Role** | ✅ IAM Role | ✅ Custom Role | ❌ | ❌ | | **SshKey** | ✅ Key Pair | ❌ | ✅ SSH Key | ✅ SSH Key | | **Bucket** | ✅ S3 | ✅ Cloud Storage | ✅ S3-compat | ✅ Spaces S3 | | **DnsZone** | ✅ Route53 | ✅ Cloud DNS | ✅ DNS API | ✅ Domain | | **DnsRecord** | ✅ Route53 | ✅ Cloud DNS | ✅ DNS API | ✅ Record | | **Custom** | ✅ wired | ✅ wired | ✅ wired | ✅ wired | ## Credentials Required | Provider | Env vars | Source | |----------|----------|--------| | **AWS** | `AWS_REGION`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` | IAM user or OIDC | | **GCP** | `GOOGLE_PROJECT_ID`, `GOOGLE_APPLICATION_CREDENTIALS` | Service account key or ADC | | **Hetzner** | `HCLOUD_TOKEN` | Cloud Console → API Tokens | | **Hetzner DNS** | `HETZNER_DNS_TOKEN` | DNS Console → API Tokens | | **Hetzner S3** | `HETZNER_S3_REGION`, `HETZNER_S3_ACCESS_KEY`, `HETZNER_S3_SECRET_KEY` | Console → Security → S3 Credentials | | **DigitalOcean** | `DIGITALOCEAN_TOKEN` | API → Tokens | | **DO Spaces** | `DO_SPACES_REGION`, `DO_SPACES_ACCESS_KEY`, `DO_SPACES_SECRET_KEY` | API → Spaces | ## Notes - **GCP TargetGroup**: Not supported — use managed instance groups instead. - **GCP SshKey**: Not supported — use `sshPublicKey` field directly on Vm. - **Hetzner Database/Identity/Role**: Not available in Hetzner Cloud API. - **DO Identity/Role**: Not available in DigitalOcean API. - **Subnet**: Synthetic resource — auto-injected by the planner; no standalone cloud resource. - **Bucket (Hetzner S3 & DO Spaces)**: Uses `@aws-sdk/client-s3` with endpoint override + SigV4 auth. ## Authentication Strategies | Strategy | AWS | GCP | Hetzner | DO | |----------|-----|-----|---------|----| | Env vars | ✅ | ✅ | ✅ | ✅ | | OIDC (CI) | ✅ STS AssumeRoleWithWebIdentity | ✅ Workload Identity Federation | ❌ | ❌ | | SSO | ✅ aws sso login | ✅ gcloud auth | ❌ | ❌ | | Token file | ❌ | ✅ ADC file | ✅ HCLOUD_TOKEN | ✅ DIGITALOCEAN_TOKEN | --- # Kubernetes The Kubernetes provider manages resources inside Kubernetes clusters — namespaces, deployments, services, ingresses, ConfigMaps, Secrets, PersistentVolumeClaims, and Helm releases. ## Credentials The Kubernetes provider uses your local kubeconfig, just like `kubectl`: ```bash export KUBECONFIG=/path/to/kubeconfig # optional, defaults to ~/.kube/config ``` Or use an in-cluster config when running inside a pod: ```bash # In-cluster config is auto-detected when KUBECONFIG is unset ``` ## Architecture Kyku communicates with the Kubernetes API server directly via the [`@kubernetes/client-node`](https://github.com/kubernetes-client/javascript) SDK — no `kubectl` subprocesses. Resource managers translate Kyku resource types to native Kubernetes API objects (`apps/v1.Deployment`, `v1.Service`, `v1.ConfigMap`, etc.). ## Supported Resources | Resource | K8s API Object | Status | |----------|---------------|--------| | **K8sNamespace** | `v1.Namespace` | ✅ | | **K8sConfigMap** | `v1.ConfigMap` | ✅ | | **K8sSecret** | `v1.Secret` | ✅ | | **K8sPersistentVolumeClaim** | `v1.PersistentVolumeClaim` | ✅ | | **K8sDeployment** | `apps/v1.Deployment` | ✅ | | **K8sStatefulSet** | `apps/v1.StatefulSet` | ✅ | | **K8sService** | `v1.Service` | ✅ | | **K8sIngress** | `networking/v1.Ingress` | ✅ | | **K8sManifest** | Raw manifest (inline/file/dir) | ✅ | | **K8sHelmRelease** | Helm chart deployment | ✅ | | **K8sCluster** | Cloud K8s cluster (EKS/GKE/DOKS) | ✅ | ## Cluster Resource The `KubernetesCluster` resource provisions a managed Kubernetes cluster on a cloud provider: | Cloud | Service | |-------|---------| | AWS | EKS | | GCP | GKE | | DigitalOcean | DOKS | | Hetzner | ❌ (UnsupportedFeatureError) | Once a cluster exists, all K8s resource types reference it: ```typescript const cluster = new KubernetesCluster({ id: 'cluster-prod', name: 'prod-cluster', version: '1.28', vpc: myVpc, }); const namespace = new K8sNamespace({ id: 'ns-app', name: 'app', cluster: cluster, }); const deployment = new K8sDeployment({ id: 'deploy-api', name: 'api-server', cluster: cluster, namespace: namespace, replicas: 3, containers: [{ image: 'nginx:1.25', ports: [{ containerPort: 80 }], }], }); ``` ## Dependency Graph K8s resources participate in the Kyku dependency graph: ``` K8sNamespace ──┐ ├── K8sConfigMap ──┐ ├── K8sSecret ─────┤ ├── K8sPersistentVolumeClaim ──┤ │ ├── K8sDeployment ──┐ │ ├── K8sStatefulSet ─┤ │ ├── K8sService ──┐ │ ├── K8sIngress │ └── K8sManifest / K8sHelmRelease (independently) ``` Use `kyku graph` to verify the dependency chain before applying. ## Common Properties All K8s resources share a common base: | Property | Type | Required | Description | |----------|------|----------|-------------| | `cluster` | `KubernetesCluster \| string` | ✅ | Target cluster | | `namespace` | `K8sNamespace \| string` | ❌ | Namespace (defaults to `default`) | | `dependsOn` | `string[]` | ❌ | Explicit dependency ordering | ## Raw Manifest Resource `K8sManifest` supports arbitrary Kubernetes manifests: ```typescript // Inline YAML new K8sManifest({ id: 'custom-crd', name: 'custom-crd', cluster: cluster, manifest: { apiVersion: 'example.com/v1', kind: 'CustomResource', ... } }); // File path new K8sManifest({ id: 'app-manifest', name: 'app-manifest', cluster: cluster, manifestPath: './k8s/app.yaml', }); ``` ## Helm Releases `K8sHelmRelease` deploys Helm charts: ```typescript new K8sHelmRelease({ id: 'helm-nginx', name: 'nginx-ingress', cluster: cluster, chart: 'ingress-nginx/ingress-nginx', version: '4.8.0', values: { controller: { replicas: 2 } }, }); ``` ## Gotchas ### Custom Config Every typed K8s resource (Namespace, ConfigMap, Secret, PersistentVolumeClaim, Deployment, StatefulSet, Service, Ingress) accepts a `customConfig` object — see [Custom Config](/concepts/custom-config/). It deep-merges into the same target the pre-existing raw `spec` field on Deployment/StatefulSet/Ingress already used, applied after `spec` so `customConfig` wins on overlap. Because every apply is a server-side-apply PATCH (always safe to re-run), changing `customConfig` plans an in-place update rather than a destructive replace. `K8sManifest` and `K8sHelmRelease` reject `customConfig` — the manifest/chart input you give them already is the full escape hatch. ### Cluster Must Exist First K8s resources depend on a `KubernetesCluster`. The cluster must be provisioned before any K8s resources can be applied. Kyku's dependency graph enforces this ordering automatically. ### Raw Manifest Updates `K8sManifest` applies manifests via `kubectl apply` semantics — it does not deep-merge. Large changes to raw manifests may require manual intervention. ### Helm Release State Helm releases are tracked in Kyku state. Manual `helm upgrade` outside Kyku will cause drift. --- # AutoScalingGroup An `AutoScalingGroup` manages a scalable fleet of virtual machines that automatically adjusts the instance count based on demand. On AWS this is an Auto Scaling Group, on GCP a Managed Instance Group. Hetzner and DigitalOcean do not have native auto-scaling group equivalents. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | yes | Unique resource name | | `id` | `string` | no | Explicit ID (auto-generated UUID if omitted) | | `provider` | `string` | no | Provider label for multi-provider configs | | `tags` | `Record` | no | Arbitrary key-value metadata | | `minSize` | `number` | yes | Minimum number of instances | | `maxSize` | `number` | yes | Maximum number of instances | | `desiredSize` | `number` | no | Desired instance count (defaults to `minSize`) | | `instanceType` | `InstanceType` | yes | Abstract size or provider-specific map | | `image` | `ImageType` | yes | Abstract image or provider-specific map | | `network` | `Vpc \| string` | yes | Vpc for instances | | `securityGroups` | `(SecurityGroup \| string)[]` | no | Security groups for instances | | `sshPublicKey` | `string` | no | Public key for SSH access | | `userData` | `string` | no | Cloud-init / startup script | | `distributeAcrossAzs` | `boolean` | no | Spread instances across AZs | | `healthCheckType` | `string` | no | Health check type (`'EC2'` or `'ELB'` on AWS) | | `healthCheckGracePeriod` | `number` | no | Grace period in seconds before health checks start | ## Example ```typescript import { Vpc, AutoScalingGroup } from '@kykucloud/types' const myVpc = new Vpc({ name: 'my-vpc', cidr: '10.0.0.0/16', region: 'eu-central' }) const asg = new AutoScalingGroup({ name: 'web-asg', minSize: 2, maxSize: 10, desiredSize: 3, instanceType: 'medium', image: 'ubuntu-24.04', network: myVpc, healthCheckType: 'ELB', healthCheckGracePeriod: 60, }) ``` ## Provider Support | Provider | Supported | Backend | |----------|-----------|---------| | AWS | ✅ | Auto Scaling Group (with Launch Template) | | GCP | ✅ | Managed Instance Group | | Hetzner | ❌ | Not available | | DigitalOcean | ❌ | Not available | ## Notes - **AWS ASG** uses a Launch Template. Health checks can be EC2 status checks or ELB health checks. The ASG integrates with Load Balancer target groups for traffic distribution. - **GCP MIG** (Managed Instance Group) uses an Instance Template. Supports auto-healing, autoscaling, and rolling updates. Regional MIGs distribute across zones. - **Scaling policies** (CPU-based, scheduled, or dynamic) are not yet abstracted in this resource type. Use provider-specific configuration for advanced autoscaling policies. --- # Bucket A `Bucket` represents an object storage container for files and data. On AWS this is S3, on GCP Cloud Storage, on Hetzner S3-compatible storage, and on DigitalOcean Spaces. All providers support standard object storage operations. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | yes | Unique resource name (globally unique for S3/Spaces) | | `id` | `string` | no | Explicit ID (auto-generated UUID if omitted) | | `provider` | `string` | no | Provider label for multi-provider configs | | `tags` | `Record` | no | Arbitrary key-value metadata | | `region` | `string` | no | Region for the bucket (defaults to provider region) | | `public` | `boolean` | no | Allow public access (default `false`) | | `versioning` | `boolean` | no | Enable object versioning (default `false`) | ## Example ```typescript import { Bucket } from '@kykucloud/types' const assets = new Bucket({ name: 'my-assets', region: 'eu-central', public: true, versioning: true, }) ``` ## Provider Support | Provider | Supported | Backend | |----------|-----------|---------| | AWS | ✅ | S3 | | GCP | ✅ | Cloud Storage | | Hetzner | ✅ | S3-compatible (via `@aws-sdk/client-s3` with endpoint override) | | DigitalOcean | ✅ | Spaces (via `@aws-sdk/client-s3` with endpoint override) | ## Notes - **Hetzner S3** uses `@aws-sdk/client-s3` with endpoint override and AWS SigV4 auth. Credentials via `HETZNER_S3_REGION`, `HETZNER_S3_ACCESS_KEY`, `HETZNER_S3_SECRET_KEY`. - **DigitalOcean Spaces** uses `@aws-sdk/client-s3` with Spaces endpoint override and AWS SigV4 auth. Credentials via `DO_SPACES_REGION`, `DO_SPACES_ACCESS_KEY`, `DO_SPACES_SECRET_KEY`. - **Bucket names** must be globally unique across all customers for AWS S3 and DO Spaces. GCP bucket names are also globally unique across all projects. - **Public access**: Setting `public: true` enables anonymous read access. AWS blocks public access by default — this setting overrides the account-level block. - **Versioning**: Enables object versioning for data protection. Cannot be disabled once enabled on some providers without recreating the bucket. --- # Cache A `Cache` represents a managed in-memory data store for caching, session storage, and real-time workloads. It supports Redis and Memcached engines. On AWS this is ElastiCache, on GCP Memorystore, and on DigitalOcean a Redis database (Memcached is not supported on DO). ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | yes | Unique resource name | | `id` | `string` | no | Explicit ID (auto-generated UUID if omitted) | | `provider` | `string` | no | Provider label for multi-provider configs | | `tags` | `Record` | no | Arbitrary key-value metadata | | `engine` | `'redis' \| 'memcached'` | yes | Cache engine | | `instanceType` | `string` | yes | Instance type (provider-specific, e.g. `cache.t3.micro`) | | `network` | `Vpc \| string` | no | Vpc for network placement | | `memorySize` | `number` | no | Memory size in GB (GCP Memorystore) | | `nodeCount` | `number` | no | Number of nodes (clustered Redis) | | `version` | `string` | no | Engine version (e.g. `'7.0'`) | | `authEnabled` | `boolean` | no | Enable authentication token | | `maintenanceWindow` | `string` | no | Preferred maintenance window (e.g. `'sun:03:00-sun:04:00'`) | | `snapshotRetention` | `number` | no | Snapshot retention in days (Redis) | ## Example ```typescript import { Vpc, Cache } from '@kykucloud/types' const myVpc = new Vpc({ name: 'my-vpc', cidr: '10.0.0.0/16', region: 'eu-central' }) const redis = new Cache({ name: 'session-cache', engine: 'redis', instanceType: 'cache.t3.micro', network: myVpc, version: '7.0', nodeCount: 2, authEnabled: true, snapshotRetention: 7, }) ``` ## Provider Support | Provider | Supported | Backend | |----------|-----------|---------| | AWS | ✅ | ElastiCache (Redis / Memcached) | | GCP | ✅ | Memorystore (Redis / Memcached) | | Hetzner | ❌ | Not available | | DigitalOcean | ✅ | Managed Redis (Memcached not available) | ## Notes - **AWS ElastiCache** instance types use `cache.*` prefix (e.g. `cache.t3.micro`, `cache.r6g.large`). Supports both Redis (with clustering) and Memcached. Multi-AZ with automatic failover for Redis. - **GCP Memorystore** uses instance types like `basic`, `standard`, `highmem` tiers. Tier maps to memory size (GB). For Redis, the `memorySize` configures the instance capacity. - **DigitalOcean** offers managed Redis only (no Memcached). Sizing via plan slugs. - **Network isolation**: On AWS and GCP, the cache can be placed in a VPC for private network access. DO Redis is accessible via public endpoint by default. --- # Cdn A `Cdn` fronts an origin (a `Bucket` object or a hostname) with a managed edge cache. On AWS this is CloudFront. On GCP it is Cloud CDN (backend bucket + URL map + HTTP proxy + forwarding rule). Hetzner and DigitalOcean have no CDN in this release — no shims. ACM certificate provisioning and WAF are out of scope. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | yes | Unique resource name | | `id` | `string` | no | Explicit ID (auto-generated UUID if omitted) | | `provider` | `string` | no | Provider label for multi-provider configs | | `tags` | `Record` | no | Arbitrary key-value metadata | | `origin` | `Bucket \| string` | yes | Origin bucket (object ref) or hostname | | `domain` | `string` | no | Optional custom alias (not provisioned as a cert) | ## Example ```typescript import { Bucket, Cdn } from '@kykucloud/types' const assets = new Bucket({ name: 'app-assets', region: 'us-east' }) const cdn = new Cdn({ name: 'app-cdn', origin: assets, }) ``` ## Provider Support | Provider | Supported | Backend | |----------|-----------|---------| | AWS | ✅ | CloudFront | | GCP | ✅ | Cloud CDN (backend bucket stack) | | Hetzner | ❌ | Unsupported | | DigitalOcean | ❌ | Unsupported | ## Notes - **Object refs**: pass a `Bucket` instance so the graph creates the bucket first. - **No ACM**: `domain` is an alias only; certificates are not requested. - **No WAF**: a separate WAF type is not added here. If WAF later grows past one resource type, stop and revisit. --- # Certificate A `Certificate` represents an SSL/TLS certificate for a domain, used by `LoadBalancer` listeners for HTTPS/TLS termination. On AWS this is an ACM certificate, on GCP a Certificate Manager resource. Hetzner does not have a standalone certificate resource — certificates are managed on the Load Balancer directly. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | yes | Unique resource name | | `id` | `string` | no | Explicit ID (auto-generated UUID if omitted) | | `provider` | `string` | no | Provider label for multi-provider configs | | `tags` | `Record` | no | Arbitrary key-value metadata | | `domain` | `string` | yes | Domain name for the certificate | ## Example ```typescript import { Certificate, LoadBalancer, Vpc } from '@kykucloud/types' const cert = new Certificate({ name: 'example-cert', domain: 'example.com', }) const myVpc = new Vpc({ name: 'my-vpc', cidr: '10.0.0.0/16', region: 'eu-central' }) const lb = new LoadBalancer({ name: 'web-lb', lbType: 'application', vpc: myVpc, listeners: [{ port: 443, protocol: 'https', certificate: cert.name, // or the certificate ARN targets: [], }], }) ``` ## Provider Support | Provider | Supported | Backend | |----------|-----------|---------| | AWS | ✅ | ACM (Certificate Manager) | | GCP | ✅ | Certificate Manager | | Hetzner | ❌ | Handled on the LB resource directly | | DigitalOcean | ❌ | Handled on the LB resource directly | ## Notes - **AWS ACM** certificates are regional resources. Must be in the same region as the load balancer. DNS validation is the recommended validation method. Certificates are free with ACM. - **GCP Certificate Manager** supports both Google-managed and self-managed certificates. For HTTPS LBs, use a `urlMap` with the certificate attached to the `targetHttpsProxy`. - **Hetzner & DigitalOcean**: SSL certificates are configured directly on the Load Balancer resource using provider-specific fields, not as standalone resources. --- # ContainerRegistry A `ContainerRegistry` is a managed image repository. On AWS this is ECR, on GCP Artifact Registry, and on DigitalOcean a container registry. Hetzner has no equivalent. This type does not push or pull images and does not manage lifecycle policies. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | yes | Unique resource name | | `id` | `string` | no | Explicit ID (auto-generated UUID if omitted) | | `provider` | `string` | no | Provider label for multi-provider configs | | `tags` | `Record` | no | Arbitrary key-value metadata | | `region` | `string` | no | Abstract region | | `public` | `boolean` | no | Whether the registry is public (default `false`) | ## Example ```typescript import { ContainerRegistry } from '@kykucloud/types' const images = new ContainerRegistry({ name: 'app-images', region: 'us-east', }) ``` ## Provider Support | Provider | Supported | Backend | |----------|-----------|---------| | AWS | ✅ | ECR (`CreateRepository`) | | GCP | ✅ | Artifact Registry (Docker) | | Hetzner | ❌ | Unsupported | | DigitalOcean | ✅ | Container Registry (one per account) | ## Notes - **Outputs**: `url` is the push/pull endpoint. - **DigitalOcean** allows a single registry per account. Create is idempotent via `GET /v2/registry`. - **No image CLI**: `kyku` does not push or pull images. - **No lifecycle policies**: expiry and tag rules are out of scope. --- # CustomResource A `CustomResource` allows you to define infrastructure resources that are not natively supported by Kyku. It runs a user-provided handler function during apply and destroy phases, with access to provider clients and secret management. This is the escape hatch for extending Kyku to any cloud API. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | yes | Unique resource name | | `id` | `string` | no | Explicit ID (auto-generated UUID if omitted) | | `provider` | `string` | no | Provider label for multi-provider configs | | `tags` | `Record` | no | Arbitrary key-value metadata | | `handlerId` | `string` | yes | Registered handler identifier | | `properties` | `Record` | yes | Arbitrary properties passed to the handler | ## Handler Interface ```typescript interface CustomResourceHandler { apply: (context: CustomResourceContext) => Promise destroy?: (context: CustomResourceContext) => Promise } interface CustomResourceContext { readonly id: string readonly name: string readonly properties: Record readonly previousProperties?: Record readonly previousState?: Record readonly previousProviderId?: string getClient: (name: string) => ProviderClient setSecret: (value: T) => T & { __secret: true } getOutput: (resourceId: string, key: string) => Promise readonly log: (message: string) => void } interface CustomResourceResult { providerId: string outputs?: Record status?: string } interface ProviderClient { readonly client: unknown readonly region: string } ``` ## Example ```typescript import { CustomResource, CustomResourceRegistry } from '@kykucloud/types' // Register a handler CustomResourceRegistry.register('my-custom-handler', { async apply(context) { const aws = context.getClient('aws') // AWS SDK client context.log('Creating custom resource…') return { providerId: 'custom-123', outputs: { endpoint: '…' } } }, async destroy(context) { context.log('Cleaning up…') }, }) // Use it in config const myCustom = new CustomResource({ name: 'my-custom', handlerId: 'my-custom-handler', properties: { configA: 'value1', configB: 42, }, }) ``` ## Provider Support | Provider | Supported | Backend | |----------|-----------|---------| | AWS | ✅ | Wired — can access any AWS SDK v3 service | | GCP | ✅ | Wired — can access any GCP API client | | Hetzner | ✅ | Wired — can access Hetzner API client | | DigitalOcean | ✅ | Wired — can access DO API client | ## Notes - **Handler registration**: Handlers must be registered with `CustomResourceRegistry.register(handlerId, handler)` before the plan/apply phase. - **Provider clients**: `context.getClient('aws')` returns the AWS SDK v3 client, `context.getClient('gcp')` returns the GCP auth client. The available clients depend on the provider used. - **Secrets**: Use `context.setSecret(value)` (or the exported `setSecret()`) to wrap a value. The crypto walk encrypts the inner string and unwraps the marker so state stores `ENC:` ciphertext. - **Outputs**: Return `outputs` in the result to expose values that can be referenced by other resources. - **Destroy**: If `destroy` is not provided, the resource will be removed from state without any cleanup. - **State tracking**: The result's `providerId` is used for tracking and drift detection on subsequent applies. --- # Database A `Database` represents a managed relational database instance. It supports PostgreSQL, MySQL, and MariaDB engines. On AWS this is RDS, on GCP Cloud SQL, and on DigitalOcean a Managed Database. Hetzner does not offer managed databases. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | yes | Unique resource name | | `id` | `string` | no | Explicit ID (auto-generated UUID if omitted) | | `provider` | `string` | no | Provider label for multi-provider configs | | `tags` | `Record` | no | Arbitrary key-value metadata | | `engine` | `'postgresql' \| 'mysql' \| 'mariadb'` | yes | Database engine | | `version` | `string` | yes | Engine version (e.g. `'16'`, `'8.0'`) | | `instanceType` | `InstanceType` | yes | Abstract size or provider-specific map | | `storage` | `number` | yes | Storage size in GB | | `username` | `string` | yes | Admin username | | `password` | `Secret` | no | Admin password (auto-generated if omitted) | | `iamAuth` | `boolean` | no | Enable IAM-based authentication | | `vpc` | `Vpc \| string` | yes | Vpc to place the database in | | `securityGroups` | `(SecurityGroup \| string)[]` | no | Security groups for network access | | `backupRetention` | `number` | no | Backup retention period in days | ## Example ```typescript import { Vpc, Database } from '@kykucloud/types' const myVpc = new Vpc({ name: 'my-vpc', cidr: '10.0.0.0/16', region: 'eu-central' }) const db = new Database({ name: 'my-db', engine: 'postgresql', version: '16', instanceType: 'medium', storage: 100, username: 'admin', vpc: myVpc, backupRetention: 7, }) ``` ## Provider Support | Provider | Supported | Backend | |----------|-----------|---------| | AWS | ✅ | RDS | | GCP | ✅ | Cloud SQL | | Hetzner | ❌ | Not available | | DigitalOcean | ✅ | Managed Database | ## Notes - **AWS RDS** instance types use `db.*` prefix (e.g. `db.t3.medium`). Maintained via `RDS_INSTANCE_TYPE_MAP`. Engine name mapping: `postgresql` → `postgres`, `mariadb` → `mariadb`. RDS requires a DB subnet group for VPC placement. Polling creation can take 10+ minutes (120 attempts × 5s timeout). - **GCP Cloud SQL** uses a separate API base (`sqladmin.googleapis.com`). Instance types differ from Compute Engine (`db-f1-micro`, `db-g1-small`, `db-custom-*`). Creation takes 10–15 minutes. - **DigitalOcean Managed DB** engine names differ from Kyku (`postgresql` → `pg`). Private networking requires resolving the VPC UUID. - **Password** is encrypted in state (AES-256-GCM). If omitted, the provider generates one and stores it as an output. - **IAM auth** (`iamAuth`) enables AWS IAM database authentication for PostgreSQL and MySQL RDS instances. --- # DnsRecord A `DnsRecord` represents a single DNS resource record set within a `DnsZone`. It supports standard record types including A, AAAA, CNAME, MX, TXT, NS, and SRV. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | yes | Record name (relative to zone, e.g. `www` or `@` for apex) | | `id` | `string` | no | Explicit ID (auto-generated UUID if omitted) | | `provider` | `string` | no | Provider label for multi-provider configs | | `tags` | `Record` | no | Arbitrary key-value metadata | | `zone` | `DnsZone \| string` | yes | Parent DNS zone reference | | `recordType` | `'A' \| 'AAAA' \| 'CNAME' \| 'MX' \| 'TXT' \| 'NS' \| 'SRV'` | yes | DNS record type | | `ttl` | `number` | no | Time-to-live in seconds (default `300`) | | `values` | `string[]` | yes | Record values (e.g. IP addresses for A records) | ## Example ```typescript import { DnsZone, DnsRecord } from '@kykucloud/types' const zone = new DnsZone({ name: 'example.com' }) const aRecord = new DnsRecord({ name: 'www', zone: zone, recordType: 'A', ttl: 300, values: ['10.0.0.1', '10.0.0.2'], }) const cnameRecord = new DnsRecord({ name: 'api', zone: zone, recordType: 'CNAME', values: ['lb.example.com'], }) ``` ## Provider Support | Provider | Supported | Backend | |----------|-----------|---------| | AWS | ✅ | Route53 Record Set | | GCP | ✅ | Cloud DNS Record Set | | Hetzner | ✅ | DNS Record (via `dns.hetzner.com/api/v1`) | | DigitalOcean | ✅ | Domain Record | ## Notes - **Route53 alias records**: For AWS, use provider-specific record values to create alias records (e.g. for ALB or CloudFront). Not expressed through the abstract `recordType`. - **TXT records**: Values may need quoting depending on the provider. Kyku handles quoting automatically. - **CNAME restrictions**: Cannot coexist with other record types at the same name (DNS protocol constraint). Use `@` for zone apex with A/AAAA when CNAME is not allowed. - **Hetzner DNS** uses a separate API endpoint and token from the cloud API. Requires `HETZNER_DNS_TOKEN`. --- # DnsZone A `DnsZone` represents a managed DNS zone for a domain name. It contains `DnsRecord` entries for individual DNS records. On AWS this is a Route53 hosted zone, on GCP Cloud DNS, on Hetzner DNS, and on DigitalOcean a Domain. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | yes | Domain name (e.g. `example.com`) | | `id` | `string` | no | Explicit ID (auto-generated UUID if omitted) | | `provider` | `string` | no | Provider label for multi-provider configs | | `tags` | `Record` | no | Arbitrary key-value metadata | | `region` | `string` | no | Region for the zone (provider-specific) | ## Example ```typescript import { DnsZone, DnsRecord } from '@kykucloud/types' const zone = new DnsZone({ name: 'example.com', }) const record = new DnsRecord({ name: 'www', zone: zone, recordType: 'A', ttl: 300, values: ['10.0.0.1'], }) ``` ## Provider Support | Provider | Supported | Backend | |----------|-----------|---------| | AWS | ✅ | Route53 Hosted Zone | | GCP | ✅ | Cloud DNS Managed Zone | | Hetzner | ✅ | DNS (separate REST API at `dns.hetzner.com/api/v1`) | | DigitalOcean | ✅ | Domain | ## Notes - **Hetzner DNS** uses a separate REST API (`dns.hetzner.com/api/v1`) authenticated via `Auth-API-Token` header. Requires `HETZNER_DNS_TOKEN` env var — distinct from the Cloud API token. - **AWS Route53** zones are globally available (not region-scoped). The zone's name servers are provided as outputs for delegation. - **Name format**: The zone `name` must be a valid domain (e.g. `example.com.` with trailing dot for some APIs). GCP requires lowercase. --- # Identity An `Identity` represents a human or service identity with assigned roles. On AWS this is an IAM User, on GCP a Service Account. Hetzner and DigitalOcean do not have equivalent identity resources. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | yes | Unique resource name | | `id` | `string` | no | Explicit ID (auto-generated UUID if omitted) | | `provider` | `string` | no | Provider label for multi-provider configs | | `tags` | `Record` | no | Arbitrary key-value metadata | | `roles` | `(Role \| string)[]` | yes | Roles attached to this identity | ## Example ```typescript import { Role, Identity } from '@kykucloud/types' const adminRole = new Role({ name: 'admin', permissions: ['compute:admin', 'storage:admin'] }) const deployer = new Identity({ name: 'deployer', roles: [adminRole], }) ``` ## Provider Support | Provider | Supported | Backend | |----------|-----------|---------| | AWS | ✅ | IAM User | | GCP | ✅ | Service Account | | Hetzner | ❌ | Not available | | DigitalOcean | ❌ | Not available | ## Notes - **AWS IAM** users require policy cleanup before deletion — `destroy()` must detach managed policies and delete inline policies before `DeleteUserCommand`. Catch `NoSuchEntity` on each cleanup step. - **GCP Service Accounts** use the IAM API base (`iam.googleapis.com`). Service accounts are created via `POST /v1/projects/{project}/serviceAccounts`. - **Roles** can be assigned by name or by reference. You can use built-in (AWS-managed) roles by passing role names as strings. - **AWS** identity names are immutable (rename is a separate operation). --- # Resources Kyku provides a comprehensive set of resource types covering compute, networking, storage, security, and application infrastructure. All resource classes extend `BaseResource` and are imported from `@kykucloud/types`. ## Provider Support Matrix | Resource | AWS | GCP | Hetzner | DigitalOcean | |----------|-----|-----|---------|-------------| | [Vpc](/resources/vpc/) | ✅ | ✅ | ✅ | ✅ | | [Subnet](/resources/subnet/) | ✅ | ✅ | ✅ | ✅ | | [Vm](/resources/vm/) | ✅ | ✅ | ✅ | ✅ | | [SecurityGroup](/resources/security-group/) | ✅ | ✅ | ✅ | ✅ | | [LoadBalancer](/resources/load-balancer/) | ✅ | ✅ | ✅ | ✅ | | [TargetGroup](/resources/target-group/) | ✅ | ❌ | ✅ | ✅ | | [Database](/resources/database/) | ✅ (RDS) | ✅ (Cloud SQL) | ❌ | ✅ (Managed DB) | | [Identity](/resources/identity/) | ✅ (IAM User) | ✅ (Service Account) | ❌ | ❌ | | [Role](/resources/role/) | ✅ (IAM Role) | ✅ (Custom Role) | ❌ | ❌ | | [SshKey](/resources/ssh-key/) | ✅ (Key Pair) | ❌ | ✅ | ✅ | | [Bucket](/resources/bucket/) | ✅ (S3) | ✅ (Cloud Storage) | ✅ (S3-compat) | ✅ (Spaces) | | [DnsZone](/resources/dns-zone/) | ✅ (Route53) | ✅ (Cloud DNS) | ✅ (DNS API) | ✅ (Domain) | | [DnsRecord](/resources/dns-record/) | ✅ (Route53) | ✅ (Cloud DNS) | ✅ (DNS API) | ✅ (Record) | | [Secret](/resources/secret/) | ✅ (Secrets Manager) | ✅ (Secret Manager) | ❌ | ❌ | | [Certificate](/resources/certificate/) | ✅ (ACM) | ✅ (Certificate Manager) | ❌ | ❌ | | [KmsKey](/resources/kms-key/) | ✅ (KMS) | ✅ (Cloud KMS) | ❌ | ❌ | | [Volume](/resources/volume/) | ✅ (EBS) | ✅ (Persistent Disk) | ✅ (Volume) | ❌ | | [StaticIp](/resources/static-ip/) | ✅ (Elastic IP) | ✅ (Address) | ✅ (Floating IP) | ✅ (Floating IP) | | [ContainerRegistry](/resources/container-registry/) | ✅ (ECR) | ✅ (Artifact Registry) | ❌ | ✅ (Registry) | | [Cdn](/resources/cdn/) | ✅ (CloudFront) | ✅ (Cloud CDN) | ❌ | ❌ | | [AutoScalingGroup](/resources/auto-scaling-group/) | ✅ (ASG) | ✅ (MIG) | ❌ | ❌ | | [Queue](/resources/queue/) | ✅ (SQS) | ✅ (Pub/Sub) | ❌ | ❌ | | [Cache](/resources/cache/) | ✅ (ElastiCache) | ✅ (Memorystore) | ❌ | ✅ (Redis) | | [KubernetesCluster](/resources/kubernetes-cluster/) | ✅ (EKS) | ✅ (GKE) | ❌ | ✅ (DOKS) | | [Custom](/resources/custom/) | ✅ | ✅ | ✅ | ✅ | ## Resource Overview ### Networking - **[Vpc](/resources/vpc/)** — Virtual private cloud with CIDR, region, and availability zone distribution - **[Subnet](/resources/subnet/)** — Synthetic subnet resource auto-injected by the planner - **[SecurityGroup](/resources/security-group/)** — Ingress and egress firewall rules - **[StaticIp](/resources/static-ip/)** — Reserved public IP (Elastic IP / Address / Floating IP) ### Compute - **[Vm](/resources/vm/)** — Virtual machine instances with SSH, security groups, and user data - **[AutoScalingGroup](/resources/auto-scaling-group/)** — Auto-scaling instance groups - **[KubernetesCluster](/resources/kubernetes-cluster/)** — Managed Kubernetes clusters ### Load Balancing - **[LoadBalancer](/resources/load-balancer/)** — Application and network load balancers - **[TargetGroup](/resources/target-group/)** — Logical target binding using label selectors ### Storage - **[Bucket](/resources/bucket/)** — Object storage (S3-compatible) - **[Volume](/resources/volume/)** — Block storage volumes - **[Cache](/resources/cache/)** — In-memory caching (Redis / Memcached) - **[ContainerRegistry](/resources/container-registry/)** — Managed container image registry - **[Cdn](/resources/cdn/)** — Content delivery network (CloudFront / Cloud CDN) ### Databases & Queues - **[Database](/resources/database/)** — Managed relational databases - **[Queue](/resources/queue/)** — Message queues ### Security & Identity - **[Identity](/resources/identity/)** — IAM Users and Service Accounts - **[Role](/resources/role/)** — IAM Roles and permission definitions - **[SshKey](/resources/ssh-key/)** — SSH public key management - **[Secret](/resources/secret/)** — Encrypted secrets management - **[Certificate](/resources/certificate/)** — SSL/TLS certificates - **[KmsKey](/resources/kms-key/)** — Encryption key management ### DNS - **[DnsZone](/resources/dns-zone/)** — DNS zones (Route53, Cloud DNS, etc.) - **[DnsRecord](/resources/dns-record/)** — Individual DNS records within a zone ### Custom - **[Custom](/resources/custom/)** — User-defined resources with custom lifecycle handlers ## Common Config Properties Every resource type inherits these properties from `ResourceConfig`: | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | yes | Unique resource name | | `id` | `string` | no | Explicit ID (auto-generated UUID if omitted) | | `provider` | `string` | no | Provider label for multi-provider configs | | `tags` | `Record` | no | Arbitrary key-value metadata | ## Deployment Prefix Every deployment receives a unique 8-character hex prefix (e.g. `a3f27b1d`) applied to all new resource names. This avoids naming collisions between different deployments in the same cloud account. Resources already tracked in state retain their original names. The prefix adds 9 characters (8 hex + dash) to resource names — plan for name length limits accordingly. --- # K8sConfigMap `K8sConfigMap` creates a Kubernetes ConfigMap in the target cluster. ConfigMaps store non-sensitive configuration data that can be consumed by pods as environment variables, command-line arguments, or volume mounts. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `cluster` | `KubernetesCluster \| string` | ✅ | Target cluster | | `namespace` | `K8sNamespace \| string` | ❌ | Namespace to create the ConfigMap in | | `dependsOn` | `string[]` | ❌ | Explicit dependency ordering | | `data` | `Record` | ❌ | Key-value pairs of configuration data | | `binaryData` | `Record` | ❌ | Binary data as base64-encoded strings | ## Example ```typescript const appConfig = new K8sConfigMap({ name: 'app-config', cluster: myCluster, namespace: myNamespace, data: { 'APP_ENV': 'production', 'LOG_LEVEL': 'info', 'API_URL': 'https://api.example.com', }, }) ``` ## Provider support | Provider | Supported | |----------|:---------:| | Kubernetes | ✅ | | Hetzner | ❌ | | DigitalOcean | ❌ | | AWS | ❌ | | GCP | ❌ | --- # K8sDeployment `K8sDeployment` creates a Kubernetes Deployment in the target cluster. Deployments manage stateless pods with desired replica counts, rolling updates, and self-healing. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `cluster` | `KubernetesCluster \| string` | ✅ | Target cluster | | `namespace` | `K8sNamespace \| string` | ❌ | Namespace to deploy into | | `dependsOn` | `string[]` | ❌ | Explicit dependency ordering | | `image` | `string` | ✅ | Container image (e.g., `'nginx:1.25'`) | | `replicas` | `number` | ❌ | Desired pod count (defaults to 1) | | `ports` | `{ containerPort: number; name?: string; protocol?: string }[]` | ❌ | Container ports to expose | | `env` | `{ name: string; value?: string; valueFrom?: Record }[]` | ❌ | Environment variables | | `envFrom` | `{ configMapRef?: K8sConfigMap \| string; secretRef?: K8sSecret \| string; prefix?: string }[]` | ❌ | Populate env from ConfigMaps or Secrets | | `volumeMounts` | `{ name: string; mountPath: string; readOnly?: boolean; subPath?: string }[]` | ❌ | Volume mount definitions | | `volumes` | `{ name: string; configMap?: K8sConfigMap; secret?: K8sSecret; persistentVolumeClaim?: K8sPersistentVolumeClaim }[]` | ❌ | Volume sources (references to other K8s resources) | | `rollingUpdate` | `{ maxSurge?: number; maxUnavailable?: number }` | ❌ | Rolling update strategy parameters | | `spec` | `Record` | ❌ | Raw pod template spec overrides (merged into the generated spec) | ## Example ```typescript const appConfig = new K8sConfigMap({ ... }) const api = new K8sDeployment({ name: 'api-server', cluster: myCluster, namespace: myNamespace, image: 'myapp/api:1.0.0', replicas: 3, ports: [{ containerPort: 8080, name: 'http' }], env: [ { name: 'NODE_ENV', value: 'production' }, ], envFrom: [ { configMapRef: appConfig }, ], volumes: [ { name: 'data', persistentVolumeClaim: myPVC }, ], volumeMounts: [ { name: 'data', mountPath: '/var/data' }, ], rollingUpdate: { maxSurge: 1, maxUnavailable: 0, }, }) ``` ## Provider support | Provider | Supported | |----------|:---------:| | Kubernetes | ✅ | | Hetzner | ❌ | | DigitalOcean | ❌ | | AWS | ❌ | | GCP | ❌ | --- # K8sHelmRelease `K8sHelmRelease` installs or upgrades a Helm chart on the target cluster. Supports custom values, version pinning, chart repositories, and server-side apply with pruning. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `cluster` | `KubernetesCluster \| string` | ✅ | Target cluster | | `namespace` | `K8sNamespace \| string` | ❌ | Namespace to install into | | `dependsOn` | `string[]` | ❌ | Explicit dependency ordering | | `chart` | `string` | ✅ | Chart name (e.g., `nginx-ingress`, `./local-chart`) | | `repo` | `string` | ❌ | Helm repository URL (e.g., `https://charts.bitnami.com/bitnami`) | | `version` | `string` | ❌ | Chart version constraint (e.g., `^4.0.0`) | | `values` | `Record` | ❌ | Inline values (merged with any values file) | | `valuesFile` | `string` | ❌ | Path to a values YAML file | | `prune` | `boolean` | ❌ | Remove resources not in the chart (defaults to `true`) | | `forceConflicts` | `boolean` | ❌ | Force apply on resource conflicts (defaults to `true`) | ## Example ```typescript const nginxIngress = new K8sHelmRelease({ name: 'nginx-ingress', cluster: myCluster, namespace: 'ingress-nginx', chart: 'ingress-nginx', repo: 'https://kubernetes.github.io/ingress-nginx', version: '4.10.0', values: { controller: { replicaCount: 2, service: { type: 'LoadBalancer' }, }, }, }) // Local chart with values file const app = new K8sHelmRelease({ name: 'my-app', cluster: myCluster, chart: './charts/my-app', valuesFile: './values/production.yaml', values: { image: { tag: '1.2.3' }, }, }) ``` ## Provider support | Provider | Supported | |----------|:---------:| | Kubernetes | ✅ | | Hetzner | ❌ | | DigitalOcean | ❌ | | AWS | ❌ | | GCP | ❌ | --- # Kubernetes Resources Kyku supports managing Kubernetes-native resources alongside your cloud infrastructure. After provisioning a [`KubernetesCluster`](./kubernetes-cluster) resource, you can define workloads, configuration, networking, and storage directly in your Kyku config. All K8s resources share a common base that links them to a cluster: | Property | Type | Required | Description | |----------|------|----------|-------------| | `cluster` | `KubernetesCluster \| string` | ✅ | The cluster to deploy into | | `namespace` | `K8sNamespace \| string` | ❌ | Target namespace (defaults to `default`) | | `dependsOn` | `string[]` | ❌ | Explicit dependency ordering | ## Resource types | Resource | Purpose | |----------|---------| | [`K8sNamespace`](./k8s-namespace) | Isolated environment within the cluster | | [`K8sConfigMap`](./k8s-config-map) | Non-confidential key-value configuration data | | [`K8sSecret`](./k8s-secret) | Sensitive data (credentials, tokens, keys) | | [`K8sPersistentVolumeClaim`](./k8s-persistent-volume-claim) | Persistent storage request | | [`K8sDeployment`](./k8s-deployment) | Stateless workload with replicas and rolling updates | | [`K8sStatefulSet`](./k8s-stateful-set) | Stateful workload with stable network identity and storage | | [`K8sService`](./k8s-service) | Network endpoint exposing pods | | [`K8sIngress`](./k8s-ingress) | HTTP/S routing to services | | [`K8sManifest`](./k8s-manifest) | Raw Kubernetes manifest (inline, file, or directory) | | [`K8sHelmRelease`](./k8s-helm-release) | Helm chart deployment | ## Provider support | Resource | Kubernetes | |----------|:----------:| | All K8s resources | ✅ | ## Dependency graph K8s resources participate in the Kyku dependency graph. The planner automatically resolves the creation order: ``` K8sNamespace ──┐ ├── K8sConfigMap ──┐ ├── K8sSecret ─────┤ ├── K8sPersistentVolumeClaim ──┤ │ ├── K8sDeployment ──┐ │ ├── K8sStatefulSet ─┤ │ ├── K8sService ──┐ │ ├── K8sIngress │ └── K8sManifest / K8sHelmRelease (independently) ``` Use `kyku graph` to verify the dependency chain before applying. --- # K8sIngress `K8sIngress` creates a Kubernetes Ingress in the target cluster. Ingress resources define HTTP/S routing rules from external clients to internal services, with optional TLS termination. ## IngressRule | Property | Type | Required | Description | |----------|------|----------|-------------| | `host` | `string` | ❌ | Virtual hostname (e.g., `api.example.com`) | | `paths` | `{ path: string; pathType?: string; service: K8sService \| string; port?: number }[]` | ✅ | Path routing rules | ## IngressTLS | Property | Type | Required | Description | |----------|------|----------|-------------| | `hosts` | `string[]` | ✅ | Hostnames covered by the certificate | | `secretName` | `string` | ❌ | Kubernetes Secret name holding the TLS certificate | ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `cluster` | `KubernetesCluster \| string` | ✅ | Target cluster | | `namespace` | `K8sNamespace \| string` | ❌ | Namespace to create the Ingress in | | `dependsOn` | `string[]` | ❌ | Explicit dependency ordering | | `rules` | `IngressRule[]` | ✅ | HTTP/S routing rules | | `tls` | `IngressTLS[]` | ❌ | TLS configuration | | `ingressClassName` | `string` | ❌ | IngressClass name (e.g., `nginx`, `traefik`) | | `annotations` | `Record` | ❌ | Ingress annotations (controller-specific config) | | `spec` | `Record` | ❌ | Raw spec overrides | ## Example ```typescript const appIngress = new K8sIngress({ name: 'app-ingress', cluster: myCluster, namespace: myNamespace, ingressClassName: 'nginx', rules: [ { host: 'app.example.com', paths: [ { path: '/', pathType: 'Prefix', service: apiService, port: 80 }, { path: '/static', pathType: 'Prefix', service: staticService, port: 8080 }, ], }, ], tls: [ { hosts: ['app.example.com'], secretName: 'app-tls' }, ], annotations: { 'nginx.ingress.kubernetes.io/ssl-redirect': 'true', }, }) ``` ## Provider support | Provider | Supported | |----------|:---------:| | Kubernetes | ✅ | | Hetzner | ❌ | | DigitalOcean | ❌ | | AWS | ❌ | | GCP | ❌ | --- # K8sManifest `K8sManifest` deploys arbitrary Kubernetes manifests to the target cluster. Use it for resources that don't have a dedicated Kyku resource type, or for applying pre-existing YAML files. Supports inline manifest strings, file paths, entire directories, or raw JavaScript objects. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `cluster` | `KubernetesCluster \| string` | ✅ | Target cluster | | `namespace` | `K8sNamespace \| string` | ❌ | Default namespace for resources without one | | `dependsOn` | `string[]` | ❌ | Explicit dependency ordering | | `manifest` | `string` | ❌ | Inline YAML manifest string | | `manifestFile` | `string` | ❌ | Path to a YAML file | | `manifestDir` | `string` | ❌ | Path to a directory of YAML files (applied recursively) | | `manifestObject` | `Record` | ❌ | Raw JavaScript object (converted to YAML) | | `prune` | `boolean` | ❌ | Remove resources not in the manifest (defaults to `true`) | | `forceConflicts` | `boolean` | ❌ | Force apply on resource conflicts (defaults to `true`) | At least one of `manifest`, `manifestFile`, `manifestDir`, or `manifestObject` must be provided. ## Example ```typescript // Inline manifest const dashboard = new K8sManifest({ name: 'kubernetes-dashboard', cluster: myCluster, manifest: ` apiVersion: v1 kind: ServiceAccount metadata: name: dashboard-admin --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: dashboard-admin roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin subjects: - kind: ServiceAccount name: dashboard-admin namespace: default `, }) // From file const monitoring = new K8sManifest({ name: 'prometheus-stack', cluster: myCluster, manifestFile: './manifests/prometheus.yaml', prune: false, }) // From object const crd = new K8sManifest({ name: 'custom-resource', cluster: myCluster, manifestObject: { apiVersion: 'example.com/v1', kind: 'MyResource', metadata: { name: 'my-instance' }, spec: { replicas: 3 }, }, }) ``` ## Provider support | Provider | Supported | |----------|:---------:| | Kubernetes | ✅ | | Hetzner | ❌ | | DigitalOcean | ❌ | | AWS | ❌ | | GCP | ❌ | --- # K8sNamespace `K8sNamespace` creates a Kubernetes namespace inside the target cluster. Namespaces provide scope for other K8s resources like deployments, services, and config maps. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `cluster` | `KubernetesCluster \| string` | ✅ | Target cluster | | `namespace` | `K8sNamespace \| string` | ❌ | Parent namespace (for nested scoping) | | `dependsOn` | `string[]` | ❌ | Explicit dependency ordering | | `labels` | `Record` | ❌ | Kubernetes labels applied to the namespace | | `annotations` | `Record` | ❌ | Kubernetes annotations applied to the namespace | ## Example ```typescript const teamNamespace = new K8sNamespace({ name: 'team-a', cluster: myCluster, labels: { environment: 'staging', team: 'platform', }, annotations: { 'owner': 'platform-team', }, }) ``` ## Provider support | Provider | Supported | |----------|:---------:| | Kubernetes | ✅ | | Hetzner | ❌ | | DigitalOcean | ❌ | | AWS | ❌ | | GCP | ❌ | --- # K8sPersistentVolumeClaim `K8sPersistentVolumeClaim` creates a Kubernetes PersistentVolumeClaim (PVC) in the target cluster. PVCs request storage resources that are dynamically provisioned by the cluster's StorageClass. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `cluster` | `KubernetesCluster \| string` | ✅ | Target cluster | | `namespace` | `K8sNamespace \| string` | ❌ | Namespace to create the PVC in | | `dependsOn` | `string[]` | ❌ | Explicit dependency ordering | | `accessModes` | `string[]` | ✅ | Access modes (e.g., `['ReadWriteOnce']`, `['ReadWriteMany', 'ReadOnlyMany']`) | | `storageClassName` | `string` | ❌ | StorageClass to use (cluster default if omitted) | | `size` | `string` | ✅ | Storage size (e.g., `'10Gi'`, `'100Gi'`) | ## Example ```typescript const dataVolume = new K8sPersistentVolumeClaim({ name: 'data-store', cluster: myCluster, namespace: myNamespace, accessModes: ['ReadWriteOnce'], storageClassName: 'ssd-standard', size: '50Gi', }) ``` ## Provider support | Provider | Supported | |----------|:---------:| | Kubernetes | ✅ | | Hetzner | ❌ | | DigitalOcean | ❌ | | AWS | ❌ | | GCP | ❌ | --- # K8sSecret `K8sSecret` creates a Kubernetes Secret in the target cluster. Unlike the Kyku [`Secret`](./secret) resource (which manages cloud provider secrets), `K8sSecret` maps directly to a native `v1.Secret` inside the cluster. Secrets can be populated inline or sourced from an Kyku `Secret` resource via `secretRef`. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `cluster` | `KubernetesCluster \| string` | ✅ | Target cluster | | `namespace` | `K8sNamespace \| string` | ❌ | Namespace to create the Secret in | | `dependsOn` | `string[]` | ❌ | Explicit dependency ordering | | `data` | `Record` | ❌ | Pre-encoded secret data (base64) | | `stringData` | `Record` | ❌ | Plain-text secret data (encoded by Kyku) | | `secretType` | `string` | ❌ | Kubernetes secret type (`Opaque`, `kubernetes.io/dockerconfigjson`, etc.) | | `secretRef` | `Secret` | ❌ | Reference to an Kyku `Secret` resource to populate from | ## Example ```typescript // Inline secret data const dbCreds = new K8sSecret({ name: 'db-credentials', cluster: myCluster, namespace: myNamespace, stringData: { username: 'admin', password: 's3cret!', }, secretType: 'Opaque', }) // Sourced from Kyku Secret resource const tlsSecret = new K8sSecret({ name: 'tls-certs', cluster: myCluster, secretRef: myKykuSecret, secretType: 'kubernetes.io/tls', }) ``` ## Provider support | Provider | Supported | |----------|:---------:| | Kubernetes | ✅ | | Hetzner | ❌ | | DigitalOcean | ❌ | | AWS | ❌ | | GCP | ❌ | --- # K8sService `K8sService` creates a Kubernetes Service in the target cluster. Services provide stable network endpoints for pods, supporting internal cluster DNS, load-balanced access, and external exposure. ## Service port | Property | Type | Required | Description | |----------|------|----------|-------------| | `port` | `number` | ✅ | Service port | | `targetPort` | `number` | ❌ | Pod port to route to (defaults to `port`) | | `protocol` | `string` | ❌ | Protocol (`TCP`, `UDP`, `SCTP`; defaults to `TCP`) | | `name` | `string` | ❌ | Port name (required for multi-port services) | | `nodePort` | `number` | ❌ | Static node port (for `NodePort` type) | ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `cluster` | `KubernetesCluster \| string` | ✅ | Target cluster | | `namespace` | `K8sNamespace \| string` | ❌ | Namespace to create the Service in | | `dependsOn` | `string[]` | ❌ | Explicit dependency ordering | | `serviceType` | `'ClusterIP' \| 'NodePort' \| 'LoadBalancer' \| 'ExternalName'` | ❌ | Service type (defaults to `ClusterIP`) | | `selector` | `Record` | ❌ | Label selector for pod targeting | | `selectorRef` | `K8sDeployment \| K8sStatefulSet` | ❌ | Reference to a Deployment or StatefulSet (auto-generates selector) | | `ports` | `K8sServicePort[]` | ✅ | Port mappings | ## Example ```typescript // Using selectorRef to target a deployment const apiService = new K8sService({ name: 'api', cluster: myCluster, namespace: myNamespace, serviceType: 'ClusterIP', selectorRef: apiDeployment, ports: [ { port: 80, targetPort: 8080, name: 'http' }, { port: 443, targetPort: 8443, name: 'https' }, ], }) // External load balancer const ingressGateway = new K8sService({ name: 'gateway', cluster: myCluster, serviceType: 'LoadBalancer', selector: { app: 'gateway' }, ports: [{ port: 443, protocol: 'TCP', name: 'https' }], }) ``` ## Provider support | Provider | Supported | |----------|:---------:| | Kubernetes | ✅ | | Hetzner | ❌ | | DigitalOcean | ❌ | | AWS | ❌ | | GCP | ❌ | --- # K8sStatefulSet `K8sStatefulSet` creates a Kubernetes StatefulSet in the target cluster. StatefulSets manage stateful pods with stable, persistent network identities and ordered deployment, scaling, and termination. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `cluster` | `KubernetesCluster \| string` | ✅ | Target cluster | | `namespace` | `K8sNamespace \| string` | ❌ | Namespace to deploy into | | `dependsOn` | `string[]` | ❌ | Explicit dependency ordering | | `image` | `string` | ✅ | Container image (e.g., `'postgres:16'`) | | `replicas` | `number` | ❌ | Desired pod count (defaults to 1) | | `serviceName` | `K8sService \| string` | ✅ | Headless service governing network identity | | `ports` | `{ containerPort: number; name?: string; protocol?: string }[]` | ❌ | Container ports to expose | | `env` | `{ name: string; value?: string; valueFrom?: Record }[]` | ❌ | Environment variables | | `volumeClaimTemplates` | `{ name: string; accessModes: string[]; size: string; storageClassName?: string }[]` | ❌ | Template for per-pod PVCs | | `spec` | `Record` | ❌ | Raw pod template spec overrides | ## Example ```typescript const dbHeadless = new K8sService({ name: 'db-svc', cluster: myCluster, serviceType: 'ClusterIP', clusterIP: 'None', ports: [{ port: 5432, name: 'postgres' }], }) const postgres = new K8sStatefulSet({ name: 'postgres', cluster: myCluster, namespace: myNamespace, image: 'postgres:16', replicas: 3, serviceName: dbHeadless, ports: [{ containerPort: 5432, name: 'postgres' }], env: [ { name: 'POSTGRES_PASSWORD', valueFrom: { secretKeyRef: { name: 'pg-pass', key: 'password' } } }, ], volumeClaimTemplates: [ { name: 'data', accessModes: ['ReadWriteOnce'], size: '100Gi', storageClassName: 'ssd' }, ], }) ``` ## Provider support | Provider | Supported | |----------|:---------:| | Kubernetes | ✅ | | Hetzner | ❌ | | DigitalOcean | ❌ | | AWS | ❌ | | GCP | ❌ | --- # KmsKey A `KmsKey` represents a customer-managed encryption key used for encrypting data at rest — EBS volumes, S3 buckets, RDS instances, and other resources. On AWS this is a KMS Key, on GCP a Cloud KMS CryptoKey. Hetzner and DigitalOcean do not have equivalent standalone key management services. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | yes | Unique resource name | | `id` | `string` | no | Explicit ID (auto-generated UUID if omitted) | | `provider` | `string` | no | Provider label for multi-provider configs | | `tags` | `Record` | no | Arbitrary key-value metadata | | `rotationPeriod` | `string` | no | Auto-rotation period (e.g. `'90d'`, `'1y'`) | ## Example ```typescript import { KmsKey } from '@kykucloud/types' const myKey = new KmsKey({ name: 'data-key', rotationPeriod: '90d', }) ``` ## Provider Support | Provider | Supported | Backend | |----------|-----------|---------| | AWS | ✅ | KMS (Key Management Service) | | GCP | ✅ | Cloud KMS (CryptoKey) | | Hetzner | ❌ | Not available | | DigitalOcean | ❌ | Not available | ## Notes - **AWS KMS** keys can be symmetric or asymmetric (default: symmetric). Rotation period sets automatic yearly rotation (when `rotationPeriod` is specified). Costs $1/month per key plus usage fees. - **GCP Cloud KMS** keys live in a key ring. The provider manages key ring creation automatically. Rotation period uses duration strings like `90d` or `1y`. - **KMS keys** are referenced by other resources (e.g. `Bucket`, `Database`) through provider-specific configuration. Future Kyku versions will add a direct reference field on resource configs. --- # KubernetesCluster A `KubernetesCluster` represents a managed Kubernetes cluster for container orchestration. On AWS this is EKS, on GCP GKE, and on DigitalOcean DOKS. Hetzner does not offer managed Kubernetes (though kubeadm can be used on Hetzner Cloud Servers). ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | yes | Unique resource name | | `id` | `string` | no | Explicit ID (auto-generated UUID if omitted) | | `provider` | `string` | no | Provider label for multi-provider configs | | `tags` | `Record` | no | Arbitrary key-value metadata | | `version` | `string` | no | Kubernetes version (default `'latest'`) | | `nodeInstanceType` | `string` | yes | Instance type for worker nodes (provider-specific) | | `minNodes` | `number` | yes | Minimum node count | | `maxNodes` | `number` | yes | Maximum node count for autoscaler | | `network` | `Vpc \| string` | yes | Vpc for cluster networking | | `region` | `string` | no | Region override | | `privateCluster` | `boolean` | no | Restrict API server to private network | | `nodeDiskSize` | `number` | no | Node disk size in GB | | `autoUpgrade` | `boolean` | no | Enable automatic version upgrades | | `maintenanceWindow` | `string` | no | Preferred maintenance window | ## Example ```typescript import { Vpc, KubernetesCluster } from '@kykucloud/types' const myVpc = new Vpc({ name: 'my-vpc', cidr: '10.0.0.0/16', region: 'eu-central' }) const cluster = new KubernetesCluster({ name: 'prod-cluster', version: '1.28', nodeInstanceType: 't3.medium', minNodes: 3, maxNodes: 10, network: myVpc, privateCluster: true, autoUpgrade: false, nodeDiskSize: 100, }) ``` ## Provider Support | Provider | Supported | Backend | |----------|-----------|---------| | AWS | ✅ | EKS (Elastic Kubernetes Service) | | GCP | ✅ | GKE (Google Kubernetes Engine) | | Hetzner | ❌ | Not available (use `CustomResource` or manual setup) | | DigitalOcean | ✅ | DOKS (DigitalOcean Kubernetes) | ## Notes - **AWS EKS** creates a control plane and a managed node group. Requires IAM roles for the cluster and node group. Cluster endpoint can be public, private, or both. EKS cluster creation is slow (10–15 minutes). - **GCP GKE** supports both standard and autopilot modes. Private clusters restrict the API server endpoint. GKE supports regional clusters (multi-zone) and zonal clusters. - **DigitalOcean DOKS** creates a Kubernetes cluster with a node pool. The `nodeInstanceType` maps to DO Droplet sizes. Node pools can be scaled independently. - **kubeconfig** is available as a resource output after cluster creation. Use `cluster.outputs.kubeconfig` to connect. - **Network**: The cluster is placed in the specified VPC. On AWS, subnets tagged for EKS are required. On GKE, a VPC-native cluster uses secondary IP ranges. --- # LoadBalancer A `LoadBalancer` distributes incoming traffic across a set of virtual machines. It can be an application layer (HTTP/HTTPS) or network layer (TCP/UDP) load balancer. On AWS this is an ALB or NLB, on GCP a multi-resource LB stack, on Hetzner a Load Balancer, and on DigitalOcean a Load Balancer. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | yes | Unique resource name | | `id` | `string` | no | Explicit ID (auto-generated UUID if omitted) | | `provider` | `string` | no | Provider label for multi-provider configs | | `tags` | `Record` | no | Arbitrary key-value metadata | | `lbType` | `'application' \| 'network'` | yes | Application (HTTP/HTTPS) or Network (TCP/UDP) LB | | `vpc` | `Vpc \| string` | yes | Vpc to attach the LB to | | `listeners` | `LoadBalancerListener[]` | yes | Port/protocol listeners with targets | | `healthCheck` | `HealthCheck` | no | Health check configuration | | `spanAcrossAzs` | `boolean` | no | Spread across availability zones (default `true`) | | `securityGroups` | `(SecurityGroup \| string)[]` | no | Security groups attached to the LB | | `size` | `string` | no | Abstract size (provider-specific mapping) | | `staticIp` | `StaticIp \| string` | no | Reserved public IP referenced at create time | ### LoadBalancerListener | Property | Type | Required | Description | |----------|------|----------|-------------| | `port` | `number` | yes | Listener port | | `protocol` | `'http' \| 'https' \| 'tcp' \| 'udp'` | yes | Listener protocol | | `certificate` | `string` | no | SSL/TLS certificate ARN or reference | | `targets` | `LoadBalancerTarget[]` | yes | Target VMs and ports | ### LoadBalancerTarget | Property | Type | Required | Description | |----------|------|----------|-------------| | `vm` | `Vm \| string \| OutputValue` | yes | Target VM or output reference | | `port` | `number` | yes | Target port | ### HealthCheck | Property | Type | Required | Description | |----------|------|----------|-------------| | `protocol` | `'http' \| 'https' \| 'tcp'` | yes | Health check protocol | | `port` | `number` | yes | Health check port | | `path` | `string` | no | Health check path (HTTP/HTTPS only) | | `interval` | `number` | no | Check interval in seconds | | `timeout` | `number` | no | Check timeout in seconds | | `healthyThreshold` | `number` | no | Consecutive successes to mark healthy | | `unhealthyThreshold` | `number` | no | Consecutive failures to mark unhealthy | ## Example ```typescript import { Vpc, Vm, LoadBalancer } from '@kykucloud/types' const myVpc = new Vpc({ name: 'my-vpc', cidr: '10.0.0.0/16', region: 'eu-central' }) const server = new Vm({ name: 'web', instanceType: 'small', image: 'ubuntu-24.04', network: myVpc, }) const lb = new LoadBalancer({ name: 'web-lb', lbType: 'application', vpc: myVpc, listeners: [{ port: 443, protocol: 'https', targets: [{ vm: server, port: 8080 }], }], healthCheck: { protocol: 'tcp', port: 8080, interval: 10, timeout: 5 }, }) ``` ## Provider Support | Provider | Supported | Backend | |----------|-----------|---------| | AWS | ✅ | ALB (application) / NLB (network) | | GCP | ✅ | Multi-resource stack (health check → backend service → url map → proxy → forwarding rule) | | Hetzner | ✅ | Load Balancer | | DigitalOcean | ✅ | Load Balancer (with `size_unit`) | ## Notes - **GCP LB** is composed of 5+ resources: health check, backend service, URL map, target HTTP(S) proxy, and forwarding rule. Destroy reverses this order. - **AWS ALB** name limit is 32 characters (not 255). ALB provisioning uses `State.Code` — poll for `active`, handle `failed` state. - **Hetzner LB** network attachment is async. Poll the `private_net` field before adding targets (up to 120s). POST to `/load_balancers` does not return `private_net` — must poll GET. - **DigitalOcean LB** uses `size_unit` (1–100) for sizing, not named sizes. Map abstract `size` config to `size_unit`. - **Label selectors** preferred over server IDs for target registration to avoid lookup race conditions and ordering issues. - **AWS listeners** support SSL termination via `certificate` (ACM ARN). --- # Queue A `Queue` represents a message queue for decoupling services and asynchronous processing. On AWS this is SQS, on GCP Pub/Sub. Hetzner and DigitalOcean do not have native managed queue services. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | yes | Unique resource name | | `id` | `string` | no | Explicit ID (auto-generated UUID if omitted) | | `provider` | `string` | no | Provider label for multi-provider configs | | `tags` | `Record` | no | Arbitrary key-value metadata | | `fifo` | `boolean` | no | Enable FIFO (first-in-first-out) ordering (default `false`) | | `retentionSeconds` | `number` | no | Message retention period in seconds (default `86400`) | ## Example ```typescript import { Queue } from '@kykucloud/types' const taskQueue = new Queue({ name: 'task-queue', retentionSeconds: 604800, // 7 days }) const orderQueue = new Queue({ name: 'order-queue', fifo: true, }) ``` ## Provider Support | Provider | Supported | Backend | |----------|-----------|---------| | AWS | ✅ | SQS (Simple Queue Service) | | GCP | ✅ | Pub/Sub | | Hetzner | ❌ | Not available | | DigitalOcean | ❌ | Not available | ## Notes - **AWS SQS** FIFO queues support exactly-once processing and message grouping. Standard queues offer at-least-once delivery. FIFO queue names must end with `.fifo`. - **GCP Pub/Sub** is a publish-subscribe messaging system. Topics and subscriptions are managed together. The `fifo` option maps to Pub/Sub ordering keys. - **Retention**: AWS SQS supports up to 14 days (1,209,600 seconds). GCP Pub/Sub message retention is configurable per subscription. - **Dead-letter queues**: Not yet abstracted in this resource type. Configure through provider-specific settings or `CustomResource`. --- # Role A `Role` defines a set of permissions that can be assigned to `Identity` resources. Permissions use an abstract format (`resource:action`) that maps to provider-native equivalents. On AWS this is an IAM Role with policy documents, on GCP a Custom Role. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | yes | Unique resource name | | `id` | `string` | no | Explicit ID (auto-generated UUID if omitted) | | `provider` | `string` | no | Provider label for multi-provider configs | | `tags` | `Record` | no | Arbitrary key-value metadata | | `permissions` | `Permission[]` | yes | List of abstract permissions | ### Permission values | Permission | Description | |------------|-------------| | `compute:read` | List/describe compute resources | | `compute:write` | Create/update/delete compute resources | | `compute:admin` | Full compute access | | `network:read` | List/describe network resources | | `network:write` | Create/update/delete network resources | | `network:admin` | Full network access | | `storage:read` | List/read storage resources | | `storage:write` | Create/update/delete storage resources | | `storage:admin` | Full storage access | | `database:connect` | Connect to databases | | `database:read` | Read database metadata | | `database:write` | Modify database resources | | `database:admin` | Full database access | | `iam:read` | List/describe IAM resources | | `iam:write` | Create/update/delete IAM resources | | `iam:admin` | Full IAM access | | `string` | Provider-specific permission (e.g. `ec2:DescribeInstances`) | ## Example ```typescript import { Role } from '@kykucloud/types' const readonlyRole = new Role({ name: 'readonly', permissions: ['compute:read', 'storage:read', 'database:read'], }) const customRole = new Role({ name: 'custom', permissions: ['compute:read', 's3:GetObject', 's3:ListBucket'], }) ``` ## Provider Support | Provider | Supported | Backend | |----------|-----------|---------| | AWS | ✅ | IAM Role (with managed/inline policy) | | GCP | ✅ | Custom Role (IAM API) | | Hetzner | ❌ | Not available | | DigitalOcean | ❌ | Not available | ## Notes - **AWS IAM** roles require policy cleanup before deletion — `destroy()` must detach managed policies and delete inline policies. AWS role names are immutable (rename is a separate operation). - **GCP Custom Roles** are created via `POST /v1/projects/{project}/roles` using the IAM API base (`iam.googleapis.com`). - **Abstract permissions** like `compute:read` map to provider-specific actions (AWS: `ec2:Describe*`, GCP: `compute.instances.list`). - **Provider-specific string permissions** bypass the abstract mapping and are passed directly to the provider. Use when the abstract permissions are insufficient. --- # Secret A `Secret` stores sensitive values such as API keys, database passwords, or tokens. Secrets are encrypted at rest in Kyku state (AES-256-GCM with PBKDF2) and stored in the provider's secrets manager. On AWS this is Secrets Manager, on GCP Secret Manager. Hetzner and DigitalOcean have limited or no native secrets management. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | yes | Unique resource name | | `id` | `string` | no | Explicit ID (auto-generated UUID if omitted) | | `provider` | `string` | no | Provider label for multi-provider configs | | `tags` | `Record` | no | Arbitrary key-value metadata | | `value` | `string` | yes | Secret value (encrypted in state) | ## Example ```typescript import { Secret } from '@kykucloud/types' const apiKey = new Secret({ name: 'stripe-api-key', value: 'sk_live_abc123…', }) ``` ## Provider Support | Provider | Supported | Backend | |----------|-----------|---------| | AWS | ✅ | Secrets Manager | | GCP | ✅ | Secret Manager | | Hetzner | ❌ | Not available | | DigitalOcean | ❌ | Not available | ## Notes - **Encryption**: Value is encrypted with AES-256-GCM in Kyku state before storage. Decrypted only during the apply phase. - **Rotation**: AWS Secrets Manager supports automatic rotation via `rotationPeriod`. Configure via provider-specific settings. - **Access**: Secrets can be referenced from `Database.password` using `Secret` type. --- # SecurityGroup A `SecurityGroup` defines network access rules for `Vm` and `LoadBalancer` resources. It contains ingress and egress rules with protocol, port ranges, and source/destination CIDRs or references to other security groups. On AWS this is an EC2 Security Group, on GCP a set of Firewall Rules, on Hetzner a Firewall, and on DigitalOcean a Cloud Firewall. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | yes | Unique resource name | | `id` | `string` | no | Explicit ID (auto-generated UUID if omitted) | | `provider` | `string` | no | Provider label for multi-provider configs | | `tags` | `Record` | no | Arbitrary key-value metadata | | `ingress` | `SecurityGroupRule[]` | yes | List of inbound rules | | `egress` | `SecurityGroupRule[]` | no | List of outbound rules | | `vpc` | `Vpc \| string` | no | Vpc this security group belongs to | ### SecurityGroupRule | Property | Type | Required | Description | |----------|------|----------|-------------| | `protocol` | `'tcp' \| 'udp' \| 'icmp' \| 'all'` | yes | IP protocol | | `fromPort` | `number` | yes | Start of port range | | `toPort` | `number` | yes | End of port range | | `sources` | `(string \| SecurityGroup)[]` | yes | CIDR blocks or SecurityGroup references | | `description` | `string` | no | Human-readable description | ## Example ```typescript import { Vpc, SecurityGroup } from '@kykucloud/types' const myVpc = new Vpc({ name: 'my-vpc', cidr: '10.0.0.0/16', region: 'eu-central' }) const webSg = new SecurityGroup({ name: 'web-sg', vpc: myVpc, ingress: [ { protocol: 'tcp', fromPort: 80, toPort: 80, sources: ['0.0.0.0/0'] }, { protocol: 'tcp', fromPort: 443, toPort: 443, sources: ['0.0.0.0/0'] }, ], egress: [ { protocol: 'all', fromPort: 0, toPort: 0, sources: ['0.0.0.0/0'] }, ], }) ``` ## Provider Support | Provider | Supported | Backend | |----------|-----------|---------| | AWS | ✅ | EC2 Security Group (`IpPermissions`/`IpPermissionsEgress`) | | GCP | ✅ | Firewall Rules (one rule per ingress/egress entry) | | Hetzner | ✅ | Firewall | | DigitalOcean | ✅ | Cloud Firewall | ## Notes - **GCP** creates one firewall rule per ingress/egress entry, named `${sg.name}-ingress-0`, `${sg.name}-egress-0`, etc. Destroy cleans up by name prefix. - **DigitalOcean** firewall rules use `ports` as a string (`"22"`, `"80-443"`, `"all"`). ICMP rules omit the `ports` field. Firewall attachment uses tags: droplets tagged with `kyku-sg:` are matched by `sources.tags`. - **Hetzner** firewall destroy may fail with "still in use" if label selectors are active. PUT `{ applied_to: [] }` before DELETE to clear selectors. - **AWS** security group names are immutable — the SG cannot be renamed after creation. Referencing other security groups as sources creates cross-SG rules. - **Rule ordering**: Rules are evaluated in order within each provider but the exact behavior varies. Best practice is to define permissive egress and restrictive ingress. --- # SshKey An `SshKey` resource manages an SSH public key for authentication to virtual machines. On AWS this is an EC2 Key Pair, on Hetzner an SSH Key, and on DigitalOcean an SSH Key. GCP does not support standalone SSH keys — use the `sshPublicKey` field directly on `Vm` instead. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | yes | Unique resource name | | `id` | `string` | no | Explicit ID (auto-generated UUID if omitted) | | `provider` | `string` | no | Provider label for multi-provider configs | | `tags` | `Record` | no | Arbitrary key-value metadata | | `publicKey` | `string` | yes | Public key contents (e.g. `ssh-ed25519 AAAA…`) | ## Example ```typescript import { SshKey, Vm, Vpc } from '@kykucloud/types' const myKey = new SshKey({ name: 'deploy-key', publicKey: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI…', }) const myVpc = new Vpc({ name: 'my-vpc', cidr: '10.0.0.0/16', region: 'eu-central' }) const server = new Vm({ name: 'web', instanceType: 'small', image: 'ubuntu-24.04', network: myVpc, sshKey: myKey, }) ``` ## Provider Support | Provider | Supported | Backend | |----------|-----------|---------| | AWS | ✅ | EC2 Key Pair (`ImportKeyPairCommand`) | | GCP | ❌ | Not supported — use `sshPublicKey` field directly on Vm | | Hetzner | ✅ | SSH Key | | DigitalOcean | ✅ | SSH Key | ## Notes - **AWS** requires the key to be imported before VM creation. The provider uses `ImportKeyPairCommand` to upload the public key with a name derived from the SshKey resource name. Pass the key name (not the public key string) to `RunInstances.KeyName`. - **GCP** throws `UnsupportedFeatureError`. Set `vm.sshPublicKey` directly instead — GCP injects it as project-level metadata (`ssh-keys`). - **Name length**: Hetzner 63 chars, DO 255 chars. Deploy prefix adds 9 characters. --- # StaticIp A `StaticIp` is a reserved public address that survives VM replacement. On AWS this is an Elastic IP, on GCP a regional Address, on Hetzner a Floating IP, and on DigitalOcean a Floating IP. `Vm` and `LoadBalancer` reference it as an object (not a string-only ID) so the graph creates the address first. Attachment happens at create time only — imported VMs are not re-associated. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | yes | Unique resource name | | `id` | `string` | no | Explicit ID (auto-generated UUID if omitted) | | `provider` | `string` | no | Provider label for multi-provider configs | | `tags` | `Record` | no | Arbitrary key-value metadata | | `region` | `string` | no | Abstract region (`us-east`, `eu-central`, …) | | `ipVersion` | `'ipv4' \| 'ipv6'` | no | Address family (default `ipv4`) | ## Example ```typescript import { LoadBalancer, StaticIp, Vm, Vpc } from '@kykucloud/types' const vpc = new Vpc({ name: 'web-vpc', cidr: '10.0.0.0/16', region: 'eu-central' }) const ip = new StaticIp({ name: 'web-ip', region: 'eu-central', }) const server = new Vm({ name: 'web-server', instanceType: 'small', image: 'ubuntu-24.04', network: vpc, staticIp: ip, }) const lb = new LoadBalancer({ name: 'web-lb', lbType: 'application', vpc, listeners: [{ port: 80, protocol: 'http', targets: [{ vm: server, port: 80 }] }], staticIp: ip, }) ``` ## Provider Support | Provider | Supported | Backend | |----------|-----------|---------| | AWS | ✅ | Elastic IP (`AllocateAddress`) | | GCP | ✅ | Compute Address (regional) | | Hetzner | ✅ | Floating IP | | DigitalOcean | ✅ | Floating IP | ## Notes - **Outputs**: `address` is the allocated public IP. AWS also exposes `allocationId`. - **AWS**: IPv6 Elastic IPs are not supported. Name lookup uses the `Name` tag. - **DigitalOcean**: Floating IPs have no name field. Idempotent create relies on state `providerId` (the address). Object-ref attach at VM create is skipped; pass the address string to attach. - **Attachment**: Create-time only. Destroy the VM/LB before the StaticIp. --- # Subnet A `Subnet` is a **synthetic resource** — it is never declared directly in user config. The planner auto-injects subnets based on `Vpc.distributeAcrossAzs`. Each subnet is either public (with Internet Gateway route) or private (NAT-only). Providers create real cloud subnets during Vpc creation. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | yes | Subnet name (auto-generated by planner) | | `vpc` | `Vpc \| string` | yes | Parent VPC reference | | `cidr` | `string` | yes | CIDR block within the VPC | | `az` | `string` | no | Availability zone override | | `public` | `boolean` | yes | Whether this subnet has a route to the internet | While `Subnet` can be constructed directly in code, the recommended approach is to rely on auto-injection via `Vpc.distributeAcrossAzs`. ## Example ```typescript import { Vpc } from '@kykucloud/types' const myVpc = new Vpc({ name: 'my-vpc', cidr: '10.0.0.0/16', region: 'eu-central', distributeAcrossAzs: 3, // creates 3 public + 3 private subnets }) ``` ## Provider Support | Provider | Supported | Backend | |----------|-----------|---------| | AWS | ✅ | EC2-VPC subnet (public + private per AZ) | | GCP | ✅ | Synthetic (auto-injected by planner; no GCP subresource) | | Hetzner | ✅ | Synthetic (auto-injected by planner; subnets created as part of Network) | | DigitalOcean | ✅ | Synthetic (auto-injected by planner; subresources within VPC) | ## Notes - **Synthetic resource**: Subnet has no standalone cloud lifecycle. The parent `Vpc` manager handles creation and destruction. - **AWS**: `distributeAcrossAzs` creates paired public/private subnets per AZ. The AWS provider always creates this topology. - **Hetzner**: Servers need a subnet before attaching to a network. Always create subnets via `distributeAcrossAzs >= 1`. - **GCP & DO**: Subnet is an auto-injected abstraction. `createResource`/`destroyResource` are no-ops; VpcManager handles everything. --- # TargetGroup A `TargetGroup` is a logical binding between a `Vm` and a `LoadBalancer`. It uses label selectors rather than server IDs — the VM is tagged with a label at creation time, and the load balancer discovers it via the label selector. On AWS this maps to a Target Group, on Hetzner and DigitalOcean to LB target label selectors. GCP does not support TargetGroup — use managed instance groups instead. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | yes | Unique resource name | | `id` | `string` | no | Explicit ID (auto-generated UUID if omitted) | | `provider` | `string` | no | Provider label for multi-provider configs | | `tags` | `Record` | no | Arbitrary key-value metadata | | `port` | `number` | yes | Target port for traffic | | `label` | `string` | no | Label selector (defaults to `kyku-tg=`) | | `vm` | `Vm \| string` | yes | Target VM reference | | `lb` | `LoadBalancer \| string` | yes | Parent LoadBalancer reference | ## Example ```typescript import { Vpc, Vm, LoadBalancer, TargetGroup } from '@kykucloud/types' const myVpc = new Vpc({ name: 'my-vpc', cidr: '10.0.0.0/16', region: 'eu-central' }) const server = new Vm({ name: 'web', instanceType: 'small', image: 'ubuntu-24.04', network: myVpc, }) const lb = new LoadBalancer({ name: 'web-lb', lbType: 'application', vpc: myVpc, listeners: [{ port: 80, protocol: 'http', targets: [] }], }) const tg = new TargetGroup({ name: 'web-tg', port: 8080, vm: server, lb: lb, }) ``` ## Provider Support | Provider | Supported | Backend | |----------|-----------|---------| | AWS | ✅ | Target Group (ALB/NLB) | | GCP | ❌ | Not supported — use managed instance groups | | Hetzner | ✅ | LB target with label selector | | DigitalOcean | ✅ | LB target with label selector | ## Notes - **Label selectors**: The `label` property defaults to `kyku-tg=`. The VM's labels are set from its `tags` at creation time. The LB target uses `label_selector: { selector: "kyku-tg=" }` to discover the VM. - **GCP**: TargetGroup throws `UnsupportedFeatureError`. Use managed instance groups for GCP traffic distribution. - **Hetzner**: Do not set `use_private_ip: true` when using label selectors — the API returns an error. - **AWS**: Target Groups are a separate resource with health check configuration. The LB listener `targets` array should reference the TargetGroup or use the label selector pattern. - **Circular dependencies**: Avoid `TargetGroup ↔ LoadBalancer` cycles by making only one direction reference the other. --- # Vm A `Vm` represents a virtual machine instance — an EC2 instance on AWS, a Compute Engine VM on GCP, a Cloud Server on Hetzner, or a Droplet on DigitalOcean. It is the primary compute resource and must be attached to a Vpc network. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | yes | Unique resource name | | `id` | `string` | no | Explicit ID (auto-generated UUID if omitted) | | `provider` | `string` | no | Provider label for multi-provider configs | | `tags` | `Record` | no | Arbitrary key-value metadata | | `instanceType` | `InstanceType` | yes | Abstract size: `'micro'` … `'8xlarge'` or a provider-specific map | | `image` | `ImageType` | yes | Abstract image: `'ubuntu-22.04' \| 'ubuntu-24.04' \| 'debian-12'` or provider-specific map | | `network` | `Vpc \| string` | yes | Vpc this instance belongs to | | `subnet` | `Subnet \| string` | no | Specific subnet override (auto-selected if omitted) | | `securityGroups` | `(SecurityGroup \| string)[]` | no | Security groups attached to this instance | | `sshPublicKey` | `string` | no | Public key string for SSH access | | `sshKey` | `SshKey \| string` | no | Reference to an `SshKey` resource | | `userData` | `string` | no | Cloud-init / startup script | | `distributeAcrossAzs` | `boolean` | no | Spread across availability zones (default `true`) | | `staticIp` | `StaticIp \| string` | no | Reserved public IP attached at create time | ## Example ```typescript import { Vpc, Vm, SecurityGroup } from '@kykucloud/types' const myVpc = new Vpc({ name: 'my-vpc', cidr: '10.0.0.0/16', region: 'eu-central' }) const webSg = new SecurityGroup({ name: 'web-sg', ingress: [{ protocol: 'tcp', fromPort: 80, toPort: 80, sources: ['0.0.0.0/0'] }], }) const server = new Vm({ name: 'web-server', instanceType: 'medium', image: 'ubuntu-24.04', network: myVpc, securityGroups: [webSg], sshPublicKey: 'ssh-ed25519 AAAAC3…', userData: '#!/bin/bash\necho "hello" > /etc/motd', }) ``` ## Provider Support | Provider | Supported | Backend | |----------|-----------|---------| | AWS | ✅ | EC2 (`RunInstances`) | | GCP | ✅ | Compute Engine | | Hetzner | ✅ | Cloud Server | | DigitalOcean | ✅ | Droplet | ## Notes - **Immutable properties**: `network` cannot be changed after creation on AWS (can't move between VPCs). `instanceType` can be changed (stop + resize) on some providers. - **Image names**: Abstract names like `'ubuntu-24.04'` are mapped per provider. Provider-specific maps let you pin exact images. - **SSH key**: On Hetzner and DO, `sshPublicKey` or `sshKey` imports the key before provisioning. On GCP, the key is set as project metadata. On AWS, the key is imported as a Key Pair. - **User data**: Cloud-init scripts run on first boot. AWS supports up to 16 KB base64-encoded. - **Name length**: Hetzner 63 chars, GCP 62 chars, DO 255 chars. Deploy prefix adds 9 characters. - **Distribute across AZs**: When `true`, instances are spread evenly across available zones. On Hetzner this distributes across subnets in different zones. --- # Volume A `Volume` represents a block storage device that can be attached to a `Vm`. On AWS this is an EBS volume, on GCP a Persistent Disk. Hetzner and DigitalOcean provide block storage with their cloud server offerings (volume API available on Hetzner, not on DO). ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | yes | Unique resource name | | `id` | `string` | no | Explicit ID (auto-generated UUID if omitted) | | `provider` | `string` | no | Provider label for multi-provider configs | | `tags` | `Record` | no | Arbitrary key-value metadata | | `size` | `number` | yes | Volume size in GB | | `vm` | `Vm \| string` | no | VM to attach the volume to | ## Example ```typescript import { Vpc, Vm, Volume } from '@kykucloud/types' const myVpc = new Vpc({ name: 'my-vpc', cidr: '10.0.0.0/16', region: 'eu-central' }) const server = new Vm({ name: 'db-server', instanceType: 'large', image: 'ubuntu-24.04', network: myVpc, }) const dataVolume = new Volume({ name: 'data-volume', size: 500, vm: server, }) ``` ## Provider Support | Provider | Supported | Backend | |----------|-----------|---------| | AWS | ✅ | EBS (Elastic Block Store) | | GCP | ✅ | Persistent Disk | | Hetzner | ✅ | Volume | | DigitalOcean | ❌ | Not available | ## Notes - **AWS EBS** volumes are AZ-specific. If `vm` is specified, the volume is created in the same AZ as the VM. Types include `gp3` (general purpose), `io2` (provisioned IOPS), and `st1` (throughput optimized). Default type is `gp3`. - **GCP Persistent Disks** support both standard and SSD types. Disks are zonal resources. Snapshots are managed separately. - **Hetzner Volumes** are created in the same location as the attached server. Minimum size is 10 GB. - **Volume attachment** is handled automatically when `vm` is specified. Without `vm`, the volume is created unattached. - **Immutable**: Volume size can be increased but not decreased on most providers. AZ/zone cannot be changed. --- # Vpc A `Vpc` represents an isolated virtual network. It is the foundational network resource — every `Vm`, `LoadBalancer`, `Database`, and `Cache` must reference a Vpc. In AWS this is an EC2-VPC, in GCP a VPC Network, in Hetzner a Network, and in DigitalOcean a VPC. ## Config | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | yes | Unique resource name | | `id` | `string` | no | Explicit ID (auto-generated UUID if omitted) | | `provider` | `string` | no | Provider label for multi-provider configs | | `tags` | `Record` | no | Arbitrary key-value metadata | | `cidr` | `string` | yes | CIDR block (e.g. `10.0.0.0/16`) | | `region` | `string \| RegionMap` | yes | Abstract region name or per-provider map | | `distributeAcrossAzs` | `number` | no | Number of AZs to distribute subnets across (default `3`) | ## Example ```typescript import { Vpc } from '@kykucloud/types' const myVpc = new Vpc({ name: 'my-vpc', cidr: '10.0.0.0/16', region: 'eu-central', distributeAcrossAzs: 2, }) ``` ## Provider Support | Provider | Supported | Backend | |----------|-----------|---------| | AWS | ✅ | EC2-VPC with IGW, NAT gateways, route tables, subnets | | GCP | ✅ | VPC Network | | Hetzner | ✅ | Network | | DigitalOcean | ✅ | VPC | ## Notes - **CIDR validation**: DigitalOcean requires private ranges (`10.x`, `172.16-31.x`, `192.168.x`). - **AWS VPC auto-creates** a full network stack: Internet Gateway, NAT gateways per public subnet, Elastic IPs, and public/private route tables. NAT gateways incur ongoing costs. - **Hetzner Network zones** are abstract (`eu-central`), not location-specific (`fsn1`, `nbg1`). - **Immutable**: VPC CIDR and region cannot be changed after creation on AWS. - **Name length**: AWS 255 chars (tags), GCP 62 chars, Hetzner 63 chars, DO 255 chars. Deploy prefix adds 9 characters. --- # Roadmap Kyku's refinement tracks three themes: BDD testing (✅ done), feature completeness (in progress), and documentation + agent tooling (✅ done). ## Current State | Area | Status | |------|--------| | Core engine | ✅ DAG, diff, plan, apply, state, crypto, outputs, state locking, plan files | | Providers | ✅ AWS, GCP, Hetzner, DigitalOcean, Kubernetes | | Resources | ✅ 32 first-class types (incl. full K8s workload set) | | CLI | ✅ 14 commands: init, validate, plan, apply, destroy, output, refresh, graph, import, state list/show/rm | | Testing | ✅ Unit tests + BDD suite (30 scenarios) | | Docs | ✅ This site (82 pages), llms.txt artifacts, docs MCP server | ## Completed Milestones ### Phase A — Foundation ✅ - BDD harness — Vitest + vitest-cucumber, 30 scenarios across engine, plan, apply, destroy, crypto, graph, and provider parity - `kyku validate` — static config validation without cloud calls - State subcommands — `state list`, `state show`, `state rm` - `kyku refresh` — re-read cloud state, update state file ### Phase B — Import & Plan Files ✅ - `kyku import ` — adopt existing cloud resources - Plan files — `kyku plan --out=plan.json` + `kyku apply --plan plan.json` - State locking — file lock with stale detection and timeout ### Documentation & Agent Tooling ✅ - This docs site — Astro Starlight, 82 pages, full-text search - LLM artifacts — [`/llms.txt`](/llms.txt), [`/llms-full.txt`](/llms-full.txt), [`/llms-small.txt`](/llms-small.txt) - Docs MCP server — `search_docs`, `get_page`, `list_resources`, `list_providers` tools for AI agents ## Phase C — Operational Maturity (next) 1. ~~**Drift detection flag**~~ — `--detailed-exitcode` on plan; fail CI on unexpected drift 2. ~~**Remote state backends**~~ — S3, GCS, Spaces, and HTTP 3. ~~**`--target` / `--exclude`**~~ — scoped plan/apply (id, glob, or `tags.key=value`; resource + ancestors) 4. ~~**Rollback**~~ — failed apply restores the pre-apply state backup; `--rollback-destroy` optionally destroys leftovers 5. ~~**Plan diff as JSON**~~ — `kyku plan --json` plus plan-file serial/hash checks **Deliverable:** Team CI/CD workflows fully supported. ## Phase D — Ecosystem 1. **`kyku test`** — typed TypeScript policy assertions against the offline plan 2. **Modules** — convention: factories return `BaseResource[]` (see Config → TypeScript modules) 3. **New resource types** — Container Registry, Static IP, CDN/WAF 4. **DX polish** — `doctor`, `version`, `completion` (no `fmt` yet) 5. **New providers** — Scaleway/Azure based on demand ## Success Metrics | Metric | Target | Current | |--------|--------|---------| | BDD coverage | ≥30 scenarios | ✅ 30 | | CLI parity | 12+ subcommands | ✅ 14 | | Resource coverage | 12+ types | ✅ 32 | | Docs coverage | All resources/providers/CLI | ✅ 82 pages | | Remote state | S3 / GCS / Spaces / HTTP | ✅ shipped | | Drift detection | `--detailed-exitcode` | ✅ shipped |