agentskills.codes
MO

module-conventions

Binding rules for every module in rad-modules — TF file layout, variables.tf structure with UIMeta, provider-auth impersonation, and common deployment-ID / project / trusted-users patterns.

Install

mkdir -p .claude/skills/module-conventions && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/13886" && unzip -o skill.zip -d .claude/skills/module-conventions && rm skill.zip

Installs to .claude/skills/module-conventions

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Binding rules for every module in rad-modules — TF file layout, variables.tf structure with UIMeta, provider-auth impersonation, and common deployment-ID / project / trusted-users patterns.
189 charsno explicit “when” trigger

About this skill

Module Conventions

Every module under modules/ is an independent OpenTofu root module and shares the same structural conventions. Deviating from them breaks either rad-launcher variable validation or the RAD UI rendering. Treat these rules as load-bearing.

Directory Layout

A module directory looks like this (Bank_GKE shown as the canonical multi-file example; AKS_GKE and EKS_GKE are simpler):

modules/<Module_Name>/
├── README.md              # Short summary + Usage + Requirements/Providers/Resources/Inputs/Outputs tables
├── <MODULE_NAME>.md       # Long-form educational deep dive
├── main.tf                # Locals, random_id, data.google_project, google_project_service.enabled_services
├── variables.tf           # All inputs, annotated with UIMeta tags (see below)
├── versions.tf            # OR provider.tf — required_providers + required_version
├── provider-auth.tf       # OR provider.tf — google / azurerm / aws provider config
├── network.tf             # VPC / subnet / firewall / NAT
├── <feature>.tf           # e.g. gke.tf, asm.tf, hub.tf, deploy.tf, glb.tf, mcs.tf, istiosidecar.tf
├── outputs.tf             # deployment_id + project_id at minimum
├── manifests/             # or templates/ — static or templated Kubernetes YAML
└── modules/               # optional, nested module-local helpers (not cross-module)
    └── <helper>/
        ├── main.tf
        ├── variables.tf
        └── ...

Rules:

  • No symlinks. Modules do not share TF files. If Bank_GKE and MC_Bank_GKE need similar asm.tf, each has its own copy.
  • Nested modules (e.g. modules/AKS_GKE/modules/attached-install-manifest/) are scoped to one parent module only; they must not be referenced from other modules in the repo.
  • Kubernetes templates live under manifests/ (raw YAML) or templates/ (Go-template .yaml.tpl rendered by templatefile(...)). Pick one per module based on whether any values are substituted.
  • License header: every .tf file begins with the Apache 2.0 block-comment header. Copy it from a neighbouring file when creating a new one.
  • Naming: files are lowercase with hyphens (provider-auth.tf), module directory names are PascalCase_WithUnderscores, HCL resource names are snake_case.

variables.tf Structure

Variables are organized into numbered sections using // SECTION N: or # SECTION N: comments. The ordering below is the established convention:

# SECTION 1: Deployment   → module_description, module_dependency, module_services,
#                           credit_cost, require_credit_purchases, enable_purge,
#                           public_access, deployment_id, resource_creator_identity,
#                           trusted_users, enable_services
# SECTION 2: Project      → project_id
# SECTION 3: Network      → create_network, network_name, subnet_name, ip_cidr_ranges, ...
# SECTION 4: Cluster      → create_cluster, cluster_name_prefix, k8s_version, release_channel, ...
# SECTION 5: IAM / Creds  → client_id/tenant_id/subscription_id/client_secret (Azure),
#                           aws_access_key/aws_secret_key (AWS)
# SECTION 6+: Feature-specific (e.g. service mesh, config management, application)

enable_services belongs in group 0 (SECTION 1: Deployment). Place it at the end of the Deployment section (order=109) so the API-enabling toggle is grouped with other platform-level deployment controls rather than with project-specific inputs. Use {{UIMeta group=0 order=109 }}.

Not every module needs every section — AKS_GKE has no dedicated network section because AKS manages its own VNet, and Istio_GKE merges IAM into cluster setup. The numbering should still follow this order wherever the section is present.

Every Module Ships These Ten Standard Variables

The variables below exist in every module and must keep their exact names, types, and defaults. rad-launcher looks for them; the RAD UI renders them in a standard panel.

VariableTypeDefaultNotes
module_descriptionstringmodule-specific textShown in catalog
module_dependencylist(string)e.g. ["GCP Project"]Deploy order
module_serviceslist(string)e.g. ["GCP","GKE",...]UI tags
credit_costnumber100 or 200Platform credits
require_credit_purchasesboolfalse
enable_purgebooltrue
public_accessbooltrueCatalog visibility
deployment_idstringnull4-char suffix; null ⇒ auto
resource_creator_identitystring"[email protected]"Impersonated SA
trusted_userslist(string)[]Cluster-admin emails

