diff --git a/.agents/skills/agentrax-context/SKILL.md b/.agents/skills/agentrax-context/SKILL.md index b811b33..c5de6b8 100644 --- a/.agents/skills/agentrax-context/SKILL.md +++ b/.agents/skills/agentrax-context/SKILL.md @@ -22,6 +22,7 @@ description: Project context and settled architecture decisions for the Agentrax - **Traffic splitting**: Gateway API `HTTPRoute` weighted backends. Not Istio, not ingress annotations. - **Network Isolation**: Two-tier Kubernetes `NetworkPolicy` (`allow-metrics-traffic` in `agentrax-system` allowing operator metrics on TCP 8443; `tenant-agent-isolation` rendered into every `tenant-*` namespace selecting agent pods with `agentrax.io/agent: "true"` for scraping on TCP 8080 and egress to API server/CoreDNS in `kube-system`). No service mesh. - **Cloud Workload Identity**: No static cloud credentials ever. Azure deployments use AKS Workload Identity (`azure.workload.identity/client-id` + `/tenant-id` ServiceAccount annotations; `azure.workload.identity/use: "true"` pod label). AWS deployments use IRSA (`eks.amazonaws.com/role-arn` annotation). In Helm deployments, both are opt-in via `workloadIdentity.enabled` in `charts/agentrax/values.yaml` (disabled by default for portability); in Kustomize deployments, AWS IRSA is activated via the `config/workload-identity/irsa-serviceaccount.yaml` strategic-merge patch. +- **Terraform IaC**: All cluster provisioning and Helm stack installation goes through the `infra/` Terraform modules (`kind_cluster` + `agentrax_stack`). Do not add raw shell provisioning scripts. Dev convenience via `make terraform-apply`; CI gate via `.github/workflows/terraform-lint.yml`. - **MCP registry**: embedded HTTP handler inside the operator process, backed by a `ConfigMap`. Not a separate Deployment, not a new database — HA storage is a v2 item. - **Non-goals**: no model training/fine-tuning, no general-purpose workload management, no service mesh, no UI in v1. Flag any drift toward these rather than quietly implementing them. diff --git a/.github/workflows/terraform-lint.yml b/.github/workflows/terraform-lint.yml new file mode 100644 index 0000000..12b5a2b --- /dev/null +++ b/.github/workflows/terraform-lint.yml @@ -0,0 +1,47 @@ +name: Terraform Lint + +on: + pull_request: + paths: + - "infra/**" + +permissions: + contents: read + +jobs: + terraform-lint: + name: fmt / tflint / trivy + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: "1.9.5" + + - name: Terraform Format Check + run: terraform fmt -check -recursive infra/ + + - name: Set up TFLint + uses: terraform-linters/setup-tflint@v4 + with: + tflint_version: "v0.53.0" + + - name: TFLint init + run: tflint --init --chdir=infra/environments/dev + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: TFLint + run: tflint --chdir=infra/environments/dev + + - name: Trivy IaC Scan + uses: aquasecurity/trivy-action@master + with: + scan-type: config + scan-ref: infra/ + exit-code: "1" + severity: HIGH,CRITICAL diff --git a/.gitignore b/.gitignore index 8ba1736..c3ac790 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,16 @@ go.work # Temporary design scratch pad rough/ + +# Terraform +**/.terraform/ +*.tfstate +*.tfstate.* +crash.log +crash.*.log +override.tf +override.tf.json +*_override.tf +*_override.tf.json +*-config +*kubeconfig* diff --git a/Makefile b/Makefile index 1a7320a..44c244a 100644 --- a/Makefile +++ b/Makefile @@ -168,6 +168,26 @@ check-metrics: ## Diagnostic: check whether custom metrics are available via the $(KUBECTL) get --raw /apis/custom.metrics.k8s.io/v1beta1 2>/dev/null | python3 -m json.tool || \ echo "Custom metrics API not available — ensure Prometheus Adapter is running." +##@ Terraform (Infrastructure) + +TF_DIR ?= infra/environments/dev + +.PHONY: terraform-init +terraform-init: ## Initialise Terraform in the dev environment (downloads providers). + terraform -chdir=$(TF_DIR) init + +.PHONY: terraform-plan +terraform-plan: ## Preview Terraform changes for the dev kind cluster. + terraform -chdir=$(TF_DIR) plan + +.PHONY: terraform-apply +terraform-apply: ## Provision the dev kind cluster and deploy the full Agentrax stack. + terraform -chdir=$(TF_DIR) apply -auto-approve + +.PHONY: terraform-destroy +terraform-destroy: ## Tear down the dev kind cluster and all provisioned resources. + terraform -chdir=$(TF_DIR) destroy -auto-approve + ##@ Dependencies ## Location to install dependencies to diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index cb604bf..dcae614 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -92,7 +92,7 @@ The repository enforces strict directional boundaries to prevent circular depend | Package | Scope & Responsibility | Key Invariants | | ---------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `api/v1alpha1/` | CRD type definitions, OpenAPI markers, schema validation rules, and status condition constants. | **Zero business logic**; only struct declarations and generated deep-copy methods. | -| `internal/controller/` | Controller-runtime reconcile loops (`AgentDeployment`, `TenantQuota`). | Only layer that executes write calls against the Kubernetes API for core-owned resources (Deployments, Services, HPAs, HTTPRoutes, ServiceMonitors). Consumes subsystems via interfaces. | +| `internal/controller/` | Controller-runtime reconcile loops (`AgentDeployment`, `TenantQuota`). | Only layer that executes write calls against the Kubernetes API for core-owned resources (Deployments, Services, HPAs, HTTPRoutes, ServiceMonitors). Consumes subsystems via interfaces. | | `internal/quota/` | Quota arithmetic and concurrency-safe in-flight reservation cache. | Pure arithmetic; mutex-guarded state map; zero direct API server network calls in calculation paths. | | `internal/webhook/` | Validating and Mutating admission webhooks. | Shared with `internal/quota` to enforce admission rules before objects are persisted. | | `internal/scaling/` | HPA synthesis, velocity rules, and dynamic quota ceiling headroom. | Calculates `QuotaHeadroom()` to cap HPA `maxReplicas` and applies stabilization windows. | @@ -325,9 +325,9 @@ Agentrax enforces a zero-trust network perimeter around all AI agent workloads r Agentrax maintains a **two-tier network policy model**: -| Policy Manifest | Target Namespace | Scope & Responsibility | -| :---------------------------- | :---------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `allow-metrics-traffic.yaml` | `agentrax-system` | Protects the operator process; allows Prometheus to scrape operator `/metrics` on port `:8443` (HTTPS). | +| Policy Manifest | Target Namespace | Scope & Responsibility | +| :---------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `allow-metrics-traffic.yaml` | `agentrax-system` | Protects the operator process; allows Prometheus to scrape operator `/metrics` on port `:8443` (HTTPS). | | `tenant-agent-isolation.yaml` | Every `tenant-*` | Isolates agent pods; enforces default-deny on ingress/egress, strictly whitelisting only metrics scraping (`:8080`), Kubernetes API server (`:443`/`:6443`), and CoreDNS (`:53`). | #### Ingress & Egress Invariants: @@ -344,10 +344,10 @@ Agentrax and managed agent pods often require cloud API access (e.g., Azure Key Static credentials baked into `Secret` objects rotate manually, are visible in etcd, and create a long-lived blast radius if leaked. The operator and tenant agents are instead bound to a cloud-managed identity at the pod level: -| Cloud | Mechanism | How it works | -| :------ | :------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------- | -| **Azure** (AKS) | Azure Workload Identity | An OIDC-projected service account token is exchanged for a short-lived Azure AD access token by the Azure Identity SDK. The AKS admission webhook injects the projected volume and `AZURE_*` env vars when the pod carries `azure.workload.identity/use: "true"`. | -| **AWS** (EKS) | IRSA (IAM Roles for SA) | EKS projects a signed OIDC token into the pod; the AWS SDK exchanges it for temporary STS credentials scoped to the bound IAM role via `eks.amazonaws.com/role-arn` annotation. | +| Cloud | Mechanism | How it works | +| :-------------- | :---------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Azure** (AKS) | Azure Workload Identity | An OIDC-projected service account token is exchanged for a short-lived Azure AD access token by the Azure Identity SDK. The AKS admission webhook injects the projected volume and `AZURE_*` env vars when the pod carries `azure.workload.identity/use: "true"`. | +| **AWS** (EKS) | IRSA (IAM Roles for SA) | EKS projects a signed OIDC token into the pod; the AWS SDK exchanges it for temporary STS credentials scoped to the bound IAM role via `eks.amazonaws.com/role-arn` annotation. | #### Helm Configuration @@ -380,17 +380,73 @@ When `workloadIdentity.enabled=true`: --- +### 4.8 Infrastructure as Code — Terraform Module + +Agentrax ships a Terraform module under `infra/` that replaces the manual `make deploy-deps && make deploy` sequence with a single declarative apply. The module targets a local `kind` cluster for development and is designed to be re-used against an Azure AKS cluster in production. + +#### Directory Structure + +``` +infra/ +├── modules/ +│ ├── kind_cluster/ # kind cluster via tehcyx/kind provider +│ │ ├── main.tf # kind_cluster resource; exposes kubeconfig outputs +│ │ ├── variables.tf +│ │ └── outputs.tf # kubeconfig, endpoint, client credentials +│ └── agentrax_stack/ # cert-manager → kube-prometheus-stack → agentrax +│ ├── main.tf # three helm_release resources in dependency order +│ ├── variables.tf +│ └── outputs.tf +└── environments/ + ├── dev/ # local kind — local backend, no shared state + │ ├── main.tf # calls both modules; configures helm/kubernetes providers + │ ├── variables.tf + │ └── outputs.tf + └── prod/ # Azure AKS stub (remote backend, azurerm provider) + └── README.md +``` + +#### Dependency Order + +`cert-manager` must be ready before the `agentrax` webhook certificates can be issued. `kube-prometheus-stack` must be ready before canary rollout PromQL evaluation is live. Terraform `depends_on` chains enforce this strictly: + +``` +kind_cluster → helm_release.cert_manager → helm_release.kube_prometheus_stack → helm_release.agentrax +``` + +#### Developer Workflow + +```bash +make terraform-init # initialise providers +make terraform-plan # preview changes +make terraform-apply # provision cluster + full stack +make terraform-destroy # tear down everything +``` + +The default `TF_DIR` is `infra/environments/dev` and can be overridden: `make terraform-apply TF_DIR=infra/environments/prod`. + +#### CI Lint Gate + +The `.github/workflows/terraform-lint.yml` workflow runs on every PR touching `infra/**`: + +- `terraform fmt -check -recursive` — consistent formatting +- `tflint --chdir=infra/environments/dev` — provider schema & naming rules (config: `infra/.tflint.hcl`) +- `trivy config infra/` — IaC security scan (`HIGH`/`CRITICAL` findings fail the pipeline) + +--- + ## 5. Architectural Decision Records (ADRs) & Trade-Offs -| Decision | Alternative Considered | Trade-Off & Rationale for Agentrax | -| --------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **Gateway API (`HTTPRoute`)** | Istio `VirtualService` / Ingress Annotations | Istio requires a heavy service-mesh control plane and sidecar injection. Ingress annotations lack standardized multi-backend weighted traffic splits. Gateway API provides a lightweight, vendor-neutral standard for traffic shifting. | -| **Custom Canary Rollout Engine** | Argo Rollouts / Flagger | Generic rollout tools treat metric anomalies as pure percentages without low-traffic statistical gating (`minRequestSample`). Building an embedded, re-entrant state machine allowed us to guarantee sample-size gating and MCP tool re-registration upon promotion. | -| **Native HPA via Custom Metrics** | KEDA (`ScaledObject`) | KEDA is powerful but adds external CRD dependencies. Generating native Kubernetes `HorizontalPodAutoscaler` objects tied to the Prometheus Adapter custom metrics pipeline minimized dependencies while giving full control over stabilization windows. | -| **Embedded Registry + ConfigMap Store** | Dedicated etcd / Redis / Database | Adding a dedicated database for service discovery increases operator operational complexity. The in-operator HTTP server with ConfigMap write-through store provides simple, robust storage for hundreds of agent services with cold-restart recovery. | -| **Two-Tier NetworkPolicy** | Istio / Linkerd Service Mesh | Service mesh requires sidecar injection and significant control plane memory overhead. Native Kubernetes NetworkPolicy with label-selector binding (`agentrax.io/agent: "true"`) provides lightweight, CNI-enforced zero-trust tenant isolation with default-deny rules. | -| **Workload Identity (no static secrets)** | Kubernetes `Secret` with cloud credentials | Static credentials require manual rotation, are stored in etcd, and present a wide blast radius on leak. OIDC-projected pod tokens (Azure Workload Identity / AWS IRSA) are short-lived, auto-rotated, and scoped to a single identity. | -| **Go (`controller-runtime`)** | Python (`Kopf`) | Go provides native compile-time safety, seamless alignment with Kubernetes upstream libraries, and access to `setup-envtest` for isolated in-process integration testing. | +| Decision | Alternative Considered | Trade-Off & Rationale for Agentrax | +| ----------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **Gateway API (`HTTPRoute`)** | Istio `VirtualService` / Ingress Annotations | Istio requires a heavy service-mesh control plane and sidecar injection. Ingress annotations lack standardized multi-backend weighted traffic splits. Gateway API provides a lightweight, vendor-neutral standard for traffic shifting. | +| **Custom Canary Rollout Engine** | Argo Rollouts / Flagger | Generic rollout tools treat metric anomalies as pure percentages without low-traffic statistical gating (`minRequestSample`). Building an embedded, re-entrant state machine allowed us to guarantee sample-size gating and MCP tool re-registration upon promotion. | +| **Native HPA via Custom Metrics** | KEDA (`ScaledObject`) | KEDA is powerful but adds external CRD dependencies. Generating native Kubernetes `HorizontalPodAutoscaler` objects tied to the Prometheus Adapter custom metrics pipeline minimized dependencies while giving full control over stabilization windows. | +| **Embedded Registry + ConfigMap Store** | Dedicated etcd / Redis / Database | Adding a dedicated database for service discovery increases operator operational complexity. The in-operator HTTP server with ConfigMap write-through store provides simple, robust storage for hundreds of agent services with cold-restart recovery. | +| **Two-Tier NetworkPolicy** | Istio / Linkerd Service Mesh | Service mesh requires sidecar injection and significant control plane memory overhead. Native Kubernetes NetworkPolicy with label-selector binding (`agentrax.io/agent: "true"`) provides lightweight, CNI-enforced zero-trust tenant isolation with default-deny rules. | +| **Workload Identity (no static secrets)** | Kubernetes `Secret` with cloud credentials | Static credentials require manual rotation, are stored in etcd, and present a wide blast radius on leak. OIDC-projected pod tokens (Azure Workload Identity / AWS IRSA) are short-lived, auto-rotated, and scoped to a single identity. | +| **Terraform Modules for IaC** | Raw shell scripts / `make deploy-deps` | Shell scripts are non-idempotent and hard to parameterise. Terraform modules enforce dependency order, enable plan/apply/destroy lifecycle, support multiple environments (dev/prod), and integrate natively with CI IaC scanning tools (tflint, trivy). | +| **Go (`controller-runtime`)** | Python (`Kopf`) | Go provides native compile-time safety, seamless alignment with Kubernetes upstream libraries, and access to `setup-envtest` for isolated in-process integration testing. | --- diff --git a/infra/.tflint.hcl b/infra/.tflint.hcl new file mode 100644 index 0000000..c48b1a7 --- /dev/null +++ b/infra/.tflint.hcl @@ -0,0 +1,29 @@ +# tflint configuration for the infra/ directory. +# Enables the official Terraform plugin for provider-level schema validation. +# Run: tflint --chdir=infra/environments/dev + +plugin "terraform" { + enabled = true + preset = "recommended" +} + +# Enforce consistent code style. +rule "terraform_naming_convention" { + enabled = true +} + +rule "terraform_required_version" { + enabled = true +} + +rule "terraform_required_providers" { + enabled = true +} + +rule "terraform_documented_variables" { + enabled = true +} + +rule "terraform_documented_outputs" { + enabled = true +} diff --git a/infra/environments/dev/.terraform.lock.hcl b/infra/environments/dev/.terraform.lock.hcl new file mode 100644 index 0000000..5cd5ab7 --- /dev/null +++ b/infra/environments/dev/.terraform.lock.hcl @@ -0,0 +1,56 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/helm" { + version = "2.17.0" + constraints = "~> 2.14" + hashes = [ + "h1:K5FEjxvDnxb1JF1kG1xr8J3pNGxoaR3Z0IBG9Csm/Is=", + "zh:06fb4e9932f0afc1904d2279e6e99353c2ddac0d765305ce90519af410706bd4", + "zh:104eccfc781fc868da3c7fec4385ad14ed183eb985c96331a1a937ac79c2d1a7", + "zh:129345c82359837bb3f0070ce4891ec232697052f7d5ccf61d43d818912cf5f3", + "zh:3956187ec239f4045975b35e8c30741f701aa494c386aaa04ebabffe7749f81c", + "zh:66a9686d92a6b3ec43de3ca3fde60ef3d89fb76259ed3313ca4eb9bb8c13b7dd", + "zh:88644260090aa621e7e8083585c468c8dd5e09a3c01a432fb05da5c4623af940", + "zh:a248f650d174a883b32c5b94f9e725f4057e623b00f171936dcdcc840fad0b3e", + "zh:aa498c1f1ab93be5c8fbf6d48af51dc6ef0f10b2ea88d67bcb9f02d1d80d3930", + "zh:bf01e0f2ec2468c53596e027d376532a2d30feb72b0b5b810334d043109ae32f", + "zh:c46fa84cc8388e5ca87eb575a534ebcf68819c5a5724142998b487cb11246654", + "zh:d0c0f15ffc115c0965cbfe5c81f18c2e114113e7a1e6829f6bfd879ce5744fbb", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "2.38.0" + constraints = "~> 2.31" + hashes = [ + "h1:5CkveFo5ynsLdzKk+Kv+r7+U9rMrNjfZPT3a0N/fhgE=", + "zh:0af928d776eb269b192dc0ea0f8a3f0f5ec117224cd644bdacdc682300f84ba0", + "zh:1be998e67206f7cfc4ffe77c01a09ac91ce725de0abaec9030b22c0a832af44f", + "zh:326803fe5946023687d603f6f1bab24de7af3d426b01d20e51d4e6fbe4e7ec1b", + "zh:4a99ec8d91193af961de1abb1f824be73df07489301d62e6141a656b3ebfff12", + "zh:5136e51765d6a0b9e4dbcc3b38821e9736bd2136cf15e9aac11668f22db117d2", + "zh:63fab47349852d7802fb032e4f2b6a101ee1ce34b62557a9ad0f0f0f5b6ecfdc", + "zh:924fb0257e2d03e03e2bfe9c7b99aa73c195b1f19412ca09960001bee3c50d15", + "zh:b63a0be5e233f8f6727c56bed3b61eb9456ca7a8bb29539fba0837f1badf1396", + "zh:d39861aa21077f1bc899bc53e7233262e530ba8a3a2d737449b100daeb303e4d", + "zh:de0805e10ebe4c83ce3b728a67f6b0f9d18be32b25146aa89116634df5145ad4", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + "zh:faf23e45f0090eef8ba28a8aac7ec5d4fdf11a36c40a8d286304567d71c1e7db", + ] +} + +provider "registry.terraform.io/tehcyx/kind" { + version = "0.11.0" + constraints = "~> 0.6" + hashes = [ + "h1:w1vPgnfTycZq2MXQoS4sJ+URsXMwIv8u2drC9BE8DPA=", + "zh:10cf5f11ed1b24bcc2a64ddfe529dbe240ac72c075100039eb8a182abd5a25d8", + "zh:1c652afcea840545f9e21cf42369560966eafe52986d578c31a35247624442bf", + "zh:8ed94e1387970e7b885c7a68579b17a662d769d04dd3a0917d6c795741d0b97c", + "zh:97e3591b821b8a7cd1d0bc6322c1cbeed882ca26ff357cdcfab8dc0b17279090", + "zh:d5789b07c0a76d086d19acf948246875cb20bea0826a164f4f2c36b8fe527385", + "zh:e5a1117080f6b51e836bf41576e62ee1ea738a5d5fb2f5b0fccb25c942dfb557", + ] +} diff --git a/infra/environments/dev/main.tf b/infra/environments/dev/main.tf new file mode 100644 index 0000000..2ffa6f7 --- /dev/null +++ b/infra/environments/dev/main.tf @@ -0,0 +1,70 @@ +# dev environment — local kind cluster + agentrax stack +# This is the primary target for local development, integration testing, and CI. +# State is stored in a local backend file (terraform.tfstate) — not shared. + +terraform { + required_version = ">= 1.6" + + # Local backend — intentional for dev. Do not check in terraform.tfstate. + backend "local" {} + + required_providers { + kind = { + source = "tehcyx/kind" + version = "~> 0.6" + } + helm = { + source = "hashicorp/helm" + version = "~> 2.14" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.31" + } + } +} + +# --------------------------------------------------------------------------- +# Step 1: Provision the kind cluster +# --------------------------------------------------------------------------- +module "kind_cluster" { + source = "../../modules/kind_cluster" + cluster_name = var.cluster_name +} + +# --------------------------------------------------------------------------- +# Step 2: Configure the Helm and Kubernetes providers to target the new cluster. +# Both providers read credentials from the kind_cluster module outputs so no +# local kubeconfig file needs to exist before `terraform apply`. +# --------------------------------------------------------------------------- +provider "helm" { + kubernetes { + host = module.kind_cluster.endpoint + client_certificate = module.kind_cluster.client_certificate + client_key = module.kind_cluster.client_key + cluster_ca_certificate = module.kind_cluster.cluster_ca_certificate + } +} + +provider "kubernetes" { + host = module.kind_cluster.endpoint + client_certificate = module.kind_cluster.client_certificate + client_key = module.kind_cluster.client_key + cluster_ca_certificate = module.kind_cluster.cluster_ca_certificate +} + +# --------------------------------------------------------------------------- +# Step 3: Install cert-manager → kube-prometheus-stack → agentrax +# --------------------------------------------------------------------------- +module "agentrax_stack" { + source = "../../modules/agentrax_stack" + + cert_manager_version = var.cert_manager_version + prometheus_stack_version = var.prometheus_stack_version + agentrax_chart_path = var.agentrax_chart_path + agentrax_leader_elect = var.agentrax_leader_elect + agentrax_extra_values = var.agentrax_extra_values + + # The stack module requires the cluster to exist first. + # Provider-level dependency is enforced via the shared kubeconfig above. +} diff --git a/infra/environments/dev/outputs.tf b/infra/environments/dev/outputs.tf new file mode 100644 index 0000000..2373ca0 --- /dev/null +++ b/infra/environments/dev/outputs.tf @@ -0,0 +1,27 @@ +# dev environment — outputs + +output "cluster_endpoint" { + description = "Kubernetes API server endpoint for the dev kind cluster." + value = module.kind_cluster.endpoint +} + +output "agentrax_namespace" { + description = "Namespace where the agentrax operator was deployed." + value = module.agentrax_stack.agentrax_namespace +} + +output "agentrax_release_status" { + description = "Helm release status for the agentrax chart." + value = module.agentrax_stack.agentrax_release_status +} + +output "prometheus_namespace" { + description = "Namespace where kube-prometheus-stack is deployed." + value = module.agentrax_stack.prometheus_namespace +} + +output "kubeconfig" { + description = "Raw kubeconfig for the kind cluster. Pipe into kubectl or save to a file." + value = module.kind_cluster.kubeconfig + sensitive = true +} diff --git a/infra/environments/dev/variables.tf b/infra/environments/dev/variables.tf new file mode 100644 index 0000000..09af24b --- /dev/null +++ b/infra/environments/dev/variables.tf @@ -0,0 +1,37 @@ +# dev environment — variables + +variable "cluster_name" { + description = "Name of the local kind cluster." + type = string + default = "agentrax-dev" +} + +variable "cert_manager_version" { + description = "cert-manager Helm chart version." + type = string + default = "v1.15.3" +} + +variable "prometheus_stack_version" { + description = "kube-prometheus-stack Helm chart version." + type = string + default = "61.8.0" +} + +variable "agentrax_chart_path" { + description = "Path to the agentrax Helm chart directory, relative to this environment root." + type = string + default = "../../../charts/agentrax" +} + +variable "agentrax_leader_elect" { + description = "Enable leader election for the Agentrax controller manager." + type = bool + default = false +} + +variable "agentrax_extra_values" { + description = "Additional Helm set key=value overrides for the agentrax release." + type = map(string) + default = {} +} diff --git a/infra/environments/prod/README.md b/infra/environments/prod/README.md new file mode 100644 index 0000000..4fc391f --- /dev/null +++ b/infra/environments/prod/README.md @@ -0,0 +1,19 @@ +# Production Environment — Azure AKS (Stub) + +This environment targets an Azure Kubernetes Service (AKS) cluster for production workloads. The `agentrax_stack` module is cloud-agnostic; the production configuration differs from `dev` in four ways: + +1. **Remote State Backend**: State stored in Azure Blob Storage with state locking. +2. **Cloud Provider Authentication**: Uses `azurerm` / `azapi` providers authenticated via Azure OIDC / Workload Identity. +3. **High Availability**: `agentrax_leader_elect = true` with $\ge 2$ controller replicas. +4. **Workload Identity**: Cloud identity parameters passed via `agentrax_extra_values`. + +### Activation Runbook + +1. Provision the target AKS cluster and retrieve its kubeconfig credentials. +2. Configure `backend.tf` with the Azure Blob Storage container coordinates. +3. Export Azure authentication environment variables (`ARM_CLIENT_ID`, `ARM_TENANT_ID`, `ARM_SUBSCRIPTION_ID`, `ARM_USE_OIDC=true`). +4. Execute deployment: + ```bash + terraform -chdir=infra/environments/prod init + terraform -chdir=infra/environments/prod apply + ``` diff --git a/infra/modules/agentrax_stack/main.tf b/infra/modules/agentrax_stack/main.tf new file mode 100644 index 0000000..fa4e92c --- /dev/null +++ b/infra/modules/agentrax_stack/main.tf @@ -0,0 +1,107 @@ +# kind cluster module — agentrax_stack main.tf +# Installs cert-manager, kube-prometheus-stack, and the agentrax operator +# in strict dependency order via Helm. + +terraform { + required_providers { + helm = { + source = "hashicorp/helm" + version = "~> 2.14" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.31" + } + } + required_version = ">= 1.6" +} + +# --------------------------------------------------------------------------- +# cert-manager — prerequisite for admission webhooks +# --------------------------------------------------------------------------- +resource "helm_release" "cert_manager" { + name = "cert-manager" + repository = "https://charts.jetstack.io" + chart = "cert-manager" + version = var.cert_manager_version + namespace = "cert-manager" + create_namespace = true + + set { + name = "crds.enabled" + value = "true" + } + + # Wait until all cert-manager pods are ready before proceeding. + wait = true + timeout = 300 +} + +# --------------------------------------------------------------------------- +# kube-prometheus-stack — Prometheus + Grafana + Prometheus Adapter +# Required for canary rollout threshold evaluation. +# --------------------------------------------------------------------------- +resource "helm_release" "kube_prometheus_stack" { + name = "kube-prometheus-stack" + repository = "https://prometheus-community.github.io/helm-charts" + chart = "kube-prometheus-stack" + version = var.prometheus_stack_version + namespace = "monitoring" + create_namespace = true + + # Lightweight values for kind — disable heavy storage and alertmanager for local dev. + values = [ + yamlencode({ + grafana = { + enabled = false + } + alertmanager = { + enabled = false + } + prometheus = { + prometheusSpec = { + retention = "2h" + } + } + }) + ] + + wait = true + timeout = 600 + + depends_on = [helm_release.cert_manager] +} + +# --------------------------------------------------------------------------- +# agentrax operator — installed last, after all dependencies are ready +# --------------------------------------------------------------------------- +resource "helm_release" "agentrax" { + name = "agentrax" + chart = var.agentrax_chart_path + namespace = "agentrax-system" + create_namespace = true + + # Wire Prometheus URL so canary rollout threshold evaluation is live. + set { + name = "prometheus.url" + value = "http://kube-prometheus-stack-prometheus.monitoring.svc:9090" + } + + set { + name = "manager.leaderElect" + value = tostring(var.agentrax_leader_elect) + } + + dynamic "set" { + for_each = var.agentrax_extra_values + content { + name = set.key + value = set.value + } + } + + wait = true + timeout = 300 + + depends_on = [helm_release.kube_prometheus_stack] +} diff --git a/infra/modules/agentrax_stack/outputs.tf b/infra/modules/agentrax_stack/outputs.tf new file mode 100644 index 0000000..cc16df8 --- /dev/null +++ b/infra/modules/agentrax_stack/outputs.tf @@ -0,0 +1,16 @@ +# agentrax_stack module — outputs + +output "agentrax_namespace" { + description = "Kubernetes namespace where the agentrax operator is deployed." + value = helm_release.agentrax.namespace +} + +output "agentrax_release_status" { + description = "Helm release status for the agentrax chart." + value = helm_release.agentrax.status +} + +output "prometheus_namespace" { + description = "Namespace where kube-prometheus-stack is deployed." + value = helm_release.kube_prometheus_stack.namespace +} diff --git a/infra/modules/agentrax_stack/variables.tf b/infra/modules/agentrax_stack/variables.tf new file mode 100644 index 0000000..f048610 --- /dev/null +++ b/infra/modules/agentrax_stack/variables.tf @@ -0,0 +1,32 @@ +# agentrax_stack module — variables + +variable "cert_manager_version" { + description = "Helm chart version for cert-manager (https://charts.jetstack.io)." + type = string + default = "v1.15.3" +} + +variable "prometheus_stack_version" { + description = "Helm chart version for kube-prometheus-stack." + type = string + default = "61.8.0" +} + +variable "agentrax_chart_path" { + description = "Local path or OCI URL to the agentrax Helm chart." + type = string + # Default points to the chart in-tree relative to the Terraform working directory. + default = "../../../charts/agentrax" +} + +variable "agentrax_leader_elect" { + description = "Enable Kubernetes leader election for the controller manager. Set true for HA (multiple replicas)." + type = bool + default = false +} + +variable "agentrax_extra_values" { + description = "Additional key=value Helm set overrides for the agentrax chart (e.g. image.tag, workloadIdentity.*)." + type = map(string) + default = {} +} diff --git a/infra/modules/kind_cluster/main.tf b/infra/modules/kind_cluster/main.tf new file mode 100644 index 0000000..0a9b89a --- /dev/null +++ b/infra/modules/kind_cluster/main.tf @@ -0,0 +1,39 @@ +# kind cluster module — provisions a local kind cluster using the tehcyx/kind provider. +# This module is intentionally thin: it creates the cluster and exposes the kubeconfig +# so the agentrax_stack module can install the Helm stack into it. + +terraform { + required_providers { + kind = { + source = "tehcyx/kind" + version = "~> 0.6" + } + } + required_version = ">= 1.6" +} + +resource "kind_cluster" "this" { + name = var.cluster_name + wait_for_ready = true + + kind_config { + kind = "Cluster" + api_version = "kind.x-k8s.io/v1alpha4" + + node { + role = "control-plane" + + # Expose ports for the Agentrax discovery registry and Gateway API. + extra_port_mappings { + container_port = 9090 + host_port = 9090 + protocol = "TCP" + } + } + + # Additional worker node gives the scheduler headroom for agent pods. + node { + role = "worker" + } + } +} diff --git a/infra/modules/kind_cluster/outputs.tf b/infra/modules/kind_cluster/outputs.tf new file mode 100644 index 0000000..6666db9 --- /dev/null +++ b/infra/modules/kind_cluster/outputs.tf @@ -0,0 +1,30 @@ +# kind_cluster module — outputs + +output "kubeconfig" { + description = "Raw kubeconfig string for the provisioned kind cluster." + value = kind_cluster.this.kubeconfig + sensitive = true +} + +output "client_certificate" { + description = "PEM-encoded client certificate for Kubernetes provider auth." + value = kind_cluster.this.client_certificate + sensitive = true +} + +output "client_key" { + description = "PEM-encoded client key for Kubernetes provider auth." + value = kind_cluster.this.client_key + sensitive = true +} + +output "cluster_ca_certificate" { + description = "PEM-encoded cluster CA certificate for Kubernetes provider auth." + value = kind_cluster.this.cluster_ca_certificate + sensitive = true +} + +output "endpoint" { + description = "Kubernetes API server endpoint for the kind cluster." + value = kind_cluster.this.endpoint +} diff --git a/infra/modules/kind_cluster/variables.tf b/infra/modules/kind_cluster/variables.tf new file mode 100644 index 0000000..e0565dc --- /dev/null +++ b/infra/modules/kind_cluster/variables.tf @@ -0,0 +1,7 @@ +# kind_cluster module — variables + +variable "cluster_name" { + description = "Name of the kind cluster. Must be unique on the host." + type = string + default = "agentrax-dev" +}