--- name: kubestack description: Scaffold and maintain Kubestack framework repositories — OpenTofu/Terraform code for Kubernetes platform infrastructure on AWS, Google Cloud, Azure, and Scaleway. Use when initializing a new Kubestack repository, when changing clusters, node pools, environments, or platform features in an existing one, or when setting up its CI/CD automation. license: Apache-2.0 compatibility: Requires OpenTofu or Terraform to validate configurations, git for the GitOps workflow, and network access to fetch the framework version from kubestack.com and upstream manifests or Helm charts. Helm is required for chart-based platform features. metadata: author: kbst source: https://github.com/kbst/terraform-kubestack --- # Kubestack Agent Skill — Framework Usage > This skill teaches an AI coding agent to scaffold and maintain Kubestack framework repositories — no CLI needed. For contributing to the framework itself, see the `AGENTS.md` in the framework repository root. --- ## Start Here This skill is published at https://www.kubestack.com/SKILL.md. Read it end to end before starting any task. How you learn and store this skill is up to your agent and harness — this skill does not prescribe where skills live. If the repository you are working in contains an `AGENTS.md`, those are the user's own rules for that repository; follow them alongside this skill. Refresh this skill from the published URL occasionally, see [Other hard rules](#other-hard-rules). For a brand-new repository, follow [Scaffolding a New Repository](references/bootstrap.md#scaffolding-a-new-repository) first — it creates the `README.md` and other shared files the rest of this skill reads from. On an existing repository: **Before doing anything else, read the Platform section of `README.md`.** Use every value recorded there as-is — never ask the user for a value that is already recorded. If a required value is missing, ask the user, record the answer in `README.md`, then write the files. --- ## Constraints These rules are absolute. They apply to every task without exception. ### Commands you must never run - `terraform apply` / `tofu apply` - `terraform destroy` / `tofu destroy` - `terraform init` / `tofu init` - `terraform plan` / `tofu plan` - Any `git` command that's not read-only — no commits, pushes, branch creation, or tagging These commands make irreversible changes to live infrastructure or repository state. A human who has reviewed what will happen must always run them. If the user asks you to run any of them, explain that you cannot and give them the exact command to run themselves. ### The one command you must run After writing or editing any `.tf` file, run `tofu validate` (or `terraform validate`). It is safe — no cloud API calls, changes nothing. ### Other hard rules - Never modify `.terraform.lock.hcl` by hand. - Never hardcode secrets, credentials, keys, or certificates in any `.tf` file. - Never edit `modules//manifests/upstream.yaml` directly for Helm-based features — it is a generated file. To change what is rendered, edit `values.yaml` and re-run the `helm template` command recorded in `modules//README.md`. - Refresh this skill occasionally: fetch the published version at https://www.kubestack.com/SKILL.md — at the latest whenever unsure whether your stored copy is still current — and compare it to the copy your agent has stored. If it differs, update your stored copy the way your agent manages skills. - Always update the Platform section of `README.md` after any task that adds, changes, or removes a platform component or setting. - Always end every task with the GitOps follow-up instructions from [After Every Task](#after-every-task). - In staged processes, prepare the file changes for the current stage only, hand off with the GitOps follow-up instructions, and wait for the user to confirm the pipeline has successfully applied before starting the next stage. Never run any git or terraform command beyond `validate` and read-only git. --- ## Update Obligations Before considering a task complete, apply every obligation that matches what was done. When updating `README.md`, replace existing values — never append duplicates or leave stale entries. | What was done | Obligation | |---|---| | First-time repository scaffold | Fill the Configuration table in `README.md` (framework version, environment names, base environment, `base_domain`). Add the first cluster to the Clusters table. | | New cluster scaffolded (Stage 1) | Add a row to the Clusters table. Give the GitOps follow-up instructions; remind that node pools and platform features must follow in separate stages. | | Cluster removed | Remove its row from the Clusters table and all its node pool and feature rows. | | New node pool scaffolded | Add a row to the Node Pools table. | | Node pool removed | Remove its row from the Node Pools table. | | New platform feature scaffolded | Add a row to the Platform Features table. | | Catalog module migrated to a platform feature | Follow [Migrating a Catalog Module to a Platform Feature](references/catalog-migration.md#migrating-a-catalog-module-to-a-platform-feature). Add a `moved` block per cluster binding file and preserve the namespace. Update or add the feature's row in the Platform Features table. | | Platform feature removed | Remove its row from the Platform Features table. | | Framework version changed | Update the version in the Configuration table, the `?ref=` value in every Kubestack framework module `source` reference, and the image tag in `Dockerfile`. | | Framework version upgraded from v0.19.2-beta.0 or earlier | Apply every step of [Upgrading from v0.19.2-beta.0 or earlier](references/framework-upgrades.md#upgrading-from-v0192-beta0-or-earlier) in addition to the version-reference updates. | | Environment added | Add the name to the Configuration table and the new key to the `configuration` map in every module in the repository. | | Environment removed | **Destructive.** Do not make any file changes immediately — follow [Removing an environment](references/environments.md#removing-an-environment). | | Environment renamed | **Destructive.** Do not make any file changes immediately — follow [Renaming an environment](references/environments.md#renaming-an-environment). | | Any task completed | Give the user the full GitOps follow-up instructions from [After Every Task](#after-every-task). | --- ## About Kubestack Kubestack is an OpenTofu/Terraform framework for platform engineering teams: - **Cluster modules** — provision a managed Kubernetes cluster (EKS on AWS, GKE on Google Cloud, AKS on Azure, Kapsule on Scaleway) and all required cloud infrastructure (VPC, IAM, subnets, etc.). - **Node-pool modules** — add extra node pools to an existing cluster. - **Platform feature modules** — deploy Kubernetes resources to a cluster (nginx ingress, Prometheus, cert-manager, etc.). All modules implement a shared configuration inheritance model so that all environments derive from a single base configuration, preventing drift. Core concepts: - **Environments = Terraform workspaces.** Each environment (e.g. `ops`, `apps`) is a completely independent copy of the entire platform in the cloud. Nothing is shared between environments. - **Inheritance.** The base environment (`apps`, or whichever key is set as `configuration_base_key`) is inherited by all other environments, which may override individual values. It is not possible to unset an inherited value by setting it to `null`. - **Cluster naming.** Cluster module names follow `__`, e.g. `eks_gc0_eu-west-1`. The name is the Terraform module identifier, the file prefix, and the provider aliases. - **File-per-cluster convention.** Each cluster has its own `_cluster.tf` and `_providers.tf`. Node-pool and feature files are prefixed with the cluster name. --- ## Naming Rules This section covers decisions that may not yet be recorded. Once a decision is made, record it in `README.md` and read it from there on every subsequent task — never re-derive it. ### Environment names The default environment names are `ops` (internal, validates changes first) and `apps` (external, runs user workloads). These become Terraform workspace names. | Pattern | When to use | |---|---| | `ops`, `apps` | Default. All application environments (dev, stage, prod) run inside the single `apps` infrastructure environment. | | `ops`, `apps`, `apps-prod` | When non-prod and prod application workloads need blast-radius isolation at the infrastructure level. | Environment names must be consistent across every module in the repository. With three environments, set `configuration_base_key = "apps-prod"` so `apps` and `ops` both inherit from the production baseline. Order `configuration` map keys from most critical to least critical (e.g. `apps-prod`, then `apps`, then `ops`) — see [Configuration Inheritance](#configuration-inheritance). Choosing the right separation mechanism: | Goal | Use | |---|---| | Separate non-prod and prod workloads | Node pools (low overhead) or a third environment (maximum blast-radius protection). Not clusters. | | Separate tenants or workload types | Node pools (low overhead) or additional clusters (maximum blast-radius protection). Not environments. | ### `name_prefix` A short label identifying the cluster's purpose, tenant, or compute type. The cluster name is `--`, e.g. `gc0-apps-eu-west-1`. The numeric suffix makes the prefix a stable, incrementable identity: `gc0` — first general-compute cluster (add `gc1` when `gc0` approaches capacity, before decommissioning `gc0`); `ml0` — first machine-learning cluster; `data0`, `platform0` — departmental or workload-type clusters. Derive the `name_prefix` for a new cluster from the discovery questions in [Adding a Cluster](references/clusters.md#adding-a-cluster): - Same cloud, same region, same purpose → increment the counter on the existing prefix (e.g. `gc0` → `gc1`). - Same cloud, same region, different purpose → new prefix reflecting the new purpose (e.g. `ml0`). - New region or new cloud, same purpose → reuse the same prefix as the equivalent cluster in the other region or cloud (e.g. `gc0` again — the region in the cluster identifier already distinguishes it). ### `base_domain` The domain suffix used to construct each cluster's FQDN: `..`, e.g. `gc0-apps-eu-west-1.aws.infra.example.com`. The user must own and control this domain. It is used for DNS and TLS configuration by platform features. ### Cluster identifier The Terraform module name, file prefix, and provider alias all share the identifier `__`: | Provider | Prefix | Region slug | |---|---|---| | AWS EKS | `eks` | `eu-west-1` (as-is) | | Google GKE | `gke` | `europe-west1` (as-is) | | Azure AKS | `aks` | `westeurope` (as-is) | | Scaleway | `scw` | `fr-par` (as-is) | Examples: `eks_gc0_eu-west-1`, `gke_gc0_europe-west1`, `aks_gc0_westeurope`, `scw_gc0_fr-par`. --- ## Repository Layout ``` . ├── Dockerfile # pins the Kubestack container image version ├── .gitignore # excludes .user/ and .terraform/ ├── versions.tf # required_providers and version constraints ├── state.tf # remote state backend (created during bootstrap) │ ├── _cluster.tf # one file per cluster ├── _providers.tf # one file per cluster │ ├── _node_pool_.tf # one file per additional node pool ├── _feature_.tf # one file per platform feature, per cluster │ ├── modules/ # one subdirectory per platform feature │ └── / │ ├── README.md # upstream source, version, and regeneration command │ ├── versions.tf # required_providers for the module │ ├── variables.tf # only when the module has input variables │ ├── outputs.tf # only when the module exposes output values │ ├── main.tf # calls the kustomization/overlay framework module │ ├── values.yaml # only when upstream is a Helm chart │ └── manifests/ │ └── upstream.yaml # rendered Kubernetes resources for this feature │ ├── README.md └── .user/ # excluded from Git; persists cloud CLI auth ``` --- ## Configuration Inheritance Every module takes a `configuration` map keyed by environment name and an optional `configuration_base_key`. - `configuration_base_key` defaults to `apps`. All other environment keys inherit every attribute from the base. - An attribute set in a non-base environment overrides the inherited value; an attribute absent from it is transparently inherited. - It is **not** possible to unset an inherited value by setting it to `null`. - **Order `configuration` map keys from most critical to least critical environment** (e.g. `apps-prod`, then `apps`, then `ops`) — most-important-first, making the blast-radius priority immediately visible when reading the file. ### Default two-environment layout ``` configuration = { apps = { # All required and optional attributes go here. # ops inherits everything and may override individual values. } ops = {} } ``` ### Three-environment layout Use `apps-prod` as the base when the user wants a separate production environment: ``` configuration_base_key = "apps-prod" configuration = { apps-prod = { # Production values — this is the base all others inherit from. } apps = { # Non-prod overrides, e.g. smaller instance type or replica count. } ops = { # Internal validation environment overrides. } } ``` ### Scaling down non-production environments Set full production-scale values in the base environment and override only cost-sensitive attributes in non-production environments. `ops` `default_node_pool` overrides by provider: | Provider | Attributes (example) | |---|---| | EKS | `instance_types = ["t3.medium"]`, `min_size = 1`, `max_size = 3`, `desired_capacity = 1` | | GKE | `machine_type = "e2-medium"`, `min_node_count = 1`, `max_node_count = 1` (counts are per zone) | | AKS | `vm_size = "Standard_B2s"`, `min_count = 1`, `max_count = 3` | --- ## Task references This file is the core of the skill. The task groups below live in separate reference files — load the file(s) for the task at hand before starting, and only those: | Task | Load | |---|---| | Bootstrap / initialize / scaffold a new repository | [references/bootstrap.md](references/bootstrap.md), then [references/clusters.md](references/clusters.md) for the first cluster | | Add, change, or remove a cluster (incl. its node pools and platform features) | [references/clusters.md](references/clusters.md), [references/node-pools.md](references/node-pools.md), [references/platform-features.md](references/platform-features.md) | | Add or remove a node pool on an existing cluster | [references/node-pools.md](references/node-pools.md) | | Add, update, or remove a platform feature | [references/platform-features.md](references/platform-features.md) | | Migrate a catalog module to a platform feature | [references/catalog-migration.md](references/catalog-migration.md) | | Add, remove, or rename an environment | [references/environments.md](references/environments.md) | | Update the framework version, or upgrade across v0.19.2-beta.0 | [references/framework-upgrades.md](references/framework-upgrades.md) | | Set up or change the CI/CD automation pipeline | [references/ci-cd-automation.md](references/ci-cd-automation.md) | Reference files are self-contained: they do not link to each other, and they point back to sections of this file where needed. --- ## GitOps Workflow All infrastructure changes follow this four-step process. Your role is to produce correct Terraform files and hand off to the user. Applying changes is always the responsibility of the GitOps pipeline — a human must approve before anything executes against real infrastructure. 1. **Change** — Make changes in a feature branch. Push the branch. The pipeline runs `tofu plan` against the `ops` workspace. 2. **Review** — A peer reviews the diff and the plan. Make additional commits if changes are requested. 3. **Merge** — Merge to main. The pipeline applies to `ops`, then plans against `apps`. 4. **Promote** — Tag the merge commit with an `apps-deploy-*` tag. The pipeline applies to `apps`. On three-environment platforms, tag with `apps-prod-deploy-*` instead to apply to `apps-prod`. --- ## After Every Task At the end of every task that writes or deletes files, give the user the instructions below. Adapt the branch name and file list to what was actually changed. If the task was a framework version update, include the lock file refresh step (marked below) between staging and committing. --- > The Terraform files are ready. Here is what to do next to get these changes applied through the GitOps pipeline. > > **1. Create a feature branch and stage the changes:** > ``` > git checkout -b > git add > ``` > > *(Framework version update only)* **Refresh the provider lock file** before committing: > ``` > tofu init -upgrade > git add .terraform.lock.hcl > ``` > This locks the provider versions required by the new framework release. Include the updated lock file in the same commit. > > **2. Commit and push the branch:** > ``` > git commit -m "" > git push -u origin > ``` > The pipeline will now run `tofu plan` against the `ops` workspace. Review the plan output before proceeding. > > **3. Open a pull request / merge request** and have a peer review both the file diff and the Terraform plan. > > **4. Merge to main** once approved. The pipeline applies to `ops`, then plans against `apps`. > > **5. Promote to `apps`** by tagging the merge commit: > ``` > git tag apps-deploy-$(date +%Y%m%d%H%M%S) > git push origin --tags > ``` > The pipeline will apply the changes to `apps`. > On three-environment platforms, tag with `apps-prod-deploy-$(date +%Y%m%d%H%M%S)` instead to apply to `apps-prod`.