trusted_users should carry the duplicate-and-whitespace validations from AKS_GKE/variables.tf; copy them when adding to a new module.

UIMeta Tags

Every variable description ends with a {{UIMeta ...}} tag (inside the description string, not a comment) that drives UI rendering:

variable "gcp_region" {
  description = "GCP region where the GKE cluster ... Defaults to 'us-central1'. {{UIMeta group=2 order=302 updatesafe }}"
  type        = string
  default     = "us-central1"
}

Parameters:

  • group=N — UI panel grouping, corresponding loosely to SECTION (0=Deployment, 1=Project, 2=Network, etc.).
  • order=NNN — sort order within the group. Gaps are fine; leave room to insert new variables.
  • updatesafepresence flag, not a key=value. Include it for variables that can change in place without recreating the module (e.g. trusted_users, resource_creator_identity, region-pinned lookups). Omit it for variables that force replacement (e.g. cluster names, network CIDRs).

Sensitive credentials (client_secret, aws_secret_key, etc.) must also set sensitive = true on the variable itself — the UIMeta tag alone does not mark them secret.

Description Copy Style

Variable descriptions are one flowing paragraph and follow this shape:

[What it is / effect] [Format or example] [Default] [Consequences of change]. {{UIMeta ... }}

Example: "Kubernetes version to deploy on the AKS cluster, specified as major.minor (e.g. '1.34'). Must be a version currently supported by AKS in the selected azure_region. The patch version is managed automatically by AKS. Defaults to '1.34'. {{UIMeta group=4 order=403 updatesafe }}"

Keep this style when editing — the RAD UI shows the description verbatim in tooltips.

Provider Authentication

Two patterns exist; pick based on whether the module touches Google APIs that must run as the impersonated service account.

Pattern A — Direct provider (used by AKS_GKE, EKS_GKE)

Single provider.tf with all required providers and a direct provider "google" block. No impersonation — authentication comes from the caller's Application Default Credentials / Cloud Build service account.

# provider.tf
terraform {
  required_providers {
    google  = { source = "hashicorp/google",  version = ">=5.0.0" }
    azurerm = { source = "hashicorp/azurerm", version = ">=3.17.0" }
    helm    = { source = "hashicorp/helm",    version = "~> 2.0" }
    random  = { source = "hashicorp/random",  version = "3.6.2" }
  }
  required_version = ">= 0.13"
}

provider "google" { project = var.project_id }
provider "azurerm" {
  features {}
  tenant_id = var.tenant_id
  client_id = var.client_id
  client_secret = var.client_secret
  subscription_id = var.subscription_id
}

Pattern B — Impersonated provider (used by Bank_GKE, MC_Bank_GKE, Istio_GKE)

Split versions.tf (provider requirements only) + provider-auth.tf (runtime auth via service-account impersonation). This is required when the module provisions GCP resources that require a specific owner.

# provider-auth.tf — impersonation pattern, copy verbatim
provider "google" {
  alias = "impersonated"
  scopes = [
    "https://www.googleapis.com/auth/cloud-platform",
    "https://www.googleapis.com/auth/userinfo.email",
  ]
}

data "google_service_account_access_token" "default" {
  count                  = length(var.resource_creator_identity) != 0 ? 1 : 0
  provider               = google.impersonated
  scopes                 = ["userinfo-email", "cloud-platform"]
  target_service_account = var.resource_creator_identity
  lifetime               = "3600s"  # Bank_GKE and MC_Bank_GKE; Istio_GKE uses "1800s"
}

provider "google" {
  access_token = length(var.resource_creator_identity) != 0 ? data.google_service_account_access_token.default[0].access_token : null
}

provider "google-beta" {
  access_token = length(var.resource_creator_identity) != 0 ? data.google_service_account_access_token.default[0].access_token : null
}

If a new module needs google-beta, it must use Pattern B so the beta provider also gets the impersonated token.

main.tf Boilerplate

Every module's main.tf starts with this scaffold. The exact shape varies (AKS_GKE uses an unconditional random_id, Bank_GKE makes it conditional), but the ingredients are identical:

locals {
  random_id      = var.deployment_id != null ? var.deployment_id : random_id.default[0].hex
  project        = try(data.google_project.existing_project, null)
  project_number = try(local.project.number, null)

  default_apis     = [ /* module-specific list */ ]
  project_services = var.enable_services ? local.default_apis : []
}

resource "random_id" "default" {
  count       = var.deployment_id == null ? 1 : 0
  byte_length = 2
}

data "google_project" "existing_project" {
  project_id = trimspace(var.project_id)
}

resource "google_project_service" "enabled_services" {
  for_each                   = toset(local.project_services)
  project                    = local.project.project_id
  service                    = each.value
  disable_dependent_services = false   # do NOT flip to true — breaks other modules
  disable_on_destroy         = false   # do N

---

*Content truncated.*

Search skills

Search the agent skills registry