# About Me Source: https://kubestarterkit.com/about-me The person behind Kube Starter Kit # About Me I'm **Sid Palas**, the creator of Kube Starter Kit and founder of [DevOps Directive](https://devopsdirective.com/about/). I've helped engineering teams at companies across industries build and scale their infrastructure. Kube Starter Kit distills the knowledge and patterns I've gathered over the past 6 years into a production-ready starting point for your Kubernetes platform. ## Companies I've Worked With Healthcare data platform DeFi risk management Satellite imagery analytics Aerospace data platform ## DevOps Training I also create world-class DevOps training content to help engineers level up their infrastructure skills. Free video tutorials and deep dives Structured, in-depth learning paths # Architecture Decisions Source: https://kubestarterkit.com/architecture-decisions/index Why I chose this technology stack This page summarizes the key architectural decisions behind Kube Starter Kit. Each feature page includes detailed rationale in its "Key Design Decisions" section, this is the high-level overview of the philosophy and cross-cutting choices. ## Guiding Principles **Opinionated but escapable:** I've made choices so you don't have to, but nothing locks you in. Every component can be swapped out as your needs evolve. **Production-ready from day one:** This isn't a learning exercise or demo. It's infrastructure I'd run in production, with proper security, observability, and operational patterns. **Balanced approach to managed services:** Kubernetes-native solutions where they add flexibility (ingress, observability, GitOps), managed services where the operational burden justifies it (object storage, databases, identity). The goal is portability without reinventing the wheel. **GitOps everything:** Infrastructure, configuration, and application deployments all flow through Git. One source of truth, full audit trail. ## Technology Choices | Category | Choice | Alternatives Considered | | --------------------------- | -------------------------------------- | --------------------------------------------- | | **Cloud Provider** | AWS | GCP, Azure | | **Infrastructure as Code** | Terraform + Terramate | Pulumi, CDK, CloudFormation, Terragrunt | | **Terraform Orchestration** | Terramate | Digger, Atlantis, Spacelift, Terraform Cloud | | **Kubernetes** | EKS | Self-managed, GKE, AKS | | **Node Provisioning** | Karpenter | Cluster Autoscaler | | **GitOps** | ArgoCD | Flux, Kluctl | | **Ingress** | Traefik | AWS ALB Ingress, ingress-nginx, Envoy Gateway | | **Secrets** | External Secrets + AWS Secrets Manager | Sealed Secrets, Vault | | **Observability** | SigNoz | Datadog, Grafana Stack | | **Database** | PlanetScale | RDS, CloudNativePG | | **Local Development** | KinD + Tilt + mirrord | Docker Compose, Skaffold, Telepresence | ## Why These Choices? ### Cloud: AWS AWS has the largest market share and the best ecosystem of supporting services (S3, RDS, SQS, etc.). Most third-party infrastructure tooling runs on AWS, so co-locating your workloads enables reduced latency and the potential for private networking (more secure and more affordable). Most engineers have AWS experience. For early-stage companies, AWS is the safe choice. ### Infrastructure as Code: Terraform Terraform has the largest community, most modules, and best tooling ecosystem. Pulumi and CDK are interesting but add complexity and have smaller ecosystems. ### Orchestration: Terramate Terramate provides a unified solution for Terraform orchestration: stack management, code generation, outputs sharing, and CI/CD integration with Terramate Cloud. Unlike Terragrunt, it uses native HCL syntax without a wrapper. Unlike Digger or Atlantis, it includes change detection and cross-stack dependency management. Terramate Cloud provides visibility into previews, deployments, and drift, without requiring external services to access your AWS credentials. ### GitOps: ArgoCD ArgoCD, Flux, and Kluctl are all solid choices. ArgoCD has the best UI for understanding deployment state, which helps teams new to GitOps. The app-of-apps pattern provides clear hierarchical organization. Flux is lighter-weight but less visual. Kluctl offers a more imperative approach with better diffing and templating flexibility, worth considering if you prefer CLI-driven workflows. ### Ingress: Traefik Traefik is one of the most popular ingress solutions (3B+ downloads, 57K GitHub stars). It supports both traditional Ingress and Gateway API, letting you start simple and adopt advanced routing as needs evolve. Single binary, automatic discovery, sensible defaults, built-in dashboard. Initially I used ingress-nginx as the default ingress controller for the kit, but it is being retired in 2026, with no updates/maintenance. ### Secrets: External Secrets Secrets belong in a secrets manager, not encrypted in Git. External Secrets keeps secrets in AWS Secrets Manager where they can be rotated, audited, and access-controlled properly. Vault is powerful but adds significant operational complexity (self-hosted) or cost (HCP Vault). For most teams, AWS Secrets Manager with External Secrets hits the sweet spot. ### Observability: SigNoz For early-stage companies, Datadog's per-host pricing gets expensive fast. SigNoz gives you metrics, logs, and traces with clear and affordable pricing with the option to self-host down the line as needed. ### Local Development: KinD + Tilt + mirrord KinD provides a real multi-node Kubernetes cluster locally. Tilt handles continuous rebuilds and deploys with smart dependency ordering. mirrord enables debugging local code against remote cluster traffic without deploying. Together they cover the full development lifecycle: local iteration, integration testing, and production debugging. ## What's Not Included (Yet) Some things are intentionally omitted from the initial kit: * **Service mesh:** Most teams don't need Istio/Linkerd on day one. Add it when you have specific requirements. * **Multi-region:** Single region is simpler to operate. The patterns here extend to multi-region when you need it. * **Advanced networking:** No VPC peering, Transit Gateway, or PrivateLink beyond basics. Add as needed. These are candidates for potential extensions as the kit evolves. # Terraform for Base Infrastructure Source: https://kubestarterkit.com/features/01-terraform Modular, well-structured Terraform for AWS with isolated state and battle-tested modules ## The Problem Infrastructure as Code is table stakes for any serious engineering team, but getting it *right* is surprisingly hard: * **Sprawling, copy-pasted modules:** Teams often start with one environment and end up with duplicated Terraform code everywhere, each copy drifting slightly from the others. * **State management headaches:** Where do you store state? How do you handle locking? How do you structure state files so changes in one area don't require touching unrelated infrastructure? * **No clear patterns for multi-environment:** Development, staging, production... how do you manage the differences without maintaining three separate codebases? * **Reinventing the wheel:** Writing VPC, EKS, and IAM configurations from scratch means debugging problems that others have already solved. ## How Kube Starter Kit Addresses This ### Isolated State Per Stack Each deployable unit (networking, EKS cluster, app-resources) has its own state file. This means you can update your EKS cluster without Terraform needing to refresh your entire VPC state. A bad apply in one area doesn't risk corrupting unrelated infrastructure. ### Battle-Tested Community Modules Rather than maintaining VPC and EKS code from scratch, I build on top of well-maintained modules from the [Terraform AWS Modules](https://github.com/terraform-aws-modules) project. These are used by thousands of teams and handle edge cases you'd otherwise discover the hard way. The kit adds the glue and opinions that make them work together. ### Hierarchical Configuration Settings cascade from root to environment to region to stack. Common values like your namespace prefix, provider versions, and IAM roles are defined once and inherited everywhere. Each environment only defines what's unique: region, sizing, feature flags. ### Application Resources Pattern Per-application AWS resources (IAM roles for Pod Identity, Secrets Manager entries, database credentials) are provisioned alongside infrastructure but in separate stacks. This keeps application concerns isolated while maintaining the same workflow for all Terraform changes. ## What's Provisioned One-time setup for new AWS accounts: * S3 bucket for Terraform state with native locking (no DynamoDB needed) * GitHub OIDC provider for keyless CI/CD authentication * IAM roles for Terraform automation Production-ready VPC with: * Public and private subnets across 3 availability zones * Configurable NAT gateway options (single, per-AZ, or fck-nat for cost savings) * Proper subnet tagging for Karpenter node discovery * VPC endpoint support for private AWS service access Fully-configured EKS cluster with: * Managed node group for baseline capacity (runs Karpenter itself) * Essential add-ons pre-configured (CoreDNS, VPC CNI, EBS CSI driver, Pod Identity) * IAM integration via AWS SSO for cluster access * Security group rules for proper inter-node communication Per-application resources provisioned via dedicated stacks: * IAM roles for Pod Identity (AWS access from Kubernetes pods) * Secrets Manager entries for application secrets * Database credentials and connection strings * Any other AWS or third-party resources specific to an application These resources are referenced by Kubernetes deployments via External Secrets. GitHub and AWS IAM Identity Center from a single source of truth: * GitHub organization membership and team assignments * AWS SSO users and group memberships * Permission sets mapped to AWS accounts See [User Management](/features/04-user-management) for details. ## Directory Structure ``` terraform/ ├── bootstrap/ # One-time account setup (state bucket, OIDC) ├── modules/ # Reusable Terraform modules │ ├── eks/ │ ├── networking/ │ └── app-resources/ └── live/ # Stack definitions by environment ├── shared/ # Cross-account resources │ ├── global/ # IAM Identity Center, GitHub org management │ └── {region}/ # ECR repositories └── {stage}/ # staging, prod, etc. ├── global/ # Account-level resources (bootstrapping, DNS) └── {region}/ # us-east-1, us-east-2, etc. ├── networking/ ├── eks/ └── app-resources/ ``` ## Key Design Decisions | Decision | Rationale | | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Terraform over Pulumi/CDK** | Declarative HCL is easier to review and reason about than imperative code. Larger ecosystem of modules and community support. | | **Terraform over Crossplane** | Simpler operational model with no controller running in-cluster to manage. Better tooling for plan/preview workflows. Crossplane shines for self-service platforms, but adds complexity for smaller teams. | | **One state file per stack** | Blast radius containment. A networking change shouldn't require EKS state refresh. Failures are isolated. | | **Community modules as building blocks** | The terraform-aws-modules are battle-tested by thousands of users. Layer opinions on top rather than maintaining infrastructure code from scratch. | | **S3 native locking** | Simpler than DynamoDB locking, fewer moving parts (requires Terraform 1.10+ or OpenTofu 1.8+). | | **Hierarchical config over copy-paste** | Define common values once, override only what differs per environment. Changes propagate automatically. | For orchestration across stacks (change detection, dependency ordering, CI/CD integration), see [Terraform Orchestration with Terramate](/features/02-terramate). For making changes to infrastructure, see [Making Terraform Changes](/usage/operations/01-updating-terraform-infrastructure). # Terraform Orchestration with Terramate Source: https://kubestarterkit.com/features/02-terramate Change detection, dependency ordering, and CI/CD integration for multi-stack Terraform ## The Problem Running Terraform locally works fine when you're a team of one, but it quickly becomes a liability as you grow: * **"Who ran that apply?":** Without centralized execution, it's hard to track who changed what and when. Your state file says it changed, but good luck figuring out the context. * **No visibility before merge:** You want to review infrastructure changes before they happen, but `terraform plan` output buried in a CI log isn't exactly reviewer-friendly. * **Credential sprawl:** Every developer with Terraform access needs AWS credentials. That's a lot of long-lived secrets floating around laptops. * **Cross-stack dependencies are painful:** `terraform_remote_state` data sources are clunky and require knowing state bucket details everywhere. * **Running all stacks is slow:** Without change detection, every PR plans every stack, even unchanged ones. ## How Kube Starter Kit Addresses This I've integrated [Terramate](https://terramate.io/) to orchestrate Terraform across environments. Here's what that gives you: ### Change Detection Terramate detects which stacks are affected by your changes. On a PR, only modified stacks get planned, not your entire infrastructure. This makes CI faster, reduces noise, and lowers costs. ### Dependency Ordering Stacks declare dependencies and Terramate runs them in the correct order. Networking before EKS, EKS before app-resources. No manual coordination required. ### Outputs Sharing Stacks can consume outputs from other stacks without `terraform_remote_state`. Define an output in one stack, consume it as a variable in another. Terramate handles the wiring. Dependencies are explicit, type-safe, and support mocks for bootstrapping. ### Code Generation Common patterns (backend config, provider setup, module invocations) are generated from templates called "mixins." Change a mixin once, regenerate everywhere. This eliminates copy-paste drift between stacks. ### Terramate Cloud Previews, deployments, and drift detection sync to a dashboard. See the state of your infrastructure across all stacks in one place: who requested changes, who approved them, what actually changed. ### Keyless Authentication GitHub OIDC assumes an AWS role. No long-lived credentials stored in GitHub secrets or on developer laptops. The trust is based on GitHub's identity, not shared secrets. ## The Workflow Make changes to Terraform code: modules, stacks, or configuration. Terramate detects affected stacks and runs `terraform plan` for each. Results sync to Terramate Cloud and appear in the PR checks. Reviewers see exactly what infrastructure changes will happen. Check the Terramate Cloud dashboard for a unified view across stacks. Merge the PR to main when approved. The deploy workflow applies changed stacks in dependency order. Results sync to Terramate Cloud. Every change is tied to a PR and tracked in Terramate Cloud: who requested it, who approved it, what changed. ## CI/CD Workflows Four GitHub Actions workflows handle different scenarios: | Workflow | Trigger | Purpose | | ------------------- | ----------------- | ------------------------------------------------------- | | **Preview** | Pull request | Plans changed stacks, syncs previews to Terramate Cloud | | **Deploy** | Merge to main | Applies changed stacks in dependency order | | **Drift Detection** | Schedule/manual | Detects when infrastructure has drifted from state | | **Provider Cache** | Lock file changes | Pre-downloads providers to speed up other workflows | ## Key Design Decisions | Decision | Rationale | | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | **Terramate over Digger/Atlantis** | Terramate provides stack orchestration, code generation, and outputs sharing, not just CI/CD. The unified platform reduces tooling complexity. | | **Apply on merge (not before)** | Simpler workflow: merge triggers apply. Terramate Cloud provides visibility if rollback is needed. | | **Outputs sharing over remote\_state** | No need to pass bucket names everywhere. Dependencies are explicit and type-safe. Mocks enable planning before dependencies exist. | | **OIDC over static credentials** | No secrets to rotate. Short-lived tokens exchanged at runtime. | | **Change detection by default** | Only plan/apply what changed. Faster CI, less noise, lower costs. | For the infrastructure that Terramate orchestrates, see [Terraform for Base Infrastructure](/features/01-terraform). For step-by-step instructions on making changes, see [Making Terraform Changes](/usage/operations/01-updating-terraform-infrastructure). # AWS Architecture Source: https://kubestarterkit.com/features/03-aws-architecture Multi-account setup with VPC, EKS, and proper IAM boundaries ## The Problem AWS gives you a blank canvas, which is both a blessing and a curse: * **Single-account sprawl:** Many teams start with one AWS account and end up with staging and production resources tangled together. One bad IAM policy or accidental deletion affects everything. * **Network design paralysis:** How many AZs? Public subnets, private subnets, or both? NAT Gateway (expensive) or something else? These decisions are hard to change later. * **EKS configuration complexity:** Getting EKS right involves dozens of settings: add-ons, IAM roles for service accounts, node groups vs Karpenter, security groups, access management... * **Cost surprises:** NAT Gateway charges, idle node capacity, and over-provisioned resources add up fast. By the time you notice, you've burned through budget. ## How Kube Starter Kit Addresses This I've designed the AWS architecture in this kit based on patterns that balance security, cost, and operational simplicity: **Multi-account by default:** Staging and production live in separate AWS accounts. This provides hard isolation: a mistake in staging can't affect production resources, IAM policies are naturally scoped, and billing is automatically separated. **VPC with flexibility:** Each environment gets a VPC spanning 3 availability zones with public and private subnets. The NAT configuration is pluggable: use AWS NAT Gateway for production reliability, or [fck-nat](https://fck-nat.dev/) for non-production cost savings. **EKS with sensible defaults:** The cluster comes pre-configured with essential add-ons (CoreDNS, VPC CNI, EBS CSI driver), proper IAM integration via AWS SSO, and a base node group sized for running Karpenter and critical workloads. **Karpenter for right-sized compute:** Instead of managing multiple node groups for different workload types, Karpenter provisions exactly the nodes you need, when you need them. Less waste, less management. ## What's Included ### Account Structure ``` AWS Organization ├── Management Account │ ├── AWS Organizations │ ├── IAM Identity Center (Manages user access across all accounts) │ └── Consolidated Billing ├── Infrastructure Account │ ├── GitHub OIDC Provider │ ├── Terraform State Bucket │ └── IAM Roles for Terraform Automation (Manages all account infrastructure) ├── ECR Account │ └── ECR Repositories (Consumed by staging + production) ├── Staging Account │ ├── VPC │ ├── EKS Cluster │ ├── Secrets Manager │ ├── Route 53 Hosted Zone │ └── Application-specific Resources (IAM roles, S3, etc.) └── Production Account ├── VPC ├── EKS Cluster ├── Secrets Manager ├── Route 53 Hosted Zone └── Application-specific Resources (IAM roles, S3, etc.) ``` AWS Architecture Diagram ### VPC Architecture Each environment VPC includes: | Component | Configuration | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | **Availability Zones** | 3 AZs for high availability | | **Public Subnets** | For load balancers and NAT | | **Private Subnets** | For EKS nodes and workloads | | **NAT** | Configurable: [AWS NAT Gateway](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html) or [fck-nat](https://fck-nat.dev/) | | **Subnet Tagging** | Pre-configured for Karpenter discovery | | **Bastion Host** | Optional EC2 instance for private EKS API access via SSM | ### EKS Cluster Configuration Pre-configured and version-pinned: * **CoreDNS:** Cluster DNS * **VPC CNI:** Pod networking with native AWS IPs * **kube-proxy:** Service networking * **EBS CSI Driver:** Persistent volume support * **Pod Identity Agent:** Modern pod-level IAM * **Base node group:** 2-3 nodes running Karpenter and critical infrastructure * **Karpenter:** Provisions workload nodes on-demand with right-sized instances * **ARM64 support:** Graviton instances for cost savings where compatible * **AWS SSO integration:** Cluster access via IAM Identity Center * **IRSA enabled:** Pods can assume IAM roles without node-level credentials * **Pod Identity:** Newer, simpler alternative to IRSA for pod-level AWS access ### Cost Optimization The kit includes several cost-conscious choices: * **fck-nat for non-production:** NAT Gateway costs `~$32/month` per AZ just to exist PLUS `$0.045/GB` processed. fck-nat runs on a `t4g.nano (~$3/month)` and handles typical dev/staging traffic fine. * **Karpenter consolidation:** Automatically bins packs workloads and removes underutilized nodes. * **Spot instances:** Karpenter can provision spot instances for fault-tolerant workloads. * **Right-sized base nodes:** The base node group uses smaller instances since it only runs infrastructure components. ## Key Design Decisions | Decision | Rationale | | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | **Multi-account over single account** | Hard isolation between environments. Can't accidentally affect production from staging. Cleaner IAM boundaries. | | **3 AZs** | Balance between availability and cost. 2 AZs risk losing half capacity; more than 3 adds cost without proportional benefit. | | **Private subnets for nodes** | Nodes don't need public IPs. Reduces attack surface and simplifies security groups. | | **Karpenter over Cluster Autoscaler** | Faster scaling, better bin-packing, supports mixed instance types without managing multiple node groups. | | **EKS managed add-ons** | AWS handles upgrades and compatibility. Less operational burden than self-managed. | # GitHub + AWS User Management Source: https://kubestarterkit.com/features/04-user-management Unified identity management across GitHub and AWS from a single source of truth ## The Problem User management across multiple systems is a constant source of friction and security risk: * **Multiple sources of truth:** GitHub org membership in one place, AWS IAM users in another, SSO groups somewhere else. When someone joins or leaves, you're updating multiple systems. * **Permission drift:** Over time, permissions accumulate. Temporary access grants become permanent without regular audits. * **Audit complexity:** Answering "who has access to production?" requires checking multiple systems and correlating information manually. * **Onboarding/offboarding overhead:** New hires need accounts created in multiple places. Departures require remembering all the places to revoke access. ## How Kube Starter Kit Addresses This I've built a unified user management system where a single YAML file drives both GitHub and AWS access: **Single source of truth:** One `users.yaml` file defines who has access to what. GitHub org membership, team assignments, AWS SSO users, and group memberships all flow from this one file. **Declarative and auditable:** Because it's in version control, you have a complete history of access changes. Who added that user? Check the git log. What permissions did they have last month? Check the commit history. **Consistent group model:** The same logical groups (Admin, ReadOnly, etc.) map to both GitHub teams and AWS SSO groups by default. This keeps things simple initially, but the model is extensible to any set of permissions across platforms as your needs evolve. **PR-based access changes:** Adding a user or changing permissions is a pull request. It gets reviewed, approved, and automatically applied via Terraform. ## What's Included ### User Definition Format Users are defined in a single YAML file with both GitHub and AWS configuration: ```yaml theme={null} users: - github: username: jsmith role: member # GitHub org role: admin or member teams: Admin: role: maintainer # GitHub team role: maintainer or member aws: user_name: jane.smith email: jane@company.com group_membership: [Admin] given_name: Jane family_name: Smith ``` ### GitHub Resources The Terraform configuration manages: | Resource | Description | | -------------------------- | --------------------------------------------- | | **Org membership** | Adds users to your GitHub organization | | **Teams** | Creates teams (Admin, ReadOnly, etc.) | | **Team membership** | Assigns users to teams with appropriate roles | | **Repository permissions** | Grants teams access to the repository | ### AWS IAM Identity Center The configuration creates: Creates users in IAM Identity Center with email, name, and group assignments. Pre-configured groups that map to permission levels: * **Admin:** Full administrative access * **PowerUser:** Developer access without IAM management * **ReadOnly:** View-only access for auditing AWS-managed policies attached to groups: * **AdministratorAccess:** Full AWS access * **PowerUserAccess:** Full access except IAM * **ViewOnlyAccess:** Read-only across services Groups are assigned to permission sets across all accounts (staging, production, shared, etc.). ### Workflow Add a new user or modify permissions in the YAML file. Terramate detects the changed stacks and runs `terraform plan`, showing exactly what will change. Team members review the access change. Clear diff of who's getting what. Changes are applied to both GitHub and AWS automatically. ### Example: Adding a New Team Member ```yaml theme={null} # Add to users.yaml - github: username: newdev role: member teams: Admin: role: member aws: user_name: new.developer email: new@company.com group_membership: [Admin] given_name: New family_name: Developer ``` This single addition: * Invites `newdev` to your GitHub organization * Adds them to the Admin team with member permissions * Creates an AWS SSO user * Adds them to the Admin group with AdministratorAccess across all accounts ## Key Design Decisions | Decision | Rationale | | -------------------------------------- | ----------------------------------------------------------------------------------------------- | | **YAML over UI** | Version control, audit history, and code review for access changes. | | **IAM Identity Center over IAM users** | Centralized identity, no long-lived credentials, integrates with external IdPs if needed later. | | **Matching group names** | Cognitive simplicity: "Admin" means the same thing in GitHub and AWS. | | **PR-based workflow** | Access changes get the same review process as code changes. No shadow IT. | # GitOps with ArgoCD Source: https://kubestarterkit.com/features/05-gitops Declarative, auditable Kubernetes deployments driven by Git ## The Problem Deploying to Kubernetes without GitOps often means: * **Imperative chaos:** `kubectl apply` from laptops, scripts that "usually work," and no clear record of what's actually running. * **Drift:** Someone makes a quick fix directly in the cluster. Now the running state doesn't match any source of truth. * **No audit trail:** "Who deployed that change?" requires digging through CI logs, Slack messages, and hoping someone remembers. * **Environment inconsistency:** Staging and production diverge over time because deployments are manual and error-prone. * **Rollback friction:** Something breaks in production. How do you get back to the previous state? Which commit was that? ## How Kube Starter Kit Addresses This I've implemented GitOps with ArgoCD, where Git is the single source of truth for your cluster state: **Git as the source of truth:** Everything running in the cluster is defined in Git. If it's not in the repo, it shouldn't be in the cluster. **Continuous reconciliation:** ArgoCD watches your Git repository and automatically syncs the cluster to match. Drift gets corrected automatically. **App of Apps pattern:** A hierarchical structure where one ArgoCD Application manages other Applications. Add a new service by adding a file; ArgoCD picks it up automatically. **Environment separation:** Each cluster (staging, production) has its own rendered manifests. Same source templates, different configurations. **Audit everything:** Every deployment is a Git commit. Who changed what, when, and why is all in the commit history. ## What's Included ### ArgoCD Structure ``` kubernetes/ ├── src/ # Source templates │ ├── argocd/ │ │ ├── argocd/ # ArgoCD itself (bootstraps everything) │ │ ├── infrastructure/ # Infrastructure apps definition │ │ └── services/ # Application services definition │ ├── infrastructure/ # Infrastructure components │ │ ├── cert-manager/ │ │ ├── external-dns/ │ │ ├── traefik/ │ │ └── ... │ └── services/ # Your applications │ └── go-backend/ └── rendered/ # Environment-specific output ├── staging/ └── production/ ``` ### App of Apps Hierarchy ArgoCD uses a three-tier Application structure: ``` argocd-app-of-apps (root) ├── argocd-app-of-apps (manages itself 🔄) ├── infrastructure-app-of-apps │ ├── cert-manager │ ├── external-dns │ ├── traefik │ ├── external-secrets │ ├── karpenter │ └── ... └── services-app-of-apps ├── go-backend └── ... ``` The root Application manages itself and the infrastructure and services Applications, which in turn manage individual components. This creates a clean separation between platform infrastructure and application workloads, while ensuring the entire ArgoCD configuration is also GitOps-managed. ### Project Organization ArgoCD Projects provide logical separation: | Project | Purpose | | ------------------ | ------------------------------------------------- | | **argocd** | ArgoCD's own configuration and app-of-apps | | **infrastructure** | Platform components (cert-manager, ingress, etc.) | | **services** | Application workloads | ### Deployment Flow Update Kubernetes manifests or Helm values in the `src/` directory. Run `mise run render-cluster ` from the application directory to generate environment-specific manifests to `rendered/staging/` or `rendered/production/`. CI validates that rendered manifests are up-to-date (no git diff after re-rendering). ArgoCD notices the rendered manifests differ from cluster state. ArgoCD applies the changes to the cluster. With auto-sync enabled, this happens automatically. ArgoCD reports sync status and health. Failed deployments are visible immediately. ### Configuration Management Applications are enabled/disabled via a simple values file: ```yaml theme={null} # infrastructure/values.yaml applications: cert-manager: enabled: true external-dns: enabled: true traefik: enabled: true karpenter: enabled: true envoy-gateway: enabled: false # Not using this yet ``` To add a new infrastructure component, create its directory in `src/infrastructure/` and add an entry to the values file. ArgoCD picks it up on the next sync. ### Repository Access ArgoCD authenticates to your Git repository using SSH deploy keys. During bootstrap, a mise task generates a key pair and creates a Kubernetes Secret with the private key. You add the public key to your GitHub repository as a deploy key. No credentials in Git. ArgoCD discovers the repository secret automatically via the `argocd.argoproj.io/secret-type=repository` label. See [cluster bootstrap](/usage/getting-started/08-deploy-kubernetes-baseline#create-deploy-key) for setup instructions. ## Key Design Decisions | Decision | Rationale | | ------------------------------------ | -------------------------------------------------------------------------------------------------------------- | | **ArgoCD over Flux** | Better UI for visibility, more mature ecosystem, easier onboarding for teams new to GitOps. | | **App of Apps over ApplicationSets** | More explicit control over the hierarchy. ApplicationSets are powerful but can be harder to reason about. | | **Rendered manifests in Git** | What's deployed is exactly what's in Git. No runtime templating surprises. Easier to audit and diff. | | **Bootstrap-generated deploy keys** | Deploy keys are generated during bootstrap and stored as Kubernetes Secrets. Simple, no external dependencies. | | **Auto-sync enabled** | Reduces manual toil. If you commit it, it deploys. Rollback is just reverting a commit. | # Baseline Kubernetes Components Source: https://kubestarterkit.com/features/06-k8s-baseline Curated infrastructure components every production cluster needs ## The Problem A fresh Kubernetes cluster is surprisingly bare. Out of the box, you can't: * **Expose services to the internet:** No ingress/gateway api controller, no way to route external traffic to your pods. * **Provision TLS certificates:** No automated certificate management. Manual cert provisioning is tedious and error-prone. * **Manage DNS:** Creating an Ingress doesn't create public DNS records. That's a separate manual step. * **Handle secrets properly:** Kubernetes Secrets are base64-encoded, not encrypted. Storing them in Git is a security risk. * **Scale nodes automatically:** Pods can scale, but if there's no node capacity, they just sit Pending. * **Observe what's happening:** No metrics, no logs aggregation, no tracing out of the box. Every team ends up installing the same set of tools, making the same configuration decisions, and debugging the same integration issues. ## How Kube Starter Kit Addresses This I've curated a set of infrastructure components that I've used across multiple production clusters. They're pre-configured to work together, with sensible defaults and AWS integrations already wired up: **Ingress with automatic TLS:** Traefik handles traffic routing; cert-manager automatically provisions and renews Let's Encrypt certificates. **DNS automation:** External-DNS watches your Ingress resources and creates Route53 records automatically. No more manually creating DNS entries. **Secrets from AWS:** External Secrets syncs secrets from AWS Secrets Manager into Kubernetes. Secrets stay in a proper secrets manager, not in Git. **Right-sized node provisioning:** Karpenter provisions exactly the nodes your workloads need, when they need them. No over-provisioning, no waiting for scale-up. **Observability ready:** SigNoz provides metrics, logs, and traces in a single platform. See what's happening across your entire stack. ## What's Included ### Component Overview | Component | Purpose | AWS Integration | | -------------------- | ------------------------------------- | ---------------------------------- | | **traefik** | HTTP(S) traffic routing | Creates NLB via AWS Load Balancer | | **cert-manager** | Automatic TLS certificates | Uses Route53 for DNS-01 challenges | | **external-dns** | DNS record management | Creates Route53 records | | **external-secrets** | Secrets synchronization | Reads from AWS Secrets Manager | | **karpenter** | Node auto-provisioning | Launches EC2 instances | | **cloudnative-pg** | PostgreSQL operator | Uses EBS for persistent storage | | **reloader** | Config/secret change detection | Restarts pods on updates | | **SigNoz** | Observability (metrics, logs, traces) | - | ### Traffic Flow ``` Internet │ ▼ ┌───────────────────────────────┐ │ AWS NLB │ │ (created by traefik) │ └───────────────────────────────┘ │ ▼ ┌───────────────────────────────┐ │ traefik controller │ │ - TLS termination │ │ - Route by Ingress rules │ └───────────────────────────────┘ │ ▼ ┌───────────────────────────────┐ │ Your application pods │ └───────────────────────────────┘ ``` ### Secrets Flow ``` AWS Secrets Manager ▲ │ (polls) │ ┌───────────────────────────────┐ │ ClusterSecretStore │ └───────────────────────────────┘ ▲ │ (references) │ ┌───────────────────────────────┐ │ ExternalSecret │ └───────────────────────────────┘ │ │ (creates/updates) ▼ ┌───────────────────────────────┐ │ Kubernetes Secret │ └───────────────────────────────┘ ``` ### Component Details A modern cloud-native ingress controller that supports both Kubernetes Ingress and Gateway API. Deployed with: * AWS NLB for external traffic (Layer 4) * Automatic service discovery * Native Kubernetes Ingress support * Full Gateway API support (HTTPRoute, GRPCRoute, TCPRoute) * IngressRoute CRDs for advanced routing Using a single controller for both Ingress and Gateway API reduces operational complexity. Start with familiar Ingress resources today, migrate to Gateway API when ready, all without changing infrastructure. Create an Ingress resource, and traffic flows automatically. Automated X.509 certificate management. Configured with: * Let's Encrypt production and staging issuers * DNS-01 challenge solver via Route53 * Self-signed issuer for internal certificates Add a `tls` section to your Ingress; cert-manager handles the rest. Synchronizes Kubernetes resources with DNS providers. Configured to: * Watch Ingress and Service resources * Create/update Route53 records * Use txt-registry to track ownership Create an Ingress with a hostname; DNS record appears automatically. Syncs external secrets into Kubernetes. Set up with: * ClusterSecretStore pointing to AWS Secrets Manager * IRSA authentication (no credentials in cluster) * Automatic refresh on secret changes Reference a secret in AWS; it appears as a Kubernetes Secret. Just-in-time node provisioning. Configured with: * NodePool defining instance requirements * EC2NodeClass for AWS-specific settings * Consolidation to remove underutilized nodes * Support for spot instances Deploy a pod that doesn't fit; Karpenter provisions a node. PostgreSQL operator for Kubernetes. Provides: * Declarative PostgreSQL clusters * Automated backups to S3 * High availability with automatic failover * Connection pooling via PgBouncer Define a Cluster resource; get a production PostgreSQL. Watches ConfigMaps and Secrets, restarts dependent pods on changes. Handles: * Automatic pod rollouts when configs change * Annotation-based opt-in per Deployment Update a ConfigMap; pods restart automatically. Open-source observability platform. Collects: * Metrics from Kubernetes and applications * Logs from all pods * Distributed traces (OpenTelemetry) Single pane of glass for your entire stack. ### Example: Exposing an Application With all components working together, exposing an app is simple: ```yaml theme={null} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: my-app annotations: cert-manager.io/cluster-issuer: letsencrypt-prod spec: ingressClassName: traefik tls: - hosts: - app.example.com secretName: my-app-tls rules: - host: app.example.com http: paths: - path: / pathType: Prefix backend: service: name: my-app port: number: 80 ``` This single resource triggers: 1. **external-dns** creates `app.example.com` → NLB in Route53 2. **cert-manager** provisions a Let's Encrypt certificate 3. **traefik** routes traffic to your service ## Key Design Decisions | Decision | Rationale | | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Traefik over AWS ALB Ingress** | More portable, cloud-native design, excellent Kubernetes integration. Supports both Ingress and Gateway API in a single controller, reducing operational overhead. NLB at L4 is simpler. | | **external-secrets over Sealed Secrets** | Secrets stay in a proper secrets manager. Better for rotation, auditing, and access control. | | **Karpenter over Cluster Autoscaler** | Faster provisioning, better bin-packing, simpler configuration for mixed instance types. | | **SigNoz over Datadog/New Relic** | Open source, self-hosted, no per-host pricing. Good enough for most teams starting out. | | **CloudNativePG over RDS** | Runs in-cluster for lower latency and cost. Easy to move to RDS later if needed. | # CI/CD Pipelines Source: https://kubestarterkit.com/features/07-ci-cd-pipeline Automated container builds and staging deployments on every merge to main ## Overview The CI/CD pipeline automates container builds and deployments via GitHub Actions: * **Change detection:** Only builds applications that have changed, keeping CI fast in a monorepo * **Consistent builds:** All containers are built in CI with the same environment and tooling * **Rendered manifests:** Kubernetes manifests are rendered and committed to Git, so what's in the repository is exactly what ArgoCD deploys * **Automatic staging deployment:** Merges to main trigger builds and deploy to staging automatically ## What's Included ### Workflow Structure ``` .github/workflows/ ├── ci-build-push-containers.yml # Build and push container images ├── ci-test.yml # Run tests (placeholder) ├── gitops-update-manifests.yml # Render and commit K8s manifests └── terramate-*.yml # Terraform orchestration ``` Terraform CI/CD workflows (preview, deploy, drift detection) are covered in [Terraform Orchestration](/features/02-terramate#github-actions-workflows). ### Change Detection The workflow uses [dorny/paths-filter](https://github.com/dorny/paths-filter) with a separate filter configuration file: ```yaml theme={null} # .github/utils/file-filters.yaml services/go-backend: - "services/go-backend/**" - "kubernetes/src/services/go-backend/**" services/another-service: - "services/another-service/**" - "kubernetes/src/services/another-service/**" ``` The workflow matrix builds only the services that match changed paths. Changes to shared dependencies (like the workflow itself) trigger rebuilds of all dependent services. ### Build and Deploy Process Determines which services have modifications based on path filters. Builds and pushes a container image to ECR for each changed service. Triggers `gitops-update-manifests` for each built service. Generates Kubernetes manifests with the new image tags. Pushes the updated manifests to the repository. Detects the manifest changes via webhook or polling and deploys automatically. ### Container Registry Images are pushed to Amazon ECR with version tags derived from `git describe`: ``` .dkr.ecr..amazonaws.com/go-backend:1.2.3-0001-g4b5d2e7 ``` The tag format `--g` shows the base version from the last release tag, how many commits since that release, and the commit hash. This provides traceability, immutability (tags are never overwritten), and easy rollback. ### Manifest Rendering The pipeline renders Helm charts or Kustomize overlays into plain Kubernetes manifests: ``` kubernetes/ ├── src/ │ └── services/ │ └── go-backend/ │ ├── Chart.yaml │ └── values.yaml └── rendered/ └── staging/ └── services/ └── go-backend/ └── manifests.yaml # Generated by CI ``` Rendered manifests ensure what's in Git is exactly what deploys, PRs show actual Kubernetes changes (not just Helm values), and Git history provides a full audit trail. ## Key Design Decisions | Decision | Rationale | | -------------------------------------- | ---------------------------------------- | | **Change detection with path filters** | Fast CI, minimal rebuilds in a monorepo | | **Versioned image tags** | Immutable, traceable, rollback-friendly | | **Rendered manifests in Git** | ArgoCD deploys exactly what's committed | | **Staging auto-deploy on merge** | Merged code is always running in staging | # Image CVE Scanning Source: https://kubestarterkit.com/features/08-image-scanning Automated vulnerability scanning for container images across environments ## Overview The image CVE scanning workflow automatically scans container images for known vulnerabilities: * **Scheduled scans:** Runs daily against production images to catch newly disclosed CVEs * **On-demand scanning:** Can be triggered manually for any environment or specific images * **Comprehensive coverage:** Extracts images from rendered Kubernetes manifests plus manually-specified images * **Detailed reporting:** Generates SBOM and vulnerability reports with severity breakdowns ## What's Included ### Workflow Structure ``` .github/ ├── workflows/ │ └── security-image-cve-scan.yml # Main scanning workflow ├── scripts/ │ └── extract-images.sh # Image extraction script └── security/ └── additional-images.yaml # Extra images to scan ``` ### Scanning Process Collects container images from rendered Kubernetes manifests for the target environment, plus any additional images from configuration. Creates a Software Bill of Materials (SBOM) for each image using [Syft](https://github.com/anchore/syft). Analyzes each SBOM with [Grype](https://github.com/anchore/grype) to identify known CVEs. Consolidates results into a summary report with vulnerability counts by severity. ### Image Discovery Images are extracted from multiple sources: 1. **Kubernetes manifests:** Parses `image:` and `imageName:` fields from `kubernetes/rendered//` 2. **Additional images file:** Manual list in `.github/security/additional-images.yaml` for images not in manifests (e.g., operator-pulled images, sidecar injectors) 3. **Workflow input:** Ad-hoc images passed via workflow dispatch ```yaml theme={null} # .github/security/additional-images.yaml images: # Example entries for images not in standard manifests: # - docker.io/library/postgres:15-alpine # - gcr.io/istio-release/proxyv2:1.20.0 ``` ### Vulnerability Scanning Each image is scanned in a separate parallel job using GitHub Actions matrix strategy, enabling fast scans even with many images. The workflow uses the Anchore toolchain: | Tool | Purpose | | ----------------------------------------- | ---------------------------------------------- | | [Syft](https://github.com/anchore/syft) | Generates CycloneDX SBOM from container images | | [Grype](https://github.com/anchore/grype) | Matches SBOM packages against CVE databases | The workflow handles both public images and private ECR images (via OIDC authentication). ### Report Output The workflow generates a GitHub Actions job summary with: * **Severity breakdown:** Critical, High, Medium, Low, Negligible, Unknown counts * **Per-image results:** Table showing vulnerability counts for each scanned image * **Scan artifacts:** Full Grype JSON output and SBOMs retained for 30 days #### 🔍 CVE Scan Report **Environment:** production\ **Scan Date:** YYYY-MM-DD HH:MM:SS UTC #### Summary | Severity | Count | | ------------ | ----- | | 🔴 Critical | 0 | | 🟠 High | 0 | | 🟡 Medium | 8 | | 🔵 Low | 12 | | ⚪ Negligible | 24 | | ❓ Unknown | 0 | **Successful scans:** 5\ **Failed scans:** 0 #### Results by Image | Image | Status | Critical | High | Medium | Low | | ---------------------------------------- | ------ | -------- | ---- | ------ | --- | | `ghcr.io/cloudnative-pg/postgresql:17.2` | ✅ | 0 | 0 | 4 | 5 | | `/go-backend:0.2.1` | ✅ | 0 | 0 | 2 | 3 | | `/go-backend/migrations:0.2.1` | ✅ | 0 | 0 | 2 | 3 | | `quay.io/argoproj/argocd:v2.13.3` | ✅ | 0 | 0 | 0 | 1 | | `docker.io/traefik:v3.6.5` | ✅ | 0 | 0 | 0 | 0 | ## Usage The workflow runs daily at 5 AM UTC (midnight EST) against production images. It can also be triggered manually via `workflow_dispatch` to scan a specific environment (`production`, `staging`, or `none`) or ad-hoc images. ## Key Design Decisions | Decision | Rationale | | ----------------------------------- | -------------------------------------------------- | | **Daily scheduled scans** | Catch newly disclosed CVEs in existing images | | **Extract from rendered manifests** | Scan exactly what's deployed, not source templates | | **Parallel per-image jobs** | Fast scanning with isolated failures | | **SBOM generation** | Enables offline analysis and compliance reporting | | **Fail-fast disabled** | Complete scan of all images even if some fail | ## Additional Configurability ### Custom Severity Thresholds To fail the workflow on critical vulnerabilities, modify the `report` job: ```yaml theme={null} - name: Fail on critical vulnerabilities if: steps.report.outputs.total_critical != '0' run: | echo "::error::Found ${{ steps.report.outputs.total_critical }} critical vulnerabilities" exit 1 ``` ### Slack/Email Notifications Add a notification step after the report job to alert on scan results. ### Vulnerability Suppression Use Grype's [ignore rules](https://github.com/anchore/grype#specifying-matches-to-ignore) for false positives or accepted risks. # Release Management Source: https://kubestarterkit.com/features/09-release-management Automated release PRs with release-please and controlled production deployments ## The Problem Production deployments need more ceremony than staging, but that doesn't mean they should be painful: * **Manual release processes:** Someone maintains a changelog, bumps versions, creates tags, and hopes they didn't miss anything. * **Changelog drift:** The changelog is always out of date because updating it is tedious and easy to forget. * **Version confusion:** What's actually in production? What commits are included in v1.2.3? Good luck piecing that together from Git history. * **Risky deployments:** Without a clear release boundary, production deployments become "let's deploy main and hope for the best." ## How Kube Starter Kit Addresses This I've integrated [release-please](https://github.com/googleapis/release-please) to automate release management with a simple, controlled workflow: **Automatic release PRs:** As commits land on main, release-please maintains a PR that accumulates changes and auto-generates a changelog. **Conventional commits drive versioning:** Commit messages determine version bumps. `fix:` bumps patch, `feat:` bumps minor, `feat!:` or `BREAKING CHANGE` bumps major. **Merge to release:** When you're ready for production, merge the release PR. That's it. **Production deployment on release:** Merging the release PR triggers the production build and deployment pipeline, rendering manifests for production and letting ArgoCD sync. ## What's Included ### Release-Please Configuration ``` .github/ ├── workflows/ │ └── release-please.yaml └── release-please-config.json ``` ### How It Works Developers merge PRs with conventional commit messages (`feat:`, `fix:`, `chore:`, etc.). release-please automatically updates (or creates) a release PR that includes all pending changes with a generated changelog. The release PR shows exactly what will be released: version bump, changelog entries, and all included commits. Merging the release PR creates a Git tag and GitHub release. The release triggers the production build workflow, which renders manifests and deploys to production via ArgoCD. ### Conventional Commits The versioning is driven by commit message prefixes: | Prefix | Version Bump | Example | | ------------------------------ | --------------------- | ------------------------------------------- | | `fix:` | Patch (1.0.0 → 1.0.1) | `fix: resolve null pointer in auth handler` | | `feat:` | Minor (1.0.0 → 1.1.0) | `feat: add user profile endpoint` | | `feat!:` or `BREAKING CHANGE:` | Major (1.0.0 → 2.0.0) | `feat!: redesign authentication API` | | `chore:`, `docs:`, `ci:` | Patch (1.0.0 → 1.0.1) | `chore: update dependencies` | Note: `chore:` commits bump the patch version to ensure pre-release image tags (e.g., `1.2.4-rc0001-g4b5d2e7`) always reflect the upcoming release version. ### Release PR Example When commits accumulate, the release PR looks like: ```markdown theme={null} ## [1.2.0](https://github.com/org/repo/compare/v1.1.0...v1.2.0) ### Features * add user profile endpoint (#45) * support bulk operations in API (#42) ### Bug Fixes * resolve null pointer in auth handler (#48) * fix pagination on large datasets (#44) ``` This becomes both the PR description and the GitHub release notes. ### Production Deployment Flow ``` main branch │ ├── commit: feat: new feature ├── commit: fix: bug fix │ ▼ Release PR (auto-maintained) │ │ [Merge when ready] ▼ Git Tag + GitHub Release │ ▼ Production Build Workflow │ ├── Build changed containers ├── Render production manifests └── Commit to rendered/production/ │ ▼ ArgoCD syncs production cluster ``` ### Multi-Application Releases In a monorepo with multiple applications, each application has its own version and receives separate Git tags (e.g., `services/go-backend@1.2.3`). However, release-please groups all pending releases into a single PR. This gives you: * **Independent versioning:** Each application's version reflects its own changes * **Coordinated releases:** One PR to review and merge for all applications ready to release * **Simpler workflow:** No need to manage multiple release PRs The grouping behavior is configurable if you prefer separate release PRs per application. ## Key Design Decisions | Decision | Rationale | | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | **release-please over manual releases** | Automated changelog generation and version bumping eliminates manual toil and human error. | | **Conventional commits** | Commit messages become meaningful. Version bumps are predictable and auditable. | | **PR-based releases** | The release PR provides a clear review point before production. You can see exactly what's included. | | **Separate staging and production pipelines** | Staging deploys on every merge for fast feedback. Production deploys only on release for controlled rollouts. | | **Same rendered manifests pattern** | Production uses the same pattern as staging: rendered manifests in Git. Consistency across environments. | ## Workflow Summary | Event | Staging | Production | | ----------------- | -------------------------------- | -------------------------------------------- | | PR merged to main | Builds and deploys automatically | No action | | Release PR merged | No action | Builds and deploys automatically | | Rollback needed | Revert commit, auto-redeploys | Revert release commit or deploy previous tag | This separation gives you fast iteration on staging while maintaining controlled, auditable production releases. # Demo Applications Source: https://kubestarterkit.com/features/10-demo-applications Fully functional example applications demonstrating end-to-end patterns ## The Problem Infrastructure and deployment tooling is only half the story. Teams often struggle with: * **No reference implementations:** You have a Kubernetes cluster, but how should applications actually be structured to use it? * **Missing the full picture:** Tutorials show isolated pieces (a Dockerfile here, a Helm chart there) but not how everything connects. * **Infrastructure gaps:** How do you provision a database for your app? How do you manage credentials? How do you connect from a pod? * **Packaging questions:** What's the right way to structure Kubernetes manifests? How much should go in the Helm chart vs. external configuration? ## How Kube Starter Kit Addresses This The kit includes fully functional demo applications that demonstrate the complete path from source code to running in production: **Complete wiring, minimal code:** The applications themselves are simple, but include full configuration and external resource integration: database connections, secrets management, health checks, and Terraform-provisioned AWS resources. **End-to-end patterns:** Each demo shows the full lifecycle: source code, Dockerfile, Helm chart, Terraform for cloud resources, and integration with the deployment pipeline. **Copy and adapt:** The demos are designed to be starting points. Fork them, rename them, and build your actual services on top of proven patterns. ## What's Included ### Application Structure ``` applications/ └── go-backend/ ├── src/ # Application source code │ ├── main.go │ ├── go.mod │ └── ... ├── Dockerfile # Container build └── README.md kubernetes/src/services/ └── go-backend/ ├── Chart.yaml # Helm chart definition ├── values.yaml # Default values ├── values-staging.yaml # Environment overrides ├── values-production.yaml └── templates/ ├── deployment.yaml ├── service.yaml ├── ingress.yaml └── ... terraform/modules/ └── app-resources/ └── go-backend/ # Cloud resources (S3, RDS, etc.) ``` ### Go Backend A production-ready Go service demonstrating: * HTTP server with health check endpoints (`/healthz`, `/readyz`) * Structured logging * Graceful shutdown handling * Configuration via environment variables * Database connectivity patterns * Multi-stage Dockerfile for minimal image size * Non-root user for security * Proper signal handling * Build-time metadata (version, commit SHA) * Deployment with resource limits and requests * Liveness and readiness probes * Service and Ingress configuration * ConfigMaps and Secrets integration * Horizontal Pod Autoscaler setup * S3 bucket for object storage * RDS PostgreSQL database (optional) * IAM roles for pod-level AWS access via Pod Identity * Secrets stored in AWS Secrets Manager ### Infrastructure Integration Each demo application shows how to provision and connect to cloud resources: ```hcl theme={null} # terraform/modules/app-resources/go-backend/main.tf # S3 bucket for application data module "s3_bucket" { source = "terraform-aws-modules/s3-bucket/aws" bucket = "${module.this.id}-data" } # IAM policy for S3 bucket access resource "aws_iam_policy" "s3_access" { name = "${module.this.id}-s3-access" policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"] Resource = "${module.s3_bucket.s3_bucket_arn}/*" } ] }) } # Pod Identity for AWS access module "pod_identity" { source = "terraform-aws-modules/eks-pod-identity/aws" name = "${var.eks_cluster_name}-go-backend" additional_policy_arns = { S3Access = aws_iam_policy.s3_access.arn } associations = { this = { cluster_name = var.eks_cluster_name namespace = var.kubernetes_namespace service_account = var.kubernetes_service_account } } } ``` Pod Identity associations are managed by Terraform. The application's ServiceAccount automatically receives AWS credentials without requiring annotations. ### Secrets Management Demo applications use External Secrets to pull credentials from AWS Secrets Manager: ```yaml theme={null} apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: go-backend-db spec: secretStoreRef: name: aws-secrets-manager kind: ClusterSecretStore target: name: go-backend-db-credentials data: - secretKey: DATABASE_URL remoteRef: key: go-backend/database property: url ``` No credentials in Git. No manual secret creation. Secrets sync automatically from AWS. ### Environment Configuration Each application supports per-environment configuration: ```yaml theme={null} # values.yaml (defaults) replicaCount: 1 resources: requests: cpu: 100m memory: 128Mi ``` ```yaml theme={null} # values.staging.yaml replicaCount: 2 ``` ```yaml theme={null} # values.production.yaml replicaCount: 3 resources: requests: cpu: 500m memory: 512Mi ``` The CI/CD pipeline renders the appropriate values for each environment. ## Using Demo Apps as Templates Duplicate an existing demo app directory and rename it for your new service. Replace the demo logic with your actual service implementation. Adjust the cloud resources to match your application's needs (different database, additional S3 buckets, etc.). Configure resource limits, replica counts, and environment variables for your service. Add your new service to the change detection filters in the CI/CD workflow. Commit and push. The pipeline builds, renders manifests, and ArgoCD deploys. ## Key Design Decisions | Decision | Rationale | | ---------------------------------- | -------------------------------------------------------------------------------------------------- | | **Full applications, not stubs** | Teams learn better from working examples than from documentation. Real code shows real patterns. | | **Terraform alongside Kubernetes** | Applications often need cloud resources. Showing both together demonstrates the complete workflow. | | **Pod Identity for AWS access** | Pod-level IAM roles are more secure than shared credentials. The demos show the proper pattern. | | **External Secrets integration** | Secrets management is often an afterthought. Including it in demos makes it the default pattern. | | **Per-environment configuration** | Demonstrates how to handle environment differences with Kustomize, Helm, or Timoni. | # Local Development Source: https://kubestarterkit.com/features/11-local-development KinD, Tilt, and mirrord for fast local Kubernetes development ## Overview The kit includes a complete local development environment using three complementary tools: * **[KinD](https://kind.sigs.k8s.io/) (Kubernetes in Docker):** A real multi-node Kubernetes cluster running locally * **[Tilt](https://tilt.dev/):** Continuous development that rebuilds and redeploys on file changes * **[mirrord](https://mirrord.dev/):** Run services directly on your host machine while connected to the KinD cluster ## How It Works ### KinD Cluster KinD creates a real Kubernetes cluster using Docker containers as nodes. The kit's configuration provides: **Multi-node cluster:** 1 control-plane + 2 worker nodes, matching a realistic production topology. **Local container registry:** Images built locally are pushed to `localhost:5001` and pulled by the cluster, avoiding the need for external registries during development. **Stable networking:** The cluster uses a dedicated Docker network (`172.20.0.0/16`) with [cloud-provider-kind](https://github.com/kubernetes-sigs/cloud-provider-kind) to assign real LoadBalancer IPs. Ingress gets `172.20.0.100`, making URLs predictable. **sslip.io for DNS:** Services are accessible via URLs like `go-backend.172-20-0-100.sslip.io` without `/etc/hosts` modifications. ### Tilt Tilt watches your source code and Kubernetes manifests, automatically rebuilding containers and redeploying when files change. **Hierarchical Tiltfiles:** The main `Tiltfile` includes infrastructure and services, each with their own `Tiltfile`. This mirrors the ArgoCD app-of-apps structure. **Dependency ordering:** Resources declare dependencies (e.g., apps wait for cloudnative-pg operator), ensuring correct startup order. **Smart rebuilds:** Only affected resources rebuild. Change application code and only that container rebuilds. Change a Helm value and only that release updates. **Live UI:** Tilt's web UI shows resource status, logs, and build times. Click a resource to see its logs, trigger manual rebuilds, or debug failures. ### mirrord While Tilt runs your services inside containers in the KinD cluster, mirrord lets you run a service directly on your host machine while staying connected to the cluster. This provides several advantages for debugging and profiling: **Native debugging:** Attach your IDE's debugger directly to the process without container layers. Set breakpoints, step through code, and inspect variables with full IDE integration. **Profiling tools:** Use host-native profilers (pprof, perf, Instruments) without containerization overhead or special configuration. **Faster iteration:** Skip container builds entirely; just recompile and restart. Your local binary intercepts traffic that would go to the in-cluster pod. **Steal traffic:** Redirect all traffic from a pod to your local process. Your local code handles real requests while you debug. **Access cluster resources:** Your local process can reach cluster-internal services (databases, APIs) as if it were running in the cluster. **Inherit environment:** Environment variables and secrets from the target pod are available locally. ## Development Workflows ### Local Kubernetes (Tilt) Best for: Testing complete deployments including Kubernetes resources, databases, and ingress. ``` local/ ├── kind/ │ └── cluster-config.yaml # Cluster definition └── tilt/ └── Tiltfile # Main entry point ``` Tilt deploys: * **Infrastructure:** cert-manager, cloudnative-pg, traefik * **Applications:** Three variants of go-backend (Kustomize, Helm, Timoni) Each application gets its own PostgreSQL database, runs migrations, and is accessible via ingress. ## Local Configuration Each component has `values.local.yaml` or a `local/` overlay: ``` kubernetes/src/ ├── infrastructure/ │ ├── traefik/values.local.yaml # LoadBalancer IP │ └── cloudnative-pg/values.local.yaml └── services/ ├── go-backend/local/ # Kustomize overlay ├── go-backend-helm/values.local.yaml └── go-backend-timoni/values.local.yaml ``` These configure: * `storageClass: standard` for local PVCs (KinD's default) * Image tags pointing to `localhost:5001` registry * sslip.io hostnames for ingress The same templating tools (Kustomize, Helm, Timoni) are used locally and in CI, ensuring consistency. # SOC2 & Compliance Frameworks Source: https://kubestarterkit.com/features/12-compliance-frameworks How Kube Starter Kit features map to SOC2 controls and other compliance requirements ## Overview Kube Starter Kit is designed with security and auditability as core principles. While the kit itself doesn't make you SOC2 compliant (that requires organizational policies, procedures, and a formal audit), it provides the technical foundation that addresses many SOC2 Trust Services Criteria. This page maps Kube Starter Kit features to specific SOC2 controls and explains how they support your compliance posture. ## SOC2 Trust Services Criteria Coverage SOC2 audits evaluate controls across five Trust Services Criteria. Kube Starter Kit primarily addresses **Security (Common Criteria)** and **Availability**, with supporting capabilities for **Processing Integrity** and **Confidentiality**. ### Security (Common Criteria) The Security category forms the foundation of SOC2 and is required for all audits. #### CC6.1 - Logical Access Security > *The entity implements logical access security software, infrastructure, and architectures over protected information assets to protect them from security events.* | Control Requirement | Kube Starter Kit Feature | Implementation | | ----------------------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | **Role-based access control** | [User Management](/features/04-user-management) | Single `users.yaml` defines AWS SSO users, groups, and permission sets. GitHub teams mirror AWS groups for consistent RBAC. | | **Centralized identity management** | [User Management](/features/04-user-management) | AWS IAM Identity Center provides centralized authentication. No long-lived credentials; users authenticate via SSO portal. | | **Least privilege enforcement** | [User Management](/features/04-user-management) | Pre-configured permission sets (Admin, PowerUser, ReadOnly) with AWS-managed policies. Easy to add custom permission sets for more granular access. | | **Network segmentation** | [AWS Architecture](/features/03-aws-architecture) | EKS nodes run in private subnets with no public IPs. Workloads are isolated from direct internet access. | | **Multi-account isolation** | [AWS Architecture](/features/03-aws-architecture) | Staging and production in separate AWS accounts. Hard boundary prevents cross-environment access or accidental impact. | **Evidence artifacts:** * Git history of `users.yaml` changes showing access reviews * AWS IAM Identity Center user/group listings * Terraform state showing VPC and subnet configurations *** #### CC6.2 - Access Provisioning and Deprovisioning > *Prior to issuing system credentials and granting system access, the entity registers and authorizes new internal and external users.* | Control Requirement | Kube Starter Kit Feature | Implementation | | ------------------------------ | ----------------------------------------------- | --------------------------------------------------------------------------------------------------- | | **Documented access requests** | [User Management](/features/04-user-management) | All access changes are pull requests. PR description documents who, what, and why. | | **Approval workflow** | [User Management](/features/04-user-management) | Branch protection requires PR approval before merge. Access changes get code review. | | **Automated provisioning** | [Terraform + Terramate](/features/01-terraform) | Merging approved PRs triggers Terraform apply. No manual AWS console access needed. | | **Timely deprovisioning** | [User Management](/features/04-user-management) | Remove user from `users.yaml`, open PR, merge. Access revoked across GitHub and AWS simultaneously. | **Evidence artifacts:** * Pull request history showing access change approvals * Git blame on `users.yaml` showing who approved each change * Terraform plan outputs showing exact access modifications *** #### CC6.3 - Access Removal > *The entity removes access to protected information assets when appropriate.* | Control Requirement | Kube Starter Kit Feature | Implementation | | -------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | **Single source of truth** | [User Management](/features/04-user-management) | One file controls all access. Removing a user entry revokes GitHub org membership, team access, and AWS SSO access. | | **Audit trail** | [GitOps](/features/05-gitops) | Git history provides immutable record of when access was removed and by whom. | | **No orphaned access** | [Terraform + Terramate](/features/01-terraform) | Terraform manages state declaratively. Users not in `users.yaml` are removed on apply. | **Evidence artifacts:** * Git commits showing user removal * Terraform destroy plans for removed users * AWS CloudTrail logs showing SSO user deletion *** #### CC6.6 - Protection Against Threats Outside System Boundaries > *The entity implements logical access security measures to protect against threats from sources outside its system boundaries.* | Control Requirement | Kube Starter Kit Feature | Implementation | | ------------------------------------ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | **TLS encryption** | [Kubernetes Baseline](/features/06-k8s-baseline) | cert-manager automatically provisions and renews Let's Encrypt certificates for all ingress endpoints. | | **No default credentials** | [Kubernetes Baseline](/features/06-k8s-baseline) | External Secrets pulls credentials from AWS Secrets Manager. No secrets in Git, no default passwords. | | **Network perimeter controls** | [AWS Architecture](/features/03-aws-architecture) | NLB with security groups controls inbound traffic. Private subnets prevent direct access to nodes. | | **Container vulnerability scanning** | [Image Scanning](/features/08-image-scanning) | Daily CVE scans against production images. SBOM generation for supply chain visibility. | **Evidence artifacts:** * cert-manager Certificate resources showing valid TLS * CVE scan reports from GitHub Actions * VPC security group configurations *** #### CC6.7 - Transmission Encryption > *The entity restricts the transmission, movement, and removal of information to authorized internal and external users and processes.* | Control Requirement | Kube Starter Kit Feature | Implementation | | ------------------------------------ | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | | **Encryption in transit** | [Kubernetes Baseline](/features/06-k8s-baseline) | All external traffic terminates TLS at traefik. Internal cluster traffic uses Kubernetes network policies. | | **Automated certificate management** | [Kubernetes Baseline](/features/06-k8s-baseline) | cert-manager handles certificate lifecycle. No manual certificate handling or risk of expiration. | **Evidence artifacts:** * Ingress resources with TLS configuration * Certificate resources showing renewal history *** #### CC6.8 - Malicious Software Prevention > *The entity implements controls to prevent or detect and act on the introduction of unauthorized or malicious software.* | Control Requirement | Kube Starter Kit Feature | Implementation | | ---------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | **Container image scanning** | [Image Scanning](/features/08-image-scanning) | Grype scans all container images for known CVEs. Reports generated with severity breakdowns. | | **SBOM generation** | [Image Scanning](/features/08-image-scanning) | Syft generates Software Bill of Materials for each image. Enables tracking of vulnerable dependencies. | | **Immutable deployments** | [CI/CD Pipeline](/features/07-ci-cd-pipeline) | Container images are tagged with version and commit hash. Tags are never overwritten. | **Evidence artifacts:** * CVE scan reports and SBOM artifacts * ECR image tags showing immutable versioning * GitHub Actions workflow runs *** ### CC8.1 - Change Management > *The entity authorizes, designs, develops or acquires, configures, documents, tests, approves, and implements changes to infrastructure, data, software, and procedures.* | Control Requirement | Kube Starter Kit Feature | Implementation | | ------------------------------ | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | **Change authorization** | [GitOps](/features/05-gitops) | All changes require pull request approval. Branch protection enforces review requirements. | | **Change documentation** | [GitOps](/features/05-gitops) | Git commits document what changed. PR descriptions explain why. Rendered manifests show exact Kubernetes changes. | | **Separation of environments** | [AWS Architecture](/features/03-aws-architecture) | Changes deploy to staging first. Production deployment requires separate approval/promotion. | | **Automated testing** | [CI/CD Pipeline](/features/07-ci-cd-pipeline) | CI runs on every PR. Manifest validation ensures rendered output matches committed files. | | **Rollback capability** | [GitOps](/features/05-gitops) | Reverting a Git commit triggers ArgoCD rollback. Previous state is always recoverable. | **Evidence artifacts:** * Pull request history with approvals * Git diff showing exact changes per deployment * ArgoCD sync history showing deployments *** #### CC8.2 - Infrastructure and Software Changes > *The entity authorizes, documents, and implements changes to infrastructure and software.* | Control Requirement | Kube Starter Kit Feature | Implementation | | -------------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------- | | **Infrastructure as Code** | [Terraform + Terramate](/features/01-terraform) | All AWS infrastructure defined in Terraform. Changes visible in PR diffs. | | **Change visibility** | [Terramate](/features/02-terramate) | `terraform plan` output posted to PRs. Reviewers see exactly what will change before approval. | | **Drift detection** | [Terramate](/features/02-terramate) | Scheduled workflows detect infrastructure drift. Alerts when actual state differs from code. | | **Kubernetes changes** | [GitOps](/features/05-gitops) | Rendered manifests committed to Git. ArgoCD shows diff between desired and actual state. | **Evidence artifacts:** * Terraform plan outputs in PRs * Drift detection workflow results * ArgoCD application sync status *** ### A1.1, A1.2 - Availability > *The entity maintains, monitors, and evaluates current processing capacity and use of system components to support availability.* | Control Requirement | Kube Starter Kit Feature | Implementation | | ----------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | **Capacity management** | [Kubernetes Baseline](/features/06-k8s-baseline) | Karpenter auto-provisions nodes based on workload demand. Removes underutilized capacity automatically. | | **High availability** | [AWS Architecture](/features/03-aws-architecture) | EKS spans 3 availability zones. Node failures don't affect application availability. | | **Observability** | [Kubernetes Baseline](/features/06-k8s-baseline) | SigNoz provides metrics, logs, and traces. Enables monitoring of system health and performance. | | **Database resilience** | [Kubernetes Baseline](/features/06-k8s-baseline) | CloudNativePG provides automated failover for PostgreSQL. Backups to S3 enable point-in-time recovery. | **Evidence artifacts:** * Karpenter provisioner logs * SigNoz dashboards and alerts * CloudNativePG backup records *** ### C1.1, C1.2 - Confidentiality > *The entity identifies and maintains confidential information to meet the entity's objectives related to confidentiality.* | Control Requirement | Kube Starter Kit Feature | Implementation | | ---------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | **Secrets management** | [Kubernetes Baseline](/features/06-k8s-baseline) | External Secrets syncs credentials from AWS Secrets Manager. Secrets never stored in Git. | | **Encryption at rest** | [AWS Architecture](/features/03-aws-architecture) | EBS volumes, S3 buckets, and Secrets Manager all encrypted with KMS by default. | | **Access to secrets** | [Kubernetes Baseline](/features/06-k8s-baseline) | Pod Identity provides scoped AWS credentials. Pods only access secrets they're authorized for. | **Evidence artifacts:** * ExternalSecret resources (not containing actual secret values) * AWS Secrets Manager access logs * Pod Identity association configurations *** ## Control Matrix Summary | SOC2 Control | Primary Feature | Evidence Location | | ------------------------------- | ------------------- | ------------------------------ | | CC6.1 - Logical Access | User Management | `users.yaml` + Git history | | CC6.2 - Access Provisioning | User Management | Pull requests | | CC6.3 - Access Removal | User Management | Git commits + Terraform state | | CC6.6 - External Threats | Image Scanning, TLS | CVE reports, Certificates | | CC6.7 - Transmission Encryption | cert-manager | Ingress TLS configs | | CC6.8 - Malicious Software | Image Scanning | SBOM + CVE reports | | CC8.1 - Change Management | GitOps | Git history + ArgoCD | | CC8.2 - Infrastructure Changes | Terraform/Terramate | Plan outputs + drift detection | | A1.1/A1.2 - Availability | Karpenter, Multi-AZ | Provisioner logs, SigNoz | | C1.1/C1.2 - Confidentiality | External Secrets | Secrets Manager audit logs | *** ## Other Compliance Frameworks The same technical controls that support SOC2 also map to other compliance frameworks: ### HIPAA (Healthcare) | HIPAA Requirement | Kube Starter Kit Feature | | -------------------------------------- | ------------------------------- | | Access Controls (§164.312(a)(1)) | User Management with RBAC | | Audit Controls (§164.312(b)) | GitOps audit trail + CloudTrail | | Transmission Security (§164.312(e)(1)) | TLS via cert-manager | | Integrity Controls (§164.312(c)(1)) | Immutable container images | ### PCI-DSS (Payment Card) | PCI-DSS Requirement | Kube Starter Kit Feature | | ---------------------------- | --------------------------------- | | Req 1: Network Segmentation | Private subnets, security groups | | Req 2: Secure Configurations | Infrastructure as Code | | Req 6: Secure Development | CI/CD with vulnerability scanning | | Req 7: Restrict Access | RBAC via User Management | | Req 8: Identify Users | AWS IAM Identity Center | | Req 10: Track Access | Git audit trail + CloudTrail | ### ISO 27001 | ISO 27001 Control | Kube Starter Kit Feature | | ------------------------ | ------------------------ | | A.9 Access Control | User Management | | A.12 Operations Security | GitOps, CI/CD | | A.14 System Development | IaC, rendered manifests | | A.16 Incident Management | Observability (SigNoz) | *** ## Preparing for Audit While Kube Starter Kit provides the technical controls, a SOC2 audit also requires: 1. **Policies and Procedures:** Written documentation of your access control policy, change management process, incident response plan, etc. 2. **Evidence Collection:** Regular exports of: * Git history for access changes * Terraform plan outputs * CVE scan reports * ArgoCD sync history 3. **Access Reviews:** Periodic review of `users.yaml` to verify access is still appropriate. 4. **Risk Assessments:** Documentation of how you triage and remediate CVE findings. 5. **Training Records:** Evidence that team members understand security procedures. **Start early:** Begin collecting evidence before your audit period. The Git-based workflow automatically creates an audit trail, but you'll want to establish the rhythm of periodic reviews and documentation. # Kube Starter Kit Source: https://kubestarterkit.com/index From zero to production Kubernetes in days, not months. # The Kubernetes platform you'd build if you had the time Getting Kubernetes right is hard. Not the “spin up a cluster” part, that’s easy. The hard part is everything else: infrastructure as code and automation, implementing GitOps and deployments, managing secrets securely, establishing observability, meeting compliance requirements, and making it all work across multiple environments without spending months on it. I've built and maintained Kubernetes platforms at multiple companies. Each time, I found myself solving the same problems, making the same architectural decisions, and wishing I had a solid starting point instead of building from scratch. **Kube Starter Kit is that starting point.** It's an opinionated, production-ready platform for **AWS and EKS** that gives you everything you need to run applications on Kubernetes: infrastructure, deployment pipelines, and baseline components, all wired together and ready to go. ## What's Included Modular, well-structured Terraform for AWS with multi-environment support via Terramate Stack-based Terraform orchestration with Terramate; change detection, outputs sharing, and CI/CD Multi-account setup with VPC, EKS, and secure account boundaries Terraform-managed GitHub and AWS IAM Identity Center users and permissions ArgoCD-based GitOps for declarative, auditable deployments Curated set of components: ingress, cert-manager, external-dns, secrets, observability, and more Automated container builds and staging deployments on every merge to main Automated vulnerability scanning for container images with daily scheduled scans Automated release PRs with release-please and controlled production deployments Fully functional example applications demonstrating end-to-end patterns KinD, Tilt, and mirrord for fast local Kubernetes development How Kube Starter Kit features map to SOC2 controls and other compliance requirements ## Who This Is For Kube Starter Kit is built for **engineering teams at early-stage companies** who: * Are confident Kubernetes is the right choice for their infrastructure * Want to get to production quickly without cutting corners * Value having an opinionated path from someone who's done this before * Would rather customize a working system than build one from scratch ## What to Expect **You own the infrastructure.** This is your platform running in your AWS account. No managed service dependency, no third-party with access to your environment. Full control, full responsibility. **Standard tools, no abstraction layer.** Unlike OpenShift or Tanzu, there's no proprietary UI or vendor lock-in. It's standard Kubernetes with standard tools. Your team's existing knowledge transfers directly. **A starting point you make your own.** Fork the repo and customize it to fit your needs. All future updates to the kit are available to you, but you're also free to diverge. It's a foundation, not a constraint. ## Get Started Learn about each component and the decisions behind them Get access to the kit and optional consulting support # Licensing / Pricing Source: https://kubestarterkit.com/license-pricing Source available, not open source ## License Kube Starter Kit is **source-available**, not open source. * **Learning and reference?** Go for it. Read the code, understand the patterns, use it for personal projects or experimentation. * **Using it to run your business?** Please purchase a license. See the full [LICENSE](https://github.com/DevOps-Directive/kube-starter-kit/blob/main/LICENSE) for details. When you purchase a license, you get perpetual rights: you own the code and can use it indefinitely. ## Pricing ### Unpaid License Free for learning, reference, personal projects, and experimentation. ### Paid Licenses **\$8,500** one-time * Use of all Terraform modules, Kubernetes manifests, and CI/CD pipelines * Access to all future updates * **Async support** (Slack/Teams/Discord) for 6 months Best for teams with strong Kubernetes and AWS experience who want a solid starting point. **\$18,500** one-time * Everything in Self-Serve * **60 hours of consulting** to help you roll out, operationalize, and extend * Pair on initial setup, customization, and deployment * Priority access to answer questions and review your implementation Best for teams who want hands-on guidance during setup. ### Additional Consulting Hours Need more help after your initial hours? Additional consulting is available at **\$235/hr**. Use extra hours for: * Extending the platform with custom features * Training your team on operations and maintenance * Troubleshooting and debugging * Architecture reviews as your needs evolve *** ## Questions? Not sure if you need a license? Just ask: [sid@devopsdirective.com](mailto:sid@devopsdirective.com) I'm reasonable. If you're a tiny bootstrapped startup or have an unusual situation, reach out and we'll figure it out. # Overview Source: https://kubestarterkit.com/usage/getting-started/00-overview Overview of the deployment process for Kube Starter Kit This page provides a high-level overview of the deployment process. Each section links to detailed documentation with step-by-step instructions. ## [1. Prerequisites](/usage/getting-started/01-prerequisites) Covers the AWS account structure, GitHub organization requirements, domain setup, and tooling you'll need before starting. ## [2. Repository Setup](/usage/getting-started/02-repository-setup) Fork the repository and install the development tools. ## [3. Bootstrap AWS Accounts](/usage/getting-started/03-bootstrap-accounts) Create the foundational resources that Terraform needs to manage everything else: state storage, IAM roles, and cross-account trust relationships. This is a one-time setup that enables both local Terraform runs and CI/CD automation. ## [4. Configure Access](/usage/getting-started/04-configure-access) Set up user access for AWS and GitHub from a single configuration file. ## [5. Configure CI/CD Integrations](/usage/getting-started/05-configure-integrations) Configure GitHub Actions to authenticate with AWS and set up any optional integrations like Terramate Cloud. ## [6. Deploy Infrastructure](/usage/getting-started/06-deploy-infrastructure) Deploy the core AWS infrastructure: networking, EKS cluster, and application-specific resources. This step is performed for each deployment environment (e.g. staging and production). ## [7. Cluster Access](/usage/getting-started/07-cluster-access) Configure kubectl access to your EKS cluster through the bastion host. This step is performed for each deployment environment (e.g. staging and production). ## [8. Deploy Kubernetes Baseline](/usage/getting-started/08-deploy-kubernetes-baseline) Bootstrap ArgoCD and the infrastructure components it depends on. Once running, ArgoCD takes over and manages all remaining cluster resources. This step is performed for each deployment environment (e.g. staging and production). ## [9. Local Development](/usage/getting-started/09-local-development-setup) Set up a local Kubernetes environment for development and testing. # Prerequisites Source: https://kubestarterkit.com/usage/getting-started/01-prerequisites What you need before getting started ## Required ### AWS Account You need an AWS account where the infrastructure will be deployed. The kit uses [AWS Organizations](https://aws.amazon.com/organizations/) to manage multiple accounts (infrastructure, staging, production). If your AWS account is brand new, you may need to request a quota increase for AWS Organizations. The default limit is 5 accounts, which may not be sufficient. Request an increase through the [Service Quotas console](https://console.aws.amazon.com/servicequotas/). #### Setting Up Multiple Accounts with Control Tower You can set up your AWS accounts however you prefer. The kit just needs an management account, infrastructure account, ECR account, staging account, and production account to exist. If you already have a multi-account structure, you can skip this section. If you're starting fresh, [AWS Control Tower](https://aws.amazon.com/controltower/) is a good option. It provides: * **Account Factory:** Provision new accounts with consistent baseline configurations * **Guardrails:** Pre-configured governance rules (SCPs) for security and compliance * **IAM Identity Center (SSO):** Centralized access management across all accounts * **Landing Zone:** A well-architected multi-account environment out of the box To set up Control Tower: 1. Enable Control Tower in your **management** account 2. Create an **Infrastructure** account (the entrypoint and hub for IaC automations) 3. Create an **ECR** account (for the Elastic Container Registry used by all environments) 4. Create a **Staging** account (for the staging EKS cluster and application resources) 5. Create a **Production** account (for the production EKS cluster and application resources) Control Tower automatically sets up AWS IAM Identity Center, which the kit uses for authenticating to each account via SSO. #### Configure IAM Identity Center for Initial Access Before you can run Terraform to bootstrap the accounts, you need at least one admin user in IAM Identity Center with access to the Infrastructure account. This is a one-time manual setup. If you used Control Tower, IAM Identity Center is already enabled. Otherwise, enable it in your management account: 1. Navigate to [IAM Identity Center](https://console.aws.amazon.com/singlesignon/) in the AWS Console 2. Click **Enable** and choose your identity source (use the built-in Identity Center directory for simplicity) 1. Go to **Users** → **Add user** 2. Enter your email address and name 3. Complete the email verification process 1. Go to **Groups** → **Create group** 2. Name it `Admin` 3. Add your user to the group 1. Go to **Permission sets** → **Create permission set** 2. Choose **Predefined permission set** → **AdministratorAccess** 3. Set session duration (12 hours recommended for development) 4. Create the permission set 1. Go to **AWS accounts** 2. Select the **Infrastructure** account 3. Click **Assign users or groups** 4. Select the **Admin** group and the **AdministratorAccess** permission set You only need to do this for the Infrastructure account. Terraform will manage assignments to other accounts after bootstrapping. Go to **Settings** and note your **AWS access portal URL** (e.g., `https://d-xxxxxxxxxx.awsapps.com/start`). You'll need this to configure Leapp. After bootstrapping, the kit manages IAM Identity Center users, groups, and assignments via Terraform in the `user-management` stack. This initial manual setup just gets you access to run Terraform for the first time. For more details, see the [AWS IAM Identity Center documentation](https://docs.aws.amazon.com/singlesignon/latest/userguide/getting-started.html). ### GitHub Organization A GitHub organization where the repository will live. The CI/CD pipelines use GitHub Actions with OIDC authentication to AWS, and the Terraform GitHub provider manages org-level resources like team memberships. You need **Owner** permissions on the GitHub organization to complete the setup. This is required to: * Create and configure repositories * Install GitHub Apps (octo-sts for token management) * Manage organization members and teams * Configure repository secrets and OIDC settings ### Domain Name A domain name is required for ingress routing to your services (e.g., `api.example.com`). The kit creates Route53 hosted zones and uses external-dns to automatically manage DNS records. You can either: * **Register a new domain** through AWS Route53 or any registrar * **Use a subdomain** of an existing domain by pointing nameservers to the Route53 hosted zone ## Optional ### SigNoz Cloud Account [SigNoz](https://signoz.io/) provides observability (traces, metrics, logs). The kit includes pre-configured OpenTelemetry collectors that ship data to SigNoz Cloud. If you don't set up SigNoz, the observability components simply won't send data anywhere. The rest of the platform works fine without it. **NOTE:** other observability providers can also be swapped in instead of Signoz. ### Terramate Cloud Account [Terramate Cloud](https://terramate.io/) enhances the Terraform workflow with: * Drift detection dashboards * PR preview comments showing planned changes * Stack health monitoring The kit works without Terramate Cloud; you just won't get the cloud features. The CLI orchestration still functions locally and in CI. # Repository Setup Source: https://kubestarterkit.com/usage/getting-started/02-repository-setup Fork and customize the repository for your organization ## Overview Kube Starter Kit is designed to be forked and customized for your organization. This page walks you through the initial repository setup: forking, cloning, and installing development tools. ## Fork and Clone Fork the repository to your GitHub organization: 1. Navigate to the [Kube Starter Kit repository](https://github.com/DevOps-Directive/kube-starter-kit) 2. Click **Fork** and select your organization as the owner 3. Keep the repository name or rename it to match your conventions ```bash theme={null} git clone "https://github.com//.git" cd "" ``` The repository uses [mise](https://mise.jdx.dev/) to manage tool versions. Install it first: ```bash theme={null} curl https://mise.run | sh ``` Then activate mise in your shell (add to your `.bashrc` or `.zshrc` for persistence): ```bash theme={null} eval "$(~/.local/bin/mise activate bash)" # or zsh/fish ``` See the [mise installation docs](https://mise.jdx.dev/getting-started.html) for alternative installation methods. With mise installed, install the project's tool dependencies: ```bash theme={null} mise install ``` This installs Terramate, Terraform, kubectl, Helm, and other tools at the versions specified in `mise.toml`. ## Next Steps You're now ready to proceed to [Bootstrap Accounts](/usage/getting-started/03-bootstrap-accounts) to set up AWS IAM roles and import GitHub organization members. # Bootstrap Accounts Source: https://kubestarterkit.com/usage/getting-started/03-bootstrap-accounts Set up AWS IAM roles and import GitHub organization members ## Overview Before deploying infrastructure via CI/CD, you need to bootstrap your AWS accounts and GitHub organization for Terraform management. This is a one-time setup that creates the foundation for all automation. ### Decisions This setup step requires deciding the following: #### Namespace The **namespace** is a short prefix (3-5 characters) used to generate unique names for all AWS resources: S3 buckets, IAM roles, EKS clusters, etc. Choose something that identifies your organization. | Examples | Description | | -------- | ------------------------- | | `acme` | Company name abbreviation | | `myco` | Short identifier | | `xyz` | Project code | The default is `ksk` ("Kube Starter Kit"). You'll use this namespace consistently across all configuration, it cannot be easily changed later. This "namespace" refers to the naming convention from [Cloud Posse's terraform-null-label](https://github.com/cloudposse/terraform-null-label), **not** a Kubernetes namespace. #### Primary AWS Region Choose a primary AWS region for your infrastructure. This region will host: * The Terraform state S3 bucket * Your EKS clusters (staging and production) * Most other AWS resources The default is `us-east-2`. Consider factors like latency to your users, service availability, and pricing when choosing. You can deploy to multiple regions later, but the state bucket region cannot be changed without migrating state. ### How Cross-Account Access Works The **Infrastructure account** is the central hub for all Terraform operations. There are two access paths: 1. **CI/CD (GitHub Actions)**: Authenticates via OIDC, then assumes roles in target accounts 2. **Human admins (IAM Identity Center)**: Authenticates via SSO to the Infrastructure account, then assumes roles in target accounts ``` GitHub Actions Human Admin (You) │ │ │ OIDC │ IAM Identity Center (SSO) ▼ ▼ ┌─────────────────────────────────────────────────────┐ │ Infrastructure Account │ │ │ │ ┌─────────────────┐ ┌────────────────────┐ │ │ │ GitHub OIDC │ │ SSO Admin Role │ │ │ │ Role │ │ (via Leapp) │ │ │ └─────────────────┘ └────────────────────┘ │ │ \ / │ │ ▼ ▼ │ │ ┌──────────────────────┐ │ │ │ Terraform State │ │ │ │ Bucket (S3) │ │ │ └──────────────────────┘ │ └─────────────────────────────────────────────────────┘ │ │ │ \ │ sts:AssumeRole (cross-account) │ \ ▼ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ Management │ │ ECR │ │ Staging │ │ Production │ │ Account │ │ Account │ │ Account │ │ Account │ │ ------------ │ │ ------------ │ │ ------------ │ │ ------------ │ │ IAM Role │ │ IAM Role │ │ IAM Role │ │ IAM Role │ │ (trusts │ │ (trusts │ │ (trusts │ │ (trusts │ │ infra) │ │ infra) │ │ infra) │ │ infra) │ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ ``` **For CI/CD (GitHub Actions):** 1. GitHub Actions authenticates via OIDC to the GitHub OIDC role in the Infrastructure account 2. That role assumes target account roles via cross-account IAM trust policies 3. Terraform runs with credentials for the target account, state stored in Infrastructure account **For Human Admins:** 1. Admin authenticates to the Infrastructure account via IAM Identity Center (using Leapp) 2. The SSO role in Infrastructure account can assume target account roles 3. Admin runs Terraform locally with the same cross-account access as CI/CD This pattern keeps credentials management simple while maintaining proper account isolation. ## What Gets Created ### Infrastructure Account (Central Hub) | Resource | Purpose | | -------------------- | ------------------------------------------------------------------------- | | S3 bucket | Stores Terraform state for **all accounts** with versioning enabled | | GitHub OIDC provider | Enables keyless authentication from GitHub Actions | | GitHub OIDC IAM role | Role that GitHub Actions assumes; can then assume roles in other accounts | ### Each Target Account (Management, ECR, Staging, Production) | Resource | Purpose | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Terraform IAM role | Admin role with trust policy allowing **both** the GitHub OIDC role (for CI/CD) **and** SSO admin role (for human admins) from the Infrastructure account to assume it | | Route53 hosted zone | DNS zone for the environment (e.g., `staging.example.com`); not created in Management or ECR accounts | ## Prerequisites ### Required Accounts Based on the [account structure](/features/03-aws-architecture#account-structure), you need these AWS accounts: | Account | Purpose | | ------------------ | ----------------------------------------------- | | **Management** | AWS Organizations, IAM Identity Center | | **Infrastructure** | Terraform state, GitHub OIDC, CI/CD automation | | **ECR** | Container registry (shared across environments) | | **Staging** | Staging environment resources | | **Production** | Production environment resources | ## Bootstrapping Overview Bootstrapping solves a chicken-and-egg problem: Terraform needs IAM roles and an S3 bucket to run, but we want Terraform to manage those resources. The solution is to manually create minimal resources, then let Terraform import and manage them. Each account needs a bootstrap IAM role. The Infrastructure account additionally needs an S3 bucket for Terraform state. The bootstrap scripts in `terraform/bootstrap/` must be run manually with AWS CLI credentials before Terraform can take over. ## Configure Leapp CLI Before you can authenticate to AWS accounts, configure the Leapp CLI with your IAM Identity Center portal: ```bash theme={null} leapp integration create \ --integrationType AWS-SSO \ --integrationAlias "My Organization" \ --integrationPortalUrl https://d-xxxxxxxxxx.awsapps.com/start \ --integrationRegion ``` Replace the portal URL with your IAM Identity Center URL (found in the AWS IAM Identity Center console under **Settings > Identity source**) and `` with your [primary region](#primary-aws-region). For a GUI experience, [Leapp](https://www.leapp.cloud/) provides a desktop app for managing AWS SSO sessions. It discovers available accounts and permission sets automatically from your configured integration. ## Bootstrap the Infrastructure Account The Infrastructure account is special, it hosts the S3 state bucket that all other accounts depend on. Start a Leapp session for the Infrastructure account: ```bash theme={null} leapp session start "Infrastructure" aws sts get-caller-identity ``` You'll need this ARN for cross-account trust policies: ```bash theme={null} mise run //terraform/bootstrap:get-sso-role-arn ``` Save this ARN, it looks like: ``` arn:aws:iam::INFRA_ACCOUNT_ID:role/aws-reserved/sso.amazonaws.com/REGION/AWSReservedSSO_AdministratorAccess_XXXXX ``` ```bash theme={null} mise run //terraform/bootstrap:create-state-bucket \ --bucket-name -gbl-infra-bootstrap-state \ --aws-region ``` S3 bucket names are globally unique. Replace `` with your [namespace](#namespace) and `` with your [primary region](#primary-aws-region). Edit `terraform/config.tm.hcl` with your namespace and account details: ```hcl theme={null} globals { namespace = "" # Your chosen namespace # Your SSO admin role in Infrastructure account (for local Terraform runs) sso_admin_assume_role_arn = "arn:aws:iam:::role/aws-reserved/sso.amazonaws.com//AWSReservedSSO_AdministratorAccess_XXXXX" # GitHub OIDC role (will be created by Terraform) github_oidc_assume_role_arn = "arn:aws:iam:::role/-gbl-infra-bootstrap-github-oidc" # S3 backend configuration (must match the bucket you created above) backend_bucket = "-gbl-infra-bootstrap-state" backend_region = "" } ``` Replace the placeholders with your values from [Decisions](#decisions). Propagate the configuration changes to generated files: ```bash theme={null} cd terraform terramate generate ``` This imports the S3 bucket and creates the GitHub OIDC provider and role: ```bash theme={null} cd terraform terramate run --tags infra:bootstrapping --parallel 1 -- terraform init terramate run --tags infra:bootstrapping -- terraform apply ``` ## Bootstrap Target Accounts Each target account (Management, ECR, Staging, Production) needs an IAM role that can be assumed from the Infrastructure account. ### Manual Role Creation For each account: ```bash theme={null} leapp session start "Staging" # or Management, ECR, Production aws sts get-caller-identity ``` ```bash theme={null} cd terraform/bootstrap mise run create-terraform-iam-role-in-target-account \ --role-name \ --infra-sso-role-arn "" ``` Replace `` with the SSO role ARN you recorded during [Bootstrap the Infrastructure Account](#bootstrap-the-infrastructure-account). Use the appropriate role name for each account: | Account | Role Name | | ---------- | ----------------------------------------- | | Management | `-gbl-mgmt-bootstrap-admin` | | ECR | `-gbl-ecr-bootstrap-admin` | | Staging | `-gbl-staging-bootstrap-admin` | | Production | `-gbl-prod-bootstrap-admin` | This creates an IAM role with: * `AdministratorAccess` policy attached * Trust policy allowing your SSO role from the Infrastructure account to assume it Repeat these steps for each target account. ### Terraform Takeover Once the manual roles exist, Terraform can import and manage them. Terramate generates an `import` block in each root module to bring the manually-created role into Terraform state. #### How Import Works The Terramate template in `terraform/imports/mixins/modules/bootstrapping.tm.hcl` generates both the import block and module call for each bootstrapping stack: ```hcl theme={null} # Generated in each root module (e.g., terraform/live/staging/global/bootstrapping/_main.tf) import { to = module.bootstrapping.module.iam_role.aws_iam_role.this[0] id = "ksk-gbl-staging-bootstrap-admin" # Uses globals: namespace-environment-stage } module "bootstrapping" { source = "../../../../modules/account-bootstrapping" # ... } ``` The import block ID is constructed from your configured globals (`namespace`, `environment`, `stage`) to match the role name you created manually (e.g., `"-gbl-staging-bootstrap-admin"`). Import blocks must be in root modules, not child modules. This is a Terraform requirement. The `account-bootstrapping` module itself does not contain the import block; it's generated by Terramate at the root module level. When you run `terraform apply`, Terraform: 1. Imports the existing IAM role into state (instead of trying to create it) 2. Updates the role's trust policy to allow **both** your SSO role **and** the GitHub OIDC role to assume it This enables CI/CD pipelines to manage infrastructure going forward. #### Apply the Bootstrapping Stacks ```bash theme={null} cd terraform # Log back into Infrastructure account leapp session start "Infrastructure" # Apply each account's bootstrapping stack terramate run --tags management:bootstrapping -- terraform init terramate run --tags management:bootstrapping -- terraform apply terramate run --tags ecr:bootstrapping -- terraform init terramate run --tags ecr:bootstrapping -- terraform apply terramate run --tags staging:bootstrapping -- terraform init terramate run --tags staging:bootstrapping -- terraform apply terramate run --tags prod:bootstrapping -- terraform init terramate run --tags prod:bootstrapping -- terraform apply ``` The `import` block was added in Terraform 1.5. It allows declarative imports without running `terraform import` commands manually. See the [Terraform import block documentation](https://developer.hashicorp.com/terraform/language/block/import) for more details. ## Configure Domain Nameservers The bootstrapping stacks create Route53 hosted zones for Staging and Production (e.g., `staging.example.com`, `prod.example.com`). For DNS to work, you must configure your domain registrar to use the Route53 nameservers. After applying the bootstrapping stacks, retrieve the nameservers for each hosted zone: ```bash theme={null} # Get staging nameservers terramate run --tags staging:bootstrapping -- terraform output hosted_zone_nameservers # Get production nameservers terramate run --tags prod:bootstrapping -- terraform output hosted_zone_nameservers ``` You'll see 4 nameservers like: ``` ns-123.awsdns-45.com ns-678.awsdns-90.net ns-111.awsdns-22.org ns-333.awsdns-44.co.uk ``` How you configure nameservers depends on your setup: **If using a subdomain** (e.g., `staging.example.com`): * Add NS records in your parent domain's DNS pointing to the Route53 nameservers * Example: Add NS records for `staging` subdomain pointing to the 4 nameservers above **If using a dedicated domain** (e.g., `example-staging.com`): * Update the domain's nameservers at your registrar (Namecheap, GoDaddy, Route53, etc.) * Replace the default nameservers with the 4 Route53 nameservers DNS changes can take up to 48 hours to propagate, but usually complete within minutes. Verify with: ```bash theme={null} dig NS staging.example.com +short ``` You should see the Route53 nameservers in the response. If you skip this step, external-dns and cert-manager will not work. DNS records created in Route53 won't resolve, and Let's Encrypt DNS-01 challenges will fail. ## Troubleshooting ### "Bucket already exists" error S3 bucket names are globally unique. If the bucket name is taken: 1. Choose a different name with your organization prefix 2. Update `backend_bucket` in `terraform/config.tm.hcl` 3. Update any hardcoded references in bootstrapping stacks ### State file in wrong location All Terraform state is stored in the Infrastructure account's S3 bucket, regardless of which account the resources are in. If you see state errors: 1. Verify `backend_bucket` and `backend_region` in `terraform/config.tm.hcl` 2. Ensure the GitHub OIDC role has S3 permissions in the Infrastructure account 3. Check that the bucket exists and has the expected state files ## Next Steps With accounts bootstrapped, proceed to [Configure Access](/usage/getting-started/04-configure-access) to set up CI/CD and user access for GitHub and AWS. # Configure Access Source: https://kubestarterkit.com/usage/getting-started/04-configure-access Set up user access for GitHub and AWS ## Overview After bootstrapping accounts, configure access for team members. This page covers: * IAM Identity Center users and groups * GitHub organization membership The user performing the bootstrap already exists in IAM Identity Center and GitHub. You must import these existing resources before Terraform can manage them (import instructions below). ## Configure User Management The kit manages both AWS IAM Identity Center and GitHub organization membership from a single `users.yaml` file via Terraform. ### Authenticate with GitHub CLI The Terraform GitHub provider uses credentials from the GitHub CLI. Authenticate with an account that has **Owner** permissions on your organization: ```bash theme={null} gh auth login ``` Follow the prompts to authenticate. Verify you have the necessary permissions: ```bash theme={null} gh api orgs//memberships/$( gh api user --jq '.login' ) --jq '.role' ``` This should return `admin`. ### Add Users Add users to `terraform/live/shared/global/user-management/data/users.yaml`: ```yaml theme={null} users: - github: username: your-github-username role: admin teams: Admin: role: maintainer aws: user_name: you@example.com email: you@example.com group_membership: [Admin] given_name: Your family_name: Name ``` This file is the single source of truth for both GitHub and AWS access. Import your existing IAM Identity Center user, groups, and GitHub membership. Add import blocks to `terraform/live/shared/global/user-management/imports.tf`: ```hcl theme={null} # IAM Identity Center user import { to = module.aws-iam-identity-center.aws_identitystore_user.sso_users["you@example.com"] id = "/" } # IAM Identity Center group (if it exists) import { to = module.aws-iam-identity-center.aws_identitystore_group.sso_groups["Admin"] id = "/" } # IAM Identity Center group membership import { to = module.aws-iam-identity-center.aws_identitystore_group_membership.sso_group_membership["you@example.com_Admin"] id = "/" } # GitHub organization member import { to = module.github_membership.github_membership.this["your-github-username"] id = ":your-github-username" } ``` To find the required AWS IDs: ```bash theme={null} # Get the Identity Store ID aws sso-admin list-instances --query 'Instances[0].IdentityStoreId' --output text # List users aws identitystore list-users --identity-store-id # List groups aws identitystore list-groups --identity-store-id # Get a group membership ID aws identitystore get-group-membership-id \ --identity-store-id \ --group-id \ --member-id UserId= ``` To find existing GitHub members: ```bash theme={null} gh api orgs//members --jq '.[].login' ``` ```bash theme={null} cd terraform terramate run --tags user-management -- terraform init terramate run --tags user-management -- terraform apply ``` This creates/imports: * IAM Identity Center users * Group memberships (Admin, PowerUser, ReadOnly) * Permission set assignments to all accounts * GitHub organization memberships ## Verify Access Configuration Before proceeding, verify that your existing users and groups are correctly configured in both AWS and GitHub. ### Review IAM Identity Center 1. Open the [IAM Identity Center console](https://console.aws.amazon.com/singlesignon/home) in your Management account 2. Navigate to **Users** and verify your bootstrap user exists 3. Navigate to **Groups** and verify the expected groups exist (Admin, PowerUser, ReadOnly) 4. Check group memberships by clicking on each group ### Review GitHub Organization 1. Open your GitHub organization's **People** page: `https://github.com/orgs//people` 2. Verify your bootstrap user appears as an **Owner** 3. Check the **Invitations** tab for pending invites; users who haven't accepted their invitation won't appear in the members list Users with pending invitations only appear under the **Invitations** tab, not in the main members list. Make sure to check both when verifying membership. ## Groups and Permission Sets The kit configures three groups with corresponding permission sets: | Group | Permission Set | Access Level | | --------- | ------------------- | --------------------------- | | Admin | AdministratorAccess | Full access to all accounts | | PowerUser | PowerUserAccess | Full access except IAM | | ReadOnly | ViewOnlyAccess | Read-only access | ## Next Steps With access configured, proceed to [Configure Integrations](/usage/getting-started/05-configure-integrations) to set up external service integrations for CI/CD. # Configure Integrations Source: https://kubestarterkit.com/usage/getting-started/05-configure-integrations Configure AWS, GitHub, and Terramate Cloud integrations ## Overview With [human access configured](/usage/getting-started/04-configure-access), you now need to configure the external service integrations that enable CI/CD automation. This page covers: * **AWS**: Account IDs and role ARNs for Terraform * **GitHub (octo-sts)**: Tokens for managing GitHub organization resources * **Terramate Cloud** (optional): Plan visualization and drift detection ## Verify Terraform Configuration Your `terraform/config.tm.hcl` should already have the correct values from the [Bootstrap Accounts](/usage/getting-started/03-bootstrap-accounts) step. Verify they look like this: ```hcl theme={null} globals { namespace = "" # These were set during bootstrap github_oidc_assume_role_arn = "arn:aws:iam:::role/-gbl-infra-bootstrap-github-oidc" sso_admin_assume_role_arn = "arn:aws:iam:::role/aws-reserved/sso.amazonaws.com//AWSReservedSSO_AdministratorAccess_XXXXX" backend_bucket = "-gbl-infra-bootstrap-state" backend_region = "" } ``` ## Configure GitHub Repository Variables GitHub Actions needs the OIDC role ARN to authenticate. Add these variables to your repository (Settings > Secrets and variables > Actions > Variables): | Variable | Value | | ------------------------ | ---------------------------------------------------------------------------------- | | `TERRAFORM_AWS_ROLE_ARN` | `arn:aws:iam:::role/-gbl-infra-bootstrap-github-oidc` | | `TERRAFORM_AWS_REGION` | Your primary AWS region (e.g., `us-east-2`) | These are repository **variables**, not secrets, since the values are not sensitive. GitHub Actions only needs the Infrastructure account role. Cross-account access is handled by each Terraform stack's provider configuration. ## Update GitHub Workflows The workflows read credentials from GitHub repository variables. Verify the workflow files reference the variables correctly: ```yaml theme={null} # In .github/workflows/terramate-preview.yml, terramate-deploy.yml, and terramate-detect-drift.yml - name: "Configure AWS Credentials" uses: aws-actions/configure-aws-credentials@v4 with: aws-region: ${{ vars.TERRAFORM_AWS_REGION }} role-to-assume: ${{ vars.TERRAFORM_AWS_ROLE_ARN }} ``` ## Configure octo-sts for GitHub Tokens The Terramate workflows use [octo-sts](https://github.com/octo-sts/app) to obtain GitHub tokens for managing GitHub organization resources (teams, members, repository settings). This is more secure than storing long-lived GitHub tokens as secrets. 1. Navigate to the [octo-sts GitHub App](https://github.com/apps/octo-sts) 2. Click **Install** and select your organization 3. Grant access to your forked repository (or all repositories) Update each policy file in `.github/chainguard/` to reference your organization: **`.github/chainguard/terramate.sts.yaml`**: ```yaml theme={null} issuer: https://token.actions.githubusercontent.com subject_pattern: "^repo:/:.*" claim_pattern: workflow_ref: '^//\.github/workflows/terramate.*@refs/.*$' permissions: administration: write metadata: read members: write contents: read ``` **`.github/chainguard/release-please.sts.yaml`**: ```yaml theme={null} issuer: https://token.actions.githubusercontent.com subject: "repo:/:ref:refs/heads/main" permissions: contents: write pull_requests: write ``` Replace `` with your GitHub organization name and `` with your repository name. ## Configure Terramate Cloud (Optional) If you want to use [Terramate Cloud](https://cloud.terramate.io/) for plan visualization and drift detection: 1. Create an account at [cloud.terramate.io (EU)](https://cloud.terramate.io/) OR [us.cloud.terramate.io (US)](https://us.cloud.terramate.io/) 2. Install the GitHub App from your Terramate Cloud dashboard under **Integrations** to enable PR comments and status checks 3. Update the `cloud_organization` in `.github/workflows/terramate-preview.yml` and other Terramate workflows: ```yaml theme={null} - name: Install Terramate uses: terramate-io/terramate-action@0500f8a40b57a793a41edd2aea0a49e31c7204e8 # v3.2.0 with: version: "0.15.1" cloud_organization: your-terramate-org ``` Terramate Cloud is optional but recommended. It provides a unified view of Terraform plans across all stacks and makes PR reviews much easier. ## Search for Remaining References Search for any remaining references to the original organization or AWS accounts: ```bash theme={null} # Find references to the original organization grep -r "DevOps-Directive" .github/ grep -r "094905625236" . # Original AWS account ID ``` ## Verify Your Setup ```bash theme={null} cd terraform terramate fmt --check terramate generate ``` Review the generated files to ensure your configuration changes propagated correctly. ## Commit Your Changes ```bash theme={null} git add . git commit -m "chore: configure AWS account IDs and role ARNs" git push ``` ## Verify CI/CD Push a commit or open a pull request to verify that GitHub Actions can authenticate to AWS: 1. Check the workflow run in the **Actions** tab 2. The `Configure AWS Credentials` step should succeed 3. Terraform plan output should appear (if using Terramate Cloud, check the PR comments) ## Troubleshooting ### "Access Denied" when assuming cross-account role The two-step authentication means there are two places trust can fail: 1. **GitHub → Infrastructure account**: Check the OIDC role trust policy ```bash theme={null} # Run from Infrastructure account (replace with your namespace) aws iam get-role --role-name "-gbl-infra-bootstrap-github-oidc" \ --query 'Role.AssumeRolePolicyDocument' ``` 2. **Infrastructure account → Target account**: Check the target role trust policy ```bash theme={null} # Run from target account (e.g. staging) aws iam get-role --role-name "-gbl-staging-bootstrap-admin" \ --query 'Role.AssumeRolePolicyDocument' ``` The target account's trust policy should include the Infrastructure account's GitHub OIDC role ARN as a trusted principal. ### GitHub Actions can't authenticate 1. Verify the OIDC provider exists in the Infrastructure account: ```bash theme={null} aws iam list-open-id-connect-providers ``` 2. Check the GitHub OIDC role trust policy allows your repository: ```bash theme={null} # Replace with your namespace aws iam get-role --role-name "-gbl-infra-bootstrap-github-oidc" \ --query 'Role.AssumeRolePolicyDocument' ``` 3. Ensure the repository name in the trust policy matches exactly (case-sensitive, including organization name). ## Next Steps With AWS integration configured, you're ready to [Deploy Infrastructure](/usage/getting-started/06-deploy-infrastructure) to provision networking and EKS clusters. # Deploy Infrastructure Source: https://kubestarterkit.com/usage/getting-started/06-deploy-infrastructure Deploy networking, EKS, and application resources via Terraform ## Overview With accounts bootstrapped and integrations configured, you're ready to deploy the core AWS infrastructure. This includes networking (VPC, subnets, NAT), EKS clusters, and application-specific resources like S3 buckets and IAM roles. ### Decisions Before deploying, review and customize these configuration options in each stack's `config.tm.hcl`: #### Networking | Setting | Location | Default | Description | | -------------------- | ------------------------------------------------------ | --------------- | ------------------------------------------------------------------------------------------------------------------------- | | VPC CIDR | `globals.networking.vpc_cidr` | `10.0.0.0/16` | IP address range for the VPC. Use non-overlapping ranges if deploying multiple VPCs or connecting to on-premise networks. | | NAT mode | `globals.networking.nat_mode` | `fck_nat` | How private subnets access the internet. See [NAT Gateway Modes](#nat-gateway-modes) below. | | Bastion host | `globals.networking.enable_bastion` | `true` | Whether to create a bastion host for SSH/SSM access to private resources. | | PlanetScale endpoint | `globals.networking.planetscale_endpoint_service_name` | Region-specific | VPC endpoint for PlanetScale private connectivity. Remove if not using PlanetScale. | #### EKS Cluster | Setting | Location | Default | Description | | ------------------ | ------------------------------------------------ | -------------------- | ---------------------------------------------------------------------------------------------------------- | | Kubernetes version | `globals.eks.kubernetes_version` | `1.34` | EKS control plane version. Update when upgrading clusters. | | Node group version | `globals.eks.base_node_group_kubernetes_version` | `1.34` | Can lag control plane during rolling upgrades. | | Public endpoint | `globals.eks.endpoint_public_access` | `false` | Whether the API server is publicly accessible. Set `false` for private-only access (requires bastion/VPN). | | Private endpoint | `globals.eks.endpoint_private_access` | `true` | Whether the API server is accessible from within the VPC. | | ArgoCD hostname | `globals.eks.argocd_hostname` | Environment-specific | FQDN for ArgoCD (used for webhook configuration). | #### Hardcoded Values You May Want to Customize These values are set in the Terraform modules and require editing the module source code to change: | Setting | Location | Default | Description | | ------------------------------ | ------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------ | | Availability zones | `terraform/modules/*/main.tf` | First 3 AZs | Number of AZs used for subnets. Currently hardcoded to 3. | | Base node group sizing | `terraform/modules/eks/main.tf` | 2-3 nodes | `min_size`, `max_size`, `desired_size` for the managed node group. | | Base node group instance types | `terraform/modules/eks/variables.tf` | `t3.large` | Instance types for the initial managed node group. | | Base node group AMI | `terraform/modules/eks/variables.tf` | `AL2023_x86_64_STANDARD` | AMI type (AL2023, Bottlerocket, etc.). | | fck-nat instance type | `terraform/modules/networking/variables.tf` | `t4g.nano` | Instance size for fck-nat NAT instances. | | Bastion instance type | `terraform/modules/networking/variables.tf` | `t4g.nano` | Instance size for the bastion host. | | EKS addon versions | `terraform/modules/eks/variables.tf` | Pinned versions | Versions for CoreDNS, VPC CNI, kube-proxy, EBS CSI driver, Pod Identity agent. | | SSO admin role ARN | `terraform/modules/eks/variables.tf` | Hardcoded | IAM Identity Center role granted cluster admin access. | ## Infrastructure Deployment Order Terramate manages dependencies between stacks automatically. The deployment order is: ``` 1. Networking → VPC, subnets, NAT gateway, bastion host 2. EKS → Kubernetes cluster, node groups, Karpenter 3. App Resources → S3 buckets, IAM roles for workloads ``` Each stack declares its dependencies, so Terramate applies them in the correct order. ## Initial Deployment (Local) For the first deployment, you must run Terraform locally. The CI/CD workflow only applies *changed* stacks, and new stacks without any Terraform state aren't detected as changed. Terramate can deploy all stacks at once with automatic dependency ordering (`terramate run --tags staging -- terraform apply`). However, deploying stacks sequentially makes it easier to follow progress, verify each component is working, and troubleshoot issues. Use Leapp to start a session for the Infrastructure account: ```bash theme={null} leapp session start "Infrastructure" # Verify you're authenticated aws sts get-caller-identity ``` ```bash theme={null} cd terraform export REGION="us-east-2" # Your AWS region export STAGE="staging" # or "prod" # Initialize the networking stack terramate run --tags ${STAGE}:${REGION}:networking -- terraform init # Preview changes terramate run --tags ${STAGE}:${REGION}:networking -- terraform plan # Apply terramate run --tags ${STAGE}:${REGION}:networking -- terraform apply ``` After networking is complete: ```bash theme={null} # Initialize the EKS stack terramate run --tags ${STAGE}:${REGION}:eks -- terraform init # Preview and apply terramate run --tags ${STAGE}:${REGION}:eks -- terraform plan terramate run --tags ${STAGE}:${REGION}:eks -- terraform apply ``` EKS cluster creation takes 10-15 minutes. If you have application-specific infrastructure: ```bash theme={null} terramate run --tags ${STAGE}:${REGION}:services -- terraform init terramate run --tags ${STAGE}:${REGION}:services -- terraform apply ``` After successful deployment, commit any updated `.terraform.lock.hcl` files: ```bash theme={null} git add terraform/live/**/.terraform.lock.hcl git commit -m "chore: update terraform lock files after initial deployment" git push ``` ## Subsequent Changes via Pull Request After the initial deployment, use pull requests for all infrastructure changes. This ensures changes are reviewed and tracked. Edit the relevant `config.tm.hcl` or module files, then regenerate: ```bash theme={null} cd terraform terramate generate ``` ```bash theme={null} git checkout -b infra/update-eks-version git add . git commit -m "chore: upgrade EKS to 1.34" git push -u origin infra/update-eks-version ``` The CI workflow will: 1. Run `terraform plan` for each changed stack 2. Post plan output to Terramate Cloud (if configured) 3. Post a plan summary as a PR comment (if using Terramate Cloud) Review the plan carefully before approving. Once approved, merge the PR. The deploy workflow will apply changes in dependency order. ## What Gets Created ### Networking Stack | Resource | Description | | --------------- | ------------------------------------------------------------------------- | | VPC | Isolated network with configurable CIDR (default: `10.0.0.0/16`) | | Public subnets | 3 subnets across availability zones for load balancers | | Private subnets | 3 subnets for EKS nodes and workloads + bastion | | NAT gateway | Internet access for private subnets (fck-nat by default for cost savings) | | S3 VPC endpoint | Free gateway endpoint for S3 access without NAT | | Bastion host | EC2 instance for SSH tunneling to private resources | ### EKS Stack | Resource | Description | | ----------------------- | ----------------------------------------------- | | EKS cluster | Managed Kubernetes control plane | | Managed node group | Initial nodes for system workloads | | Karpenter Prerequisites | Autoscaler for dynamic node provisioning | | EBS CSI driver | Persistent volume support with encryption | | Pod Identity | AWS IAM integration for workload authentication | | CoreDNS, kube-proxy | Essential cluster add-ons | ### Application Resources (go-backend example) | Resource | Description | | ------------------------ | ----------------------------------------------- | | S3 bucket | Application-specific storage | | IAM role | Pod Identity role for AWS API access | | Pod Identity association | Links the IAM role to Kubernetes ServiceAccount | ## Configuration Options ### NAT Gateway Modes The networking module supports three NAT modes via `nat_mode` variable: | Mode | Cost | Availability | Use Case | | ------------------------ | ------------------ | ----------------------- | ------------------------------------------------ | | `fck_nat` | \~\$5/month per AZ | HA with auto-failover | Development, staging | | `single_nat_gateway` | \~\$45/month | Single point of failure | Cost-sensitive production | | `one_nat_gateway_per_az` | \~\$135/month | Full HA | Production with strict availability requirements | Configure in `terraform/live/staging//networking/config.tm.hcl`: ```hcl theme={null} globals { nat_mode = "fck_nat" # or "single_nat_gateway" or "one_nat_gateway_per_az" } ``` ### EKS Node Configuration Karpenter handles most node provisioning, but you can configure the initial managed node group: ```hcl theme={null} globals { eks_managed_node_groups = { system = { instance_types = ["m6i.large"] min_size = 2 max_size = 4 desired_size = 2 } } } ``` ## Verify Deployment After deployment completes: ```bash theme={null} terramate run --tags ${STAGE}:${REGION}:eks -- terraform output ``` Note the `cluster_name` output; you'll need it for later steps. Navigate to the [EKS console](https://console.aws.amazon.com/eks) and verify: * Cluster status is `Active` * Node group shows nodes in `Ready` state * Add-ons (CoreDNS, kube-proxy, VPC CNI, EBS CSI) are `Active` If you configured private-only endpoint access (`endpoint_public_access = false`), you cannot run `kubectl` commands from your local machine without first connecting through the bastion host. Console verification is sufficient for now; you'll configure cluster access via ArgoCD in the next step. ## Deploy Production Production deployment follows the same pattern with production-specific configuration: ```bash theme={null} # List production stacks terramate list --tags prod:infrastructure # Deploy locally (replace with your region) terramate run --tags prod:${REGION}:networking -- terraform apply terramate run --tags prod:${REGION}:eks -- terraform apply ``` ## Next Steps With infrastructure deployed, proceed to [Cluster Access](/usage/getting-started/07-cluster-access) to configure kubectl access to your EKS cluster. # Set Up Cluster Access Source: https://kubestarterkit.com/usage/getting-started/07-cluster-access Connect to your EKS clusters via bastion and SOCKS proxy ## Overview EKS clusters in Kube Starter Kit are configured with private API endpoints by default for security. This means you can't access the Kubernetes API directly from the internet; you need to go through the bastion host using a SOCKS5 proxy. This page covers: * Setting up SSH over AWS SSM Session Manager * Connecting to the cluster via SOCKS proxy * Configuring kubectl for persistent proxy access If you prefer simpler access, you can enable the public API endpoint by setting `endpoint_public_access = true` in the EKS configuration (see [Deploy Infrastructure - EKS Cluster](/usage/getting-started/06-deploy-infrastructure#eks-cluster)). With a public endpoint, you can run `aws eks update-kubeconfig` and use kubectl directly without a proxy. However, this exposes your Kubernetes API to the internet, while still protected by IAM authentication, it increases your attack surface and may not meet compliance requirements. ## Architecture ``` Your Machine AWS VPC ┌───────────┐ ┌───────────────────────────┐ │ │ │ │ │ kubectl │───SOCKS5─────>│ Bastion Host │ │ │ proxy │ (private subnet) │ └───────────┘ │ │ │ │ │ ▼ │ │ SSM Session │ ┌───────────────┐ │ └────────────────────>│ │ EKS API │ │ (via AWS APIs) │ │ (private) │ │ │ └───────────────┘ │ └───────────────────────────┘ ``` The bastion host: * Lives in a private subnet (no public IP) * Uses AWS SSM Session Manager for access (no SSH keys to manage) * Acts as a [SOCKS5](https://en.wikipedia.org/wiki/SOCKS) proxy for kubectl traffic ## One-Time Setup If you haven't already, run `mise install` to install the required tools (AWS CLI, Session Manager plugin, kubectl). ### Configure SSH for SSM Add the SSM proxy configuration to your SSH config: ```bash theme={null} # View the required config mise run //tools:bastion:setup-ssh-config ``` Add this to `~/.ssh/config`: ``` # AWS SSM Session Manager SSH proxy Host i-* mi-* User ec2-user ProxyCommand sh -c "aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'" ``` This allows SSH to instances via SSM using the instance ID as the hostname. ## Connect to the Cluster Start a Leapp session for the target account: ```bash theme={null} leapp session start "Staging" ``` Verify authentication: ```bash theme={null} aws sts get-caller-identity ``` Get the cluster credentials: ```bash theme={null} mise run //tools:eks:get-credentials {cluster-name} ``` Or manually: ```bash theme={null} export REGION="us-east-2" # Your AWS region aws eks update-kubeconfig \ --name ${CLUSTER_NAME} \ --region ${REGION} \ --alias staging ``` In a **separate terminal** (with the same Leapp session active), start the proxy: ```bash theme={null} mise run //tools:eks:connect staging ``` This automatically looks up the bastion instance and starts a SOCKS5 proxy on `localhost:1080`. Keep this terminal open while accessing the cluster. The task automatically pushes your SSH public key via EC2 Instance Connect (valid for 60 seconds) before establishing the SSH tunnel. **Option A: Per-command (temporary)** ```bash theme={null} HTTPS_PROXY=socks5://localhost:1080 kubectl get nodes ``` **Option B: Update kubeconfig (persistent)** ```bash theme={null} kubectl config set-cluster {cluster-name} --proxy-url=socks5://localhost:1080 ``` Now kubectl commands work without the environment variable: ```bash theme={null} kubectl get nodes kubectl get pods -A ``` ## Configure Persistent Access To avoid passing the proxy URL each time, configure it in your kubeconfig: Set the proxy for a specific cluster context: ```bash theme={null} kubectl config set-cluster staging --proxy-url=socks5://localhost:1080 ``` This modifies `~/.kube/config` to include the proxy URL for that cluster. Set the proxy globally for all kubectl commands: ```bash theme={null} export HTTPS_PROXY=socks5://localhost:1080 kubectl get nodes ``` Add to your shell profile for persistence (but note this affects all HTTPS traffic). ## Production Access The same process applies to production: ```bash theme={null} # Authenticate to production leapp session start "Production" # Update kubeconfig mise run //tools:eks:get-credentials {cluster-name} # Start proxy (in separate terminal) mise run //tools:eks:connect production # Configure persistent proxy kubectl config set-cluster {cluster-name} --proxy-url=socks5://localhost:1080 # Use kubectl kubectl get nodes ``` For production access, consider implementing additional access controls: * Require MFA for SSM sessions * Use AWS CloudTrail to audit access * Implement just-in-time access with temporary permissions ## Next Steps With cluster access configured, proceed to [Deploy Kubernetes Baseline](/usage/getting-started/08-deploy-kubernetes-baseline) to bootstrap ArgoCD and deploy infrastructure components. # Deploy Kubernetes Baseline Source: https://kubestarterkit.com/usage/getting-started/08-deploy-kubernetes-baseline Bootstrap ArgoCD and deploy infrastructure components ## Overview With EKS clusters deployed and [cluster access configured](/usage/getting-started/07-cluster-access), the next step is to bootstrap ArgoCD and deploy the Kubernetes baseline components. ArgoCD uses an app-of-apps pattern to manage all cluster resources declaratively. Before proceeding, ensure you have `kubectl` access to the cluster. See [Cluster Access](/usage/getting-started/07-cluster-access) for setup instructions. ## Architecture The kit uses a three-tier app-of-apps pattern: ``` argocd-app-of-apps (root) ├── argocd → Self-manages ArgoCD installation ├── infrastructure-app-of-apps → Manages infrastructure components │ ├── cert-manager │ ├── traefik │ ├── external-secrets │ ├── external-dns │ ├── karpenter │ ├── cloudnative-pg │ ├── signoz-k8s-infra │ └── reloader └── services-app-of-apps → Manages application deployments ├── go-backend └── go-backend-helm ``` Each ArgoCD Application points to rendered manifests in `kubernetes/rendered/{cluster}/`. ## Update Repository URLs Before deploying, update the Git repository URLs to point to your fork: The Application manifests reference the Git repository. Update the `repoURL` in the source templates: **`kubernetes/src/argocd/argocd/templates/Application.argocd-app-of-apps.yaml`:** ```yaml theme={null} spec: source: repoURL: "git@github.com:/.git" # Update this targetRevision: main path: kubernetes/rendered/{{ .Values.cluster }}/argocd/argocd ``` Update this URL in all Application templates: * `kubernetes/src/argocd/argocd/templates/Application.*.yaml` * `kubernetes/src/argocd/infrastructure/templates/Application.GENERATOR.yaml` * `kubernetes/src/argocd/services/templates/Application.GENERATOR.yaml` After updating the source files, render the manifests for your cluster: ```bash theme={null} mise run //kubernetes/src/argocd:render-all staging mise run //kubernetes/src/infrastructure:render-all staging mise run //kubernetes/src/services:render-all staging ``` Replace `staging` with `production` for production clusters. ```bash theme={null} git add . git commit -m "chore: update ArgoCD repository URLs for fork" git push ``` ## Create Deploy Key ArgoCD needs SSH access to clone your private repository. Run the bootstrap task to generate a deploy key and create the Kubernetes secret: ```bash theme={null} cd kubernetes/bootstrap mise run create-deploy-key "git@github.com:/.git" ``` The command outputs the public key. Add it to your repository: 1. Go to your repository **Settings → Deploy keys** 2. Click **Add deploy key** 3. Title: `ArgoCD Deploy Key` 4. Paste the public key from the command output 5. Leave **Allow write access** unchecked (read-only is sufficient for GitOps) 6. Click **Add key** The private key is stored as a Kubernetes Secret (`repo-kube-starter-kit`) in the `argocd` namespace. ArgoCD automatically discovers it via the `argocd.argoproj.io/secret-type=repository` label. ## Configure GitHub OAuth (Optional) If you want to enable GitHub OAuth for ArgoCD login (recommended for production), you need to store the OAuth app secret in AWS Secrets Manager. 1. Go to your GitHub organization **Settings → Developer settings → OAuth Apps** 2. Click **New OAuth App** 3. Fill in the details: * **Application name**: `ArgoCD Staging` * **Homepage URL**: `https://argocd.staging.` * **Authorization callback URL**: `https://argocd.staging./api/dex/callback` 4. Click **Register application** 5. Generate a new client secret and save both the Client ID and Client Secret ```bash theme={null} export REGION="us-east-2" # Your AWS region export CLUSTER_NAME="" aws secretsmanager create-secret \ --name "${CLUSTER_NAME}-argocd-github-dex" \ --secret-string '{"clientId":"YOUR_CLIENT_ID","clientSecret":"YOUR_CLIENT_SECRET"}' \ --region ${REGION} ``` See the [ArgoCD Dex documentation](https://argo-cd.readthedocs.io/en/stable/operator-manual/user-management/#dex) for more details on SSO configuration. ## Bootstrap ArgoCD Only ArgoCD itself needs to be manually deployed. Once running, ArgoCD will deploy and manage all other infrastructure components automatically. ```bash theme={null} cd kubernetes/bootstrap mise run install-argocd staging ``` This task: 1. Applies ArgoCD manifests (handles CRD ordering automatically) 2. Waits for ArgoCD deployments to be ready 3. Applies AppProjects 4. Applies Applications ArgoCD will then automatically deploy all infrastructure components (cert-manager, external-secrets, external-dns, traefik, etc.) and services. ## Verify Bootstrap The deploy key secret should have been created by the bootstrap task: ```bash theme={null} kubectl get secrets -n argocd | grep repo-kube-starter-kit ``` Check the ArgoCD Applications: ```bash theme={null} kubectl get applications -n argocd ``` You should see: * `argocd-app-of-apps` - Synced * `infrastructure-app-of-apps` - Synced * `services-app-of-apps` - Synced From here, ArgoCD manages everything automatically. It may take several minutes for all applications to become healthy as reconciliation loops complete. During this time: * Infrastructure components are deployed and start up * External Secrets syncs secrets from AWS Secrets Manager * Pods restart to pick up new secrets/configurations * External DNS creates DNS records * Cert Manager issues TLS certificates You can monitor progress in the ArgoCD UI or by running `kubectl get applications -n argocd`. ## Access ArgoCD UI Once external-dns creates the DNS records, ArgoCD is accessible at `https://argocd..` (e.g., `https://argocd.staging.example.com`). Alternatively, use port-forwarding for immediate access: ```bash theme={null} kubectl port-forward svc/argocd-server -n argocd 8080:443 ``` Open [https://localhost:8080](https://localhost:8080) in your browser. ### Login If you configured GitHub OAuth, use the **Log in via GitHub** button. Otherwise, use the admin password: ```bash theme={null} kubectl get secret argocd-initial-admin-secret -n argocd \ -o jsonpath="{.data.password}" | base64 -d ``` Login with username `admin` and the retrieved password. The admin user is enabled by default for initial bootstrap and debugging. Once you've configured SSO (GitHub OAuth), consider disabling the admin user by setting `admin.enabled: "false"` in `kubernetes/src/infrastructure/argocd/values.yaml`. ## Infrastructure Components The following components are included in the Kubernetes baseline: | Component | Bootstrap | Purpose | | ---------------------------- | --------- | ----------------------------------------------- | | **argocd** | Manual | GitOps continuous delivery | | **cert-manager** | ArgoCD | TLS certificate automation with Let's Encrypt | | **external-secrets** | ArgoCD | Syncs secrets from AWS Secrets Manager | | **external-dns** | ArgoCD | Automatic DNS record management in Route53 | | **traefik** | ArgoCD | Ingress controller for routing external traffic | | **karpenter** | ArgoCD | Dynamic node provisioning and autoscaling | | **cloudnative-pg** | ArgoCD | PostgreSQL operator for in-cluster databases | | **signoz-k8s-infra** | ArgoCD | OpenTelemetry collectors for observability | | **reloader** | ArgoCD | Restarts pods when ConfigMaps/Secrets change | | **ebs-csi-driver-resources** | ArgoCD | StorageClass for encrypted EBS volumes | Only ArgoCD is deployed manually during bootstrap. All other components are deployed automatically by ArgoCD once it syncs with the repository. ## Verify Deployment ```bash theme={null} kubectl get applications -n argocd ``` All applications should show `Synced` and `Healthy` status. ```bash theme={null} kubectl get pods -n cert-manager kubectl get pods -n traefik kubectl get pods -n external-secrets kubectl get pods -n karpenter ``` All pods should be `Running`. ```bash theme={null} kubectl get ingress -A ``` Ingresses should have an ADDRESS assigned (the load balancer DNS name). Once external-dns is running, check Route53 for new records: ```bash theme={null} aws route53 list-resource-record-sets \ --hosted-zone-id "" \ --query "ResourceRecordSets[?Type=='A' || Type=='CNAME']" ``` ## Enable/Disable Components To enable or disable infrastructure components, edit `kubernetes/src/argocd/infrastructure/values.yaml`: ```yaml theme={null} applications: argocd: enabled: true cert-manager: enabled: true cloudnative-pg: enabled: true envoy-gateway: enabled: false # Disabled by default # ... other components ``` After changing, render and push: ```bash theme={null} mise run //kubernetes/src/infrastructure:render-all "" git add . && git commit -m "chore: enable/disable components" git push ``` ArgoCD will automatically sync the changes. ## Next Steps With the Kubernetes baseline deployed, proceed to [Local Development Setup](/usage/getting-started/09-local-development-setup) to set up KinD, Tilt, and mirrord for local development. # Local Development Setup Source: https://kubestarterkit.com/usage/getting-started/09-local-development-setup Set up KinD, Tilt, and mirrord for local development ## Overview The kit includes a complete local development environment using: * **[KinD](https://kind.sigs.k8s.io/)** - Kubernetes in Docker for a local multi-node cluster * **[Tilt](https://tilt.dev/)** - Live development environment with hot reloading * **[mirrord](https://mirrord.dev/)** - Remote debugging by intercepting pod traffic This lets you develop and test Kubernetes deployments locally before pushing to staging. ## Prerequisites Ensure you have: * A container runtime (Docker, Podman, or OrbStack) * mise installed with tools (`mise install` from [Repository Setup](/usage/getting-started/02-repository-setup)) ## Quick Start ```bash theme={null} mise run //local:create-cluster ``` This creates a 3-node KinD cluster (1 control-plane, 2 workers) with a local Docker registry. In a **separate terminal** (requires sudo): ```bash theme={null} mise run //local:cloud-provider-kind ``` This enables LoadBalancer services to get IPs in the `172.20.0.0/16` range. ```bash theme={null} mise run //local:tilt-up ``` Open [http://localhost:10350](http://localhost:10350) to view the Tilt dashboard. ```bash theme={null} mise run //local:ingress-urls ``` This prints the sslip.io URLs for accessing services: * `http://go-backend-kustomize.172-20-0-100.sslip.io` * `http://go-backend-helm.172-20-0-100.sslip.io` * `http://go-backend-timoni.172-20-0-100.sslip.io` ## What Gets Deployed Tilt deploys a subset of infrastructure and all demo services: ### Infrastructure Components | Component | Purpose | | ------------------ | ---------------------------------------------------------- | | **cert-manager** | TLS certificates (using self-signed ClusterIssuer locally) | | **cloudnative-pg** | PostgreSQL operator for in-cluster databases | | **traefik** | Ingress controller | ### Demo Services The kit includes three variants of `go-backend`, each demonstrating a different Kubernetes templating approach: | Service | Templating | Description | | ----------------- | ---------- | ---------------------------------------------------- | | go-backend | Kustomize | Uses overlays for environment-specific configuration | | go-backend-helm | Helm | Traditional Helm chart with values files | | go-backend-timoni | Timoni | CUE-based configuration with type safety | All three deploy the same Go application with: * PostgreSQL database (via CloudNativePG) * Database migrations (via Atlas, run as Kubernetes Job) * Ingress for external access ## Cluster Configuration ### KinD Cluster The cluster is configured in `local/kind/cluster-config.yaml`: ```yaml theme={null} kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 nodes: - role: control-plane - role: worker - role: worker ``` Key features: * 3 nodes for realistic scheduling * Local registry on `localhost:5001` * Stable Docker network (`172.20.0.0/16`) for predictable IPs ### Local Registry The `create-cluster` task creates a local Docker registry: ```bash theme={null} # Images pushed to localhost:5001 are available in the cluster docker build -t localhost:5001/my-app:latest . docker push localhost:5001/my-app:latest ``` Tilt automatically builds and pushes images to this registry. ## Tilt Configuration ### Tiltfile Structure ``` local/tilt/Tiltfile # Main entrypoint ├── kubernetes/src/infrastructure/Tiltfile # Infrastructure components └── kubernetes/src/services/Tiltfile # Application services ``` ### Infrastructure Tiltfile Deploys cluster components in order: ```python theme={null} # 1. cert-manager (no dependencies) # 2. cloudnative-pg (depends on cert-manager) # 3. traefik (no dependencies) ``` Each component uses Helm with local values (`values.local.yaml`). ### Services Tiltfile For each service: 1. Builds the Docker image 2. Waits for CloudNativePG to be ready 3. Creates a PostgreSQL Cluster CR 4. Runs database migrations 5. Deploys the application ### Customizing Tilt Behavior Edit the values files for local overrides: ```bash theme={null} # Infrastructure kubernetes/src/infrastructure/*/values.local.yaml # Services kubernetes/src/services/*/values.local.yaml ``` ## Developing Outside the Container with mirrord While Tilt runs your services inside containers in the KinD cluster, [mirrord](https://mirrord.dev/) lets you run a service directly on your host machine while staying connected to the cluster. This is ideal when you want to iterate on application code without waiting for container rebuilds: * **Skip container builds:** Just recompile and restart, no Docker builds or image pushes * **Use your local tools:** Run with your IDE, debugger, profiler, or any host-native tooling * **Access cluster resources:** Your local process can reach cluster-internal services (databases, APIs) * **Inherit pod environment:** Environment variables and secrets from the target pod are available locally ### Configuration The mirrord config is in `services/go-backend/.mirrord/mirrord.json`: ```json theme={null} { "target": { "path": "deployment/go-backend" }, "feature": { "network": { "incoming": "steal", "outgoing": true }, "fs": "local", "env": true } } ``` This configuration: * **Steals** incoming traffic from the `go-backend` deployment in the KinD cluster * Enables **outgoing** connections to cluster resources (database, etc.) * Uses **local** filesystem * Mirrors pod **environment variables** to your local process ### Using mirrord The service must be deployed in the local cluster (via Tilt) before mirrord can intercept its traffic. ```bash theme={null} cd services/go-backend mirrord exec -- go run ./cmd/main.go ``` Your local process now receives traffic that would go to the in-cluster pod. Use your IDE's debugger, add print statements, or modify code. Changes take effect immediately without rebuilding containers. Most IDEs have mirrord plugins that integrate debugging directly. See the [mirrord IDE docs](https://mirrord.dev/docs/ide-plugins/) for VS Code, IntelliJ, and others. ## Comparison: Local vs Production | Aspect | Local (KinD + Tilt) | Production (EKS + ArgoCD) | | ---------- | ------------------- | ------------------------- | | Cluster | KinD (Docker) | EKS (AWS) | | Registry | localhost:5001 | ECR | | Ingress | sslip.io | Route53 + real domain | | TLS | Self-signed | Let's Encrypt | | Secrets | Local values | AWS Secrets Manager | | Database | CloudNativePG | CloudNativePG (or RDS) | | Deployment | Tilt (live reload) | ArgoCD (GitOps) | ## Next Steps With local development set up, you're ready to start building! See the [Operations](/usage/operations) guides for: * [Updating 1st Party Applications](/usage/operations/02-updating-1st-party-applications) - Modify and deploy your services * [Bootstrapping a New Service](/usage/operations/04-bootstrapping-new-service) - Add a new application * [Database Operations](/usage/operations/07-database-operations) - Manage migrations and databases # Making Terraform Changes Source: https://kubestarterkit.com/usage/operations/01-updating-terraform-infrastructure How to make updates to infrastructure via Terraform ## Overview Infrastructure changes flow through Terramate and GitHub. Whether you're modifying an existing stack, adding a new resource, or creating entirely new infrastructure, the workflow is the same: make changes locally, open a PR, review the plan, and merge to apply. ## Workflow Edit the relevant files in `terraform/`. This might be: * Stack configuration (`config.tm.hcl`, `inputs.tm.hcl`) * Module code in `terraform/modules/` * New stack definitions (`stack.tm.hcl`) If you changed any Terramate configuration (`.tm.hcl` files), regenerate the Terraform files: ```bash theme={null} cd terraform terramate generate ``` This updates generated files like `_backend.tf`, `_provider.tf`, and `_main.tf`. Test your changes before pushing: ```bash theme={null} # List affected stacks terramate list --changed # Run validation terramate run --changed -- terraform validate # Preview plan (requires AWS credentials) terramate script run --changed -- plan ``` Push your branch and open a PR. The CI workflow will: 1. Check Terramate formatting 2. Detect changed stacks 3. Run `terraform plan` for each affected stack 4. Sync preview results to Terramate Cloud Check the plan output in: * GitHub Actions logs * PR comment with plan summary (if using Terramate Cloud) * Terramate Cloud dashboard (for a unified view across stacks) Look for: * Expected resource changes (create, update, destroy) * No unintended side effects * Correct dependency ordering Once approved, merge the PR. The deploy workflow will: 1. Detect changed stacks 2. Apply changes in dependency order 3. Sync deployment results to Terramate Cloud Use GitHub branch protection rules to control who can deploy infrastructure changes. Consider requiring PR approvals, setting up CODEOWNERS for the `terraform/` directory, and requiring status checks to pass before merging. ## Common Tasks ### Adding a New Stack 1. Create directory under `terraform/live/{stage}/{region}/`: ```bash theme={null} mkdir -p "terraform/live///my-new-stack" ``` 2. Create `stack.tm.hcl`: ```hcl theme={null} stack { id = "--my-new-stack" name = "my-new-stack" description = "Description of what this stack does" tags = ["", "", "my-new-stack"] # Declare dependencies if needed after = ["tag:::eks"] } ``` Replace `` (e.g., `use2`) and `` (e.g., `us-east-2`) with your values. 3. Create `config.tm.hcl` with stack-specific globals 4. Create `main.tf` or use module mixins 5. Generate files: ```bash theme={null} terramate generate ``` You may need to run `terramate generate` twice. The first pass generates `_outputs.tm.hcl` for some stacks (e.g., EKS), which is then used by dependent stacks in the second pass. ### Modifying a Module 1. Edit the module in `terraform/modules/{module-name}/` 2. Open a PR - Terramate will plan all stacks that use the module 3. Review plans across all affected environments 4. Merge to apply changes everywhere ### Targeting Specific Stacks Use tags to run commands on specific stacks: ```bash theme={null} export REGION="us-east-2" # Your AWS region export STAGE="staging" # or "prod" # Single stack terramate run --tags ${STAGE}:${REGION}:eks -- terraform plan # All stacks in an environment terramate run --tags ${STAGE} -- terraform plan # All networking stacks terramate run --tags networking -- terraform plan ``` ## Troubleshooting ### "Repository has untracked files" Terramate requires a clean git state. Either stage your changes or use: ```bash theme={null} terramate run --disable-safeguards=git-untracked -- terraform plan ``` ### Missing dependency outputs When a dependency stack hasn't been applied yet, Terramate uses mock values. This is expected during initial bootstrap. Apply stacks in dependency order: ```bash theme={null} # Check the order terramate list --run-order # Apply in order terramate script run -- deploy ``` ### Regenerate after config changes If you see drift between generated files and configuration: ```bash theme={null} terramate generate git diff # Review changes ``` ## Configuration Reference ### Root Configuration The `terraform/config.tm.hcl` defines globals inherited by all stacks: ```hcl theme={null} globals { namespace = "" # e.g., "ksk" github_oidc_role_arn = "arn:aws:iam::..." sso_admin_role_arn = "arn:aws:iam::..." terraform_state_bucket = "-gbl-infra-bootstrap-state" terraform_state_region = "" # e.g., "us-east-2" terraform_version = ">= 1.10" aws_provider_version = "~> 6.27" } ``` ### Terramate Project Configuration The `terramate.tm.hcl` at the repository root configures Terramate features: ```hcl theme={null} terramate { required_version = ">= 0.10.0" config { cloud { organization = "your-org" location = "us" } experiments = [ "outputs-sharing", "scripts", "tmgen" ] } } ``` ### Stack Definition Each stack requires a `stack.tm.hcl`: ```hcl theme={null} # terraform/live/staging//networking/stack.tm.hcl stack { id = "staging--networking" name = "networking" description = "VPC and networking for staging " tags = ["staging", "", "networking", "infrastructure"] # Declare dependencies if needed after = ["tag:staging::eks"] } ``` ### Stack-Specific Configuration Override globals in `config.tm.hcl` within each stack: ```hcl theme={null} # terraform/live/staging//networking/config.tm.hcl globals { vpc_cidr = "10.0.0.0/16" nat_mode = "fck_nat" # Cost-effective NAT for non-prod } ``` ### Outputs Sharing Cross-stack dependencies without `terraform_remote_state`: ```hcl theme={null} # In networking stack: outputs.tm.hcl output "vpc_id" { backend = "terraform" value = module.networking.vpc_id } # In EKS stack: inputs.tm.hcl input "vpc_id" { backend = "terraform" from_stack_id = "staging-use2-networking" value = outputs.vpc_id.value mock = "vpc-mock12345" # Used during initial bootstrap } ``` The input becomes a regular Terraform variable, usable in your module: ```hcl theme={null} module "eks" { source = "../../../modules/eks" vpc_id = var.vpc_id # ... } ``` ### Terramate Scripts The `terraform/scripts.tm.hcl` file defines reusable commands used by CI: ```hcl theme={null} script "preview" { description = "Plan with outputs sharing" job { commands = [ ["terraform", "validate"], ["terraform", "plan", "-out", "out.tfplan", "-detailed-exitcode", "-lock=false", { sync_preview = true terraform_plan_file = "out.tfplan" enable_sharing = true }], ] } } script "deploy" { description = "Apply with outputs sharing" job { commands = [ ["terraform", "apply", "-auto-approve", "-lock-timeout=5m", { enable_sharing = true }], ] } } ``` ### Mixins Code generation templates live in `terraform/imports/mixins/`. These generate common files (`_backend.tf`, `_provider.tf`, `_main.tf`) from templates, eliminating copy-paste between stacks. # Updating 1st Party Applications Source: https://kubestarterkit.com/usage/operations/02-updating-1st-party-applications Deploy changes to your applications ## Overview First-party applications are services you build and deploy, like the `go-backend` example in the kit. This page covers how to make changes and deploy them through the GitOps pipeline. ## Deployment Flow Changes flow through staging first, then to production after verification: ``` ┌────────────┐ │ STAGING │ Merge to main -> Build -> Update manifests -> ArgoCD syncs ─┐ └────────────┘ │ ┌──────────────────── Verify Changes ───────────────────────┘ ┌────────────┐ │ │ PRODUCTION │ └─> Merge Release PR -> Build -> Update manifests -> ArgoCD syncs └────────────┘ ``` Each step is detailed in the sections below. ## Make Code Changes Make changes to your application in `services/`: ```bash theme={null} # Example: modify the go-backend service vim services/go-backend/cmd/main.go ``` Use the [local development environment](/usage/getting-started/09-local-development-setup) to test: ```bash theme={null} mise run //local:tilt-up ``` Tilt automatically rebuilds and redeploys when you save changes. ```bash theme={null} git add services/go-backend/ git commit -m "feat: add new endpoint for health checks" git push origin main ``` ## What Happens After Push (Staging) ### 1. CI Build Workflow The CI workflow (`.github/workflows/ci-build-push.yml`): 1. Detects which services changed using path filters 2. Generates a version tag (e.g., `0.2.2-rc0029-g1234567`) 3. Builds the Docker image 4. Pushes to ECR 5. Triggers the GitOps workflow ### 2. GitOps Update Workflow The GitOps workflow (`.github/workflows/gitops-update-manifests.yml`): 1. Updates the image tag in values files (e.g., `kubernetes/src/services/go-backend-helm/values.yaml`) 2. Renders the Kubernetes manifests 3. Commits and pushes to `main` The image tag is identified by a comment marker: ```yaml theme={null} image: version: 0.2.2-rc0029-g1234567 # staging_services/go-backend ``` ### 3. ArgoCD Sync ArgoCD watches the repository and automatically syncs when manifests change. You can monitor the sync in the ArgoCD UI or CLI: ```bash theme={null} argocd app get go-backend-helm argocd app sync go-backend-helm ``` ## Version Tagging Strategy | Trigger | Tag Format | Example | Environment | | --------------- | ------------------- | ----------------------- | -------------- | | Push to `main` | `X.Y.Z-rcNNNN-gSHA` | `0.2.2-rc0029-g1234567` | Staging | | Release tag | `X.Y.Z` | `0.2.3` | Production | | Manual workflow | User-specified | `hotfix-123` | User-specified | ### Pre-release Versions (Staging) When you push to `main`, the version is generated as: * Base: last release tag + 1 patch (e.g., `0.2.2` → `0.2.3`) * Suffix: `-rcNNNN-gSHA` where NNNN is commits since last tag This ensures every commit has a unique, sortable version. ### Release Versions (Production) To deploy to production, create a release tag: ```bash theme={null} git tag "services/go-backend@0.2.3" git push origin "services/go-backend@0.2.3" ``` This triggers the same build workflow but: * Uses the exact version from the tag (`0.2.3`) * Sets environment to `production` ## Deploy to Production Ensure the change works correctly in staging before promoting to production. The Release Please workflow (`.github/workflows/release-please.yml`) automatically creates a release PR when changes are pushed to `main`. The PR: * Bumps the version based on conventional commit messages * Updates the CHANGELOG * Shows all changes since the last release When you merge the release PR: 1. A GitHub release is created with the new version tag 2. The CI workflow (`.github/workflows/ci-build-push.yml`) builds and pushes the production image 3. The GitOps workflow (`.github/workflows/gitops-update-manifests.yml`) updates production manifests 4. ArgoCD syncs to the production cluster You can also create a release manually by pushing a tag: ```bash theme={null} git tag "services/go-backend@0.2.3" git push origin "services/go-backend@0.2.3" ``` Watch the CI workflow and ArgoCD sync: ```bash theme={null} # Check workflow status gh run list --workflow=ci-build-push.yml # Check ArgoCD argocd app get go-backend-helm --grpc-web ``` ## Manual Deployment For debugging or hotfixes, you can trigger deployments manually: Build and push an image without deploying: ```bash theme={null} gh workflow run ci-build-push.yml \ -f service=services/go-backend \ -f version=hotfix-123 ``` Deploy an already-built image to an environment: ```bash theme={null} gh workflow run gitops-update-manifests.yml \ -f service=services/go-backend \ -f version=0.2.2-rc0029-g1234567 \ -f environment=staging ``` ## Update Kubernetes Configuration To change Kubernetes configuration (replicas, resources, etc.) without changing application code: For Helm-based services, edit the values file: ```bash theme={null} vim kubernetes/src/services/go-backend-helm/values.yaml ``` For Kustomize-based services, edit the overlay: ```bash theme={null} vim kubernetes/src/services/go-backend/staging/kustomization.yaml ``` Example changes: ```yaml theme={null} replicas: 3 # Increase replicas resources: requests: memory: "256Mi" cpu: "250m" ``` ```bash theme={null} mise run //kubernetes/src/services:render-all "" ``` ```bash theme={null} git add kubernetes/ git commit -m "chore: increase go-backend replicas to 3" git push origin main ``` ArgoCD syncs the new configuration without rebuilding the image. ## Rollback To rollback to a previous version: Update the image tag to a previous version: ```bash theme={null} # Edit values.yaml vim kubernetes/src/services/go-backend-helm/values..yaml # Change image version to previous tag # image: # version: 0.2.2-rc0028-g98b8c55 # Render and push mise run //kubernetes/src/services:render-all "" git add . && git commit -m "fix: rollback go-backend to 0.2.2-rc0028" git push origin main ``` Because auto-sync is enabled by default for all ArgoCD Applications, you would need to disable auto-sync for the app-of-apps AND the corresponding Application for this change to not be reverted. ```bash theme={null} # View history argocd app history go-backend-helm # Rollback to previous revision argocd app rollback go-backend-helm "" ``` ## Next Steps * [Updating 3rd Party Applications](/usage/operations/03-updating-3rd-party-applications) - Update infrastructure components * [Bootstrapping a New Service](/usage/operations/04-bootstrapping-new-service) - Add a new application # Updating 3rd Party Applications Source: https://kubestarterkit.com/usage/operations/03-updating-3rd-party-applications Update infrastructure components like traefik, cert-manager, and more ## Overview Third-party applications are infrastructure components deployed via Helm charts, things like traefik, cert-manager, external-secrets, and ArgoCD itself. This page covers how to update their versions and configurations. ## Infrastructure Components The kit includes these third-party components in `kubernetes/src/infrastructure/`: | Component | Chart Source | Purpose | | ---------------- | -------------------------- | ----------------------------- | | argocd | argoproj.github.io | GitOps controller | | cert-manager | quay.io/jetstack | TLS certificate automation | | cloudnative-pg | cloudnative-pg.github.io | PostgreSQL operator | | external-dns | kubernetes-sigs.github.io | DNS record management | | external-secrets | charts.external-secrets.io | Secret synchronization | | traefik | ghcr.io/traefik/helm | Ingress controller | | karpenter | public.ecr.aws/karpenter | Node autoscaling | | reloader | stakater.github.io | Pod restart on config changes | | signoz-k8s-infra | charts.signoz.io | Observability collectors | ## Update Chart Versions Each component uses a "wrapper chart" pattern: a local Helm chart that includes the upstream chart as a dependency. Check the current version in the component's `values.yaml`: ```bash theme={null} cat kubernetes/src/infrastructure/traefik/values.yaml ``` Look for the `chartVersions` section: ```yaml theme={null} chartVersions: traefik: "38.0.1" ``` Find the latest version from the chart repository: ```bash theme={null} # Search for versions using OCI registry helm search repo traefik/traefik --versions | head -10 ``` Edit the values file to update the version: ```bash theme={null} vim kubernetes/src/infrastructure/traefik/values.yaml ``` ```yaml theme={null} chartVersions: traefik: "38.1.0" # Updated version ``` For environment-specific versions, edit `values.staging.yaml` or `values.production.yaml`. Setting these independently allows for testing upgrades in lower environments before rolling out to production. Regenerate the lock file with the new dependency: ```bash theme={null} cd kubernetes/src/infrastructure/traefik helm dependency update ``` ```bash theme={null} mise run //kubernetes/src/infrastructure:render-all "" git add . git commit -m "chore: update traefik to 38.1.0" git push origin main ``` ## Update Configuration To change component settings without upgrading versions: Modify `values.yaml` for base configuration or `values.{environment}.yaml` for environment-specific settings: ```bash theme={null} vim kubernetes/src/infrastructure/traefik/values.yaml ``` For example, increase controller replicas: ```yaml theme={null} traefik: deployment: replicas: 3 ``` ```bash theme={null} mise run //kubernetes/src/infrastructure:render-all "" ``` ```bash theme={null} git diff kubernetes/rendered/ git add . git commit -m "chore: increase traefik replicas to 3" git push origin main ``` ArgoCD automatically syncs the changes. ## Add Additional Manifests Many components need additional manifests beyond the upstream chart (ClusterIssuers, StorageClasses, etc.). Add these in the wrapper chart's `templates/` directory: ``` kubernetes/src/infrastructure/cert-manager/ ├── Chart.yaml ├── Chart.yaml.tmpl ├── values.yaml ├── values.staging.yaml └── templates/ ├── ClusterIssuer.letsencrypt-production.yaml ├── ClusterIssuer.letsencrypt-staging.yaml └── ClusterIssuer.selfsigned.yaml ``` These templates are rendered alongside the upstream chart resources. ## Environment-Specific Configuration Use values overlay files for environment differences: ```yaml theme={null} # values.yaml (base) traefik: deployment: replicas: 2 # values.staging.yaml traefik: deployment: replicas: 1 # Smaller for staging # values.production.yaml traefik: deployment: replicas: 3 # Larger for production ``` The render process merges base values with environment-specific overrides. ## Enable/Disable Components Components are enabled/disabled in the ArgoCD infrastructure app-of-apps: ```bash theme={null} vim kubernetes/src/argocd/infrastructure/values.yaml ``` ```yaml theme={null} applications: argocd: enabled: true cert-manager: enabled: true envoy-gateway: enabled: false # Disabled istio: enabled: false # Disabled ``` After changing, render and push: ```bash theme={null} mise run //kubernetes/src/infrastructure:render-all "" git add . && git commit -m "chore: disable envoy-gateway" git push origin main ``` ## Upgrade Strategies ### Minor/Patch Updates For minor and patch version updates (e.g., `4.12.2` → `4.12.3`): 1. Update the version in values.yaml 2. Run `helm dependency update` 3. Render, commit, and push 4. ArgoCD syncs automatically ### Major Updates For major version updates (e.g., `4.x` → `5.x`): 1. **Read the changelog** for breaking changes 2. **Test in staging first** before production 3. **Update values** if configuration schema changed 4. **Have a rollback plan** if the component is critical Major version upgrades may require CRD updates. Check the component's upgrade guide and apply CRD changes before upgrading the chart. ### CRD Updates Some components (cert-manager, ArgoCD, Karpenter) use CRDs that may need manual updates: ```bash theme={null} # Example: Update cert-manager CRDs kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.17.0/cert-manager.crds.yaml ``` Check the component's documentation for CRD upgrade procedures. ## Best Practices 1. **Test in staging first** - Always upgrade staging before production 2. **Read changelogs** - Especially for major versions 3. **Update one component at a time** - Easier to isolate issues 4. **Keep versions consistent** - Use the same version across environments when possible 5. **Monitor after upgrades** - Watch metrics and logs for regressions ## Next Steps * [Adding K8s Infrastructure](/usage/operations/05-adding-k8s-infrastructure) - Add new infrastructure components * [Managing Secrets](/usage/operations/06-managing-secrets) - Configure External Secrets # Bootstrapping a New Service Source: https://kubestarterkit.com/usage/operations/04-bootstrapping-new-service Add a new application to the platform ## Overview This guide walks through adding a new service to the platform. You'll create the application code, Dockerfile, Kubernetes manifests, and CI/CD configuration. ## Create the Service ```bash theme={null} mkdir -p services/my-new-service cd services/my-new-service ``` Create your application. For example, a Go service: ```bash theme={null} go mod init my-new-service mkdir cmd ``` Create `cmd/main.go`: ```go theme={null} package main import ( "log" "net/http" ) func main() { http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("ok")) }) log.Println("Starting server on :8080") log.Fatal(http.ListenAndServe(":8080", nil)) } ``` Create `Dockerfile`: ```dockerfile theme={null} FROM golang:1.23-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o server ./cmd FROM gcr.io/distroless/static-debian12:nonroot COPY --from=builder /app/server /server EXPOSE 8080 ENTRYPOINT ["/server"] ``` Create `mise.toml`: ```toml theme={null} [env] # Replace with your AWS account ID and region IMAGE_REPO = ".dkr.ecr..amazonaws.com/services/my-new-service" SERVICE_RELEASE_TAG = "services/my-new-service" [tasks] run = "go run ./cmd" build-image = "docker build ." ``` ```bash theme={null} # Run the service mise run run # In another terminal, test it curl http://localhost:8080/health ``` ## Create ECR Repository Add the new repository to the ECR configuration: Edit `terraform/live/shared//ecr-repositories/main.tf` and add your service to the `local.repos` list: ```hcl theme={null} locals { region = var.aws_region repos = [ "services/container-only-dummy", "services/go-backend", "services/go-backend/migrations", "services/go-backend-no-migrations", "services/my-new-service" # Add this ] } ``` Commit and open a PR. The Terramate workflow will run a preview, and once merged, the ECR repository will be created. Alternatively, apply directly: ```bash theme={null} cd terraform terramate run --tags ecr -- terraform apply ``` ## Create Kubernetes Manifests Choose your preferred templating approach. The kit demonstrates three options. Create a Helm chart in `kubernetes/src/services/my-new-service/`: ```bash theme={null} mkdir -p kubernetes/src/services/my-new-service/templates ``` **Chart.yaml:** ```yaml theme={null} apiVersion: v2 name: my-new-service version: 0.1.0 ``` **values.yaml:** ```yaml theme={null} replicas: 1 image: # Replace with your AWS account ID and region repository: ".dkr.ecr..amazonaws.com/services/my-new-service" version: 0.1.0 # staging_services/my-new-service ingress: hostname: "my-new-service.staging." ``` **templates/deployment.yaml:** ```yaml theme={null} apiVersion: apps/v1 kind: Deployment metadata: name: my-new-service namespace: my-new-service spec: replicas: {{ .Values.replicas }} selector: matchLabels: app: my-new-service template: metadata: labels: app: my-new-service spec: containers: - name: my-new-service image: "{{ .Values.image.repository }}:{{ .Values.image.version }}" ports: - containerPort: 8080 livenessProbe: httpGet: path: /health port: 8080 ``` Add additional templates for Service, Ingress, etc. Create a Kustomize structure in `kubernetes/src/services/my-new-service/`: ```bash theme={null} mkdir -p kubernetes/src/services/my-new-service/{base,staging,production} ``` **base/kustomization.yaml:** ```yaml theme={null} apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - deployment.yaml - service.yaml - ingress.yaml ``` **staging/kustomization.yaml:** ```yaml theme={null} apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - ../base images: - name: my-new-service # Replace with your AWS account ID and region newName: ".dkr.ecr..amazonaws.com/services/my-new-service" newTag: "0.1.0" # staging_services/my-new-service ``` ## Add mise Tasks for Rendering Create `kubernetes/src/services/my-new-service/mise.toml`: ```toml theme={null} [tasks.render-cluster] description = "Render manifests for a specific cluster" usage = "arg '' help='Target cluster (staging, production)'" run = ''' #!/usr/bin/env bash set -euo pipefail CLUSTER="${usage_cluster}" OUTPUT_DIR="$(git rev-parse --show-toplevel)/kubernetes/rendered/${CLUSTER}/services/my-new-service" mise run //tools:render:prep-output-dir "$OUTPUT_DIR" helm template my-new-service . \ --namespace my-new-service \ --values values.yaml \ --values "values.${CLUSTER}.yaml" \ | mise run //tools:render:split-k8s-docs "$OUTPUT_DIR" ''' ``` ## Register with ArgoCD Add the service to the ArgoCD services app-of-apps: Edit `kubernetes/src/argocd/services/values.yaml`: ```yaml theme={null} applications: go-backend: enabled: true go-backend-helm: enabled: true my-new-service: enabled: true # Add this ``` ```bash theme={null} mise run //kubernetes/src/argocd:render-all "" # Adds the ArgoCD Application to the app-of-apps mise run //kubernetes/src/services:render-all "" # Renders the service manifests ``` ## Add to CI/CD Edit `.github/utils/file-filters.yaml`: ```yaml theme={null} services/my-new-service: - 'services/my-new-service/**' ``` Edit `.github/workflows/ci-build-push.yml`: ```yaml theme={null} inputs: service: options: - services/go-backend - services/my-new-service # Add this ``` Also update `.github/workflows/gitops-update-manifests.yml`. ## Test Locally Edit `kubernetes/src/services/Tiltfile` to include your new service: ```python theme={null} # Add your service load_dynamic('./my-new-service/Tiltfile') ``` Create `kubernetes/src/services/my-new-service/Tiltfile` following the pattern of existing services. ```bash theme={null} cd local mise run tilt-up ``` Access via the sslip.io URL or port-forward: ```bash theme={null} kubectl port-forward svc/my-new-service -n my-new-service 8080:80 curl http://localhost:8080/health ``` ## Deploy to Staging ```bash theme={null} git add . git commit -m "feat: add my-new-service" git push origin main ``` 1. Watch the CI workflow build the image 2. Watch the GitOps workflow update manifests 3. Check ArgoCD for the new Application ## Next Steps * [Managing Secrets](/usage/operations/06-managing-secrets) - Add secrets for your service * [Database Operations](/usage/operations/07-database-operations) - Add a database if needed * [Observability](/usage/operations/08-observability) - Configure metrics and tracing # Adding New K8s Infrastructure Components Source: https://kubestarterkit.com/usage/operations/05-adding-k8s-infrastructure Add new infrastructure components and manage environment-specific configurations ## Overview This guide covers adding new Kubernetes infrastructure components and managing configurations across environments. Infrastructure components are third-party tools deployed via Helm that provide platform capabilities. ## Add a New Infrastructure Component ```bash theme={null} mkdir -p kubernetes/src/infrastructure/my-component/templates cd kubernetes/src/infrastructure/my-component ``` The template allows environment-specific chart versions: ```yaml theme={null} apiVersion: v2 name: my-component version: 0.1.0 dependencies: - name: my-component version: "1.0.0" # Placeholder, replaced during render repository: https://charts.example.com ``` Define base configuration and chart versions: ```yaml theme={null} # Chart dependency versions chartVersions: my-component: "1.2.3" # Pass-through values to the upstream chart my-component: replicaCount: 2 resources: requests: memory: "128Mi" cpu: "100m" ``` **values.staging.yaml:** ```yaml theme={null} chartVersions: my-component: "1.2.3" my-component: replicaCount: 1 ``` **values.production.yaml:** ```yaml theme={null} chartVersions: my-component: "1.2.3" my-component: replicaCount: 3 ``` **values.local.yaml:** ```yaml theme={null} my-component: replicaCount: 1 ``` Create templates in `templates/` for resources not provided by the upstream chart: ```yaml theme={null} # templates/ServiceMonitor.yaml apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: my-component namespace: {{ .Release.Namespace }} spec: selector: matchLabels: app: my-component endpoints: - port: metrics ``` ```toml theme={null} [tasks.render-cluster] description = "Render manifests for a specific cluster" usage = 'arg "" help="Target cluster (staging, production)"' run = ''' #!/usr/bin/env bash set -euo pipefail CLUSTER="${usage_cluster}" OUTPUT_DIR="$(git rev-parse --show-toplevel)/kubernetes/rendered/${CLUSTER}/infrastructure/my-component" # Inject chart versions from values files VALUES_ENV="$CLUSTER" mise run //tools:render:inject-chart-versions # Build dependencies mise run //tools:render:helm-dep-build # Prepare output directory mise run //tools:render:prep-output-dir "$OUTPUT_DIR" # Render and split into individual files helm template my-component . \ --namespace my-component \ --values values.yaml \ --values "values.${CLUSTER}.yaml" \ | mise run //tools:render:split-k8s-docs "$OUTPUT_DIR" ''' ``` ## Register with ArgoCD Edit `kubernetes/src/argocd/infrastructure/values.yaml`: ```yaml theme={null} applications: argocd: enabled: true cert-manager: enabled: true my-component: enabled: true # Add this ``` ```bash theme={null} mise run //kubernetes/src/argocd:render-all "" # Adds the ArgoCD Application to the app-of-apps mise run //kubernetes/src/infrastructure:render-all "" # Renders the component manifests ``` ```bash theme={null} git add . git commit -m "feat: add my-component infrastructure" git push origin main ``` ## Environment-Specific Configuration ### Values Hierarchy Values are merged in order (later files override earlier): 1. `values.yaml` - Base configuration 2. `values.{cluster}.yaml` - Environment-specific overrides ### Common Patterns **Different resource limits per environment:** ```yaml theme={null} # values.yaml (base) my-component: resources: requests: memory: "256Mi" cpu: "250m" # values.staging.yaml my-component: resources: requests: memory: "128Mi" cpu: "100m" # values.production.yaml my-component: resources: requests: memory: "512Mi" cpu: "500m" ``` **Different hostnames:** ```yaml theme={null} # values.staging.yaml my-component: ingress: host: my-component.staging.example.com # values.production.yaml my-component: ingress: host: my-component.example.com ``` **Feature flags:** ```yaml theme={null} # values.staging.yaml my-component: debug: true metrics: enabled: true # values.production.yaml my-component: debug: false metrics: enabled: true ``` ## Add to Local Development Create `values.local.yaml` with local-specific settings: ```yaml theme={null} my-component: replicaCount: 1 ingress: host: my-component.127-0-0-1.sslip.io ``` Edit `kubernetes/src/infrastructure/Tiltfile`: ```python theme={null} # Add my-component k8s_yaml( helm( 'my-component', name='my-component', namespace='my-component', values=['my-component/values.yaml', 'my-component/values.local.yaml'], ) ) k8s_resource( 'my-component', resource_deps=['cert-manager'], # Add dependencies if needed ) ``` ## Update the Render Pipeline If your component needs to be rendered as part of the main render task: Edit `kubernetes/mise.toml` to include your component: ```toml theme={null} [tasks.render] description = "Render all manifests for all environments" run = ''' #!/usr/bin/env bash set -euo pipefail for cluster in staging production; do # ... existing components ... # Add your component mise run //kubernetes/src/infrastructure/my-component:render-cluster "$cluster" done ''' ``` ## Namespace Management Components typically run in their own namespace. Create it in your templates: ```yaml theme={null} # templates/Namespace.yaml apiVersion: v1 kind: Namespace metadata: name: {{ .Release.Namespace }} labels: app.kubernetes.io/name: my-component ``` Or include it in the Helm install: ```bash theme={null} helm template my-component . \ --namespace my-component \ --create-namespace \ ... ``` ## Troubleshooting ### Helm dependency errors ```bash theme={null} # Clear cached dependencies rm -rf charts/ Chart.lock # Re-add the repository helm repo add my-repo https://charts.example.com helm repo update # Rebuild dependencies helm dependency build ``` ### Template rendering errors ```bash theme={null} # Debug with verbose output helm template my-component . \ --debug \ --values values.yaml \ --values values.staging.yaml ``` ### ArgoCD not detecting the new Application 1. Verify the Application manifest was generated: ```bash theme={null} ls kubernetes/rendered/staging/argocd/infrastructure/ ``` 2. Check the infrastructure app-of-apps is synced: ```bash theme={null} argocd app get infrastructure-app-of-apps ``` 3. Force a sync: ```bash theme={null} argocd app sync infrastructure-app-of-apps ``` ## Best Practices 1. **Use the wrapper chart pattern** - Never modify upstream charts directly 2. **Version pin dependencies** - Always specify exact versions in `chartVersions` 3. **Minimize environment differences** - Keep staging similar to production 4. **Document custom resources** - Explain why custom templates are needed 5. **Test locally first** - Use Tilt before deploying to staging ## Next Steps * [Managing Secrets](/usage/operations/06-managing-secrets) - Configure secrets for your component * [Updating 3rd Party Applications](/usage/operations/03-updating-3rd-party-applications) - Update component versions # Managing Secrets Source: https://kubestarterkit.com/usage/operations/06-managing-secrets Create and manage secrets with External Secrets and AWS Secrets Manager ## Overview The kit uses [External Secrets Operator](https://external-secrets.io/) to sync secrets from AWS Secrets Manager into Kubernetes. This keeps sensitive data out of Git while providing a GitOps-friendly secret management workflow. ## Architecture ``` AWS Secrets Manager Kubernetes Cluster ┌───────────────────┐ ┌────────────────────────────┐ │ │ │ │ │ ksk-use2-staging- │ │ ClusterSecretStore │ │ myapp-db │<───────│ (aws-secrets-manager) │ │ │ │ │ │ └───────────────────┘ │ ▼ │ │ ExternalSecret │ │ (myapp-secrets) │ │ │ │ │ ▼ │ │ Secret │ │ (myapp-secrets) │ │ │ └────────────────────────────┘ ``` 1. **ClusterSecretStore** - Configures access to AWS Secrets Manager (one per cluster) 2. **ExternalSecret** - Defines which secrets to fetch and how to map them 3. **Secret** - The Kubernetes Secret created and kept in sync ## Prerequisites The External Secrets Operator uses Pod Identity to authenticate to AWS. This is configured automatically by the EKS Terraform module. Verify the ClusterSecretStore is working: ```bash theme={null} kubectl get clustersecretstores kubectl describe clustersecretstore aws-secrets-manager ``` ## Create a Secret in AWS Using AWS CLI: ```bash theme={null} export REGION="us-east-2" # Your AWS region aws secretsmanager create-secret \ --name "ksk-use2-staging-myapp-db" \ --secret-string "postgres://user:pass@host:5432/db" \ --region ${REGION} ``` Or using the AWS Console: 1. Navigate to Secrets Manager 2. Click "Store a new secret" 3. Choose "Other type of secret" 4. Enter key/value pairs or plaintext 5. Name it following the [naming convention](#secret-naming-conventions) Create `ExternalSecret.myapp-secrets.yaml` in your service's templates: ```yaml theme={null} apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: name: myapp-secrets namespace: myapp spec: refreshInterval: 1h secretStoreRef: name: aws-secrets-manager kind: ClusterSecretStore target: name: myapp-secrets creationPolicy: Owner data: - secretKey: DATABASE_URL remoteRef: key: ksk-use2-staging-myapp-db ``` ```bash theme={null} # Apply the ExternalSecret kubectl apply -f ExternalSecret.myapp-secrets.yaml # Check the ExternalSecret status kubectl get externalsecret myapp-secrets -n myapp # Verify the Secret was created kubectl get secret myapp-secrets -n myapp kubectl get secret myapp-secrets -n myapp -o jsonpath='{.data.DATABASE_URL}' | base64 -d ``` ## Secret Naming Conventions The kit uses [CloudPosse null-label](https://github.com/cloudposse/terraform-null-label) for consistent resource naming. Secrets follow the same pattern: ``` {namespace}-{environment}-{stage}-{name} ``` | Component | Description | Examples | | ------------- | ------------------------- | ------------------------------------------------------- | | `namespace` | Organization abbreviation | `ksk` (kube-starter-kit), `myco`, etc. | | `environment` | AWS region abbreviation | `use2` (us-east-2), `use1` (us-east-1), `gbl` (global) | | `stage` | Deployment stage | `staging`, `prod`, `shared` | | `name` | Descriptive secret name | `myapp-db`, `argocd-github-dex`, `signoz-ingestion-key` | The examples below use `ksk` as the namespace (the kit's default). Replace this with the namespace you configured during [Bootstrap Accounts](/usage/getting-started/03-bootstrap-accounts). Examples: * `ksk-use2-staging-myapp-db` - Database credentials for myapp * `ksk-use2-staging-argocd-github-dex` - ArgoCD GitHub OAuth credentials * `ksk-use2-staging-signoz-ingestion-key` - SigNoz observability ingestion key This convention: * Aligns with all other Terraform-managed resources * Makes secrets easy to identify by cluster/environment * Enables fine-grained IAM policies using prefixes * Avoids naming conflicts across environments ## Fetch Multiple Values from One Secret AWS Secrets Manager secrets can contain JSON with multiple key/value pairs: ```bash theme={null} export REGION="us-east-2" # Your AWS region aws secretsmanager create-secret \ --name "ksk-use2-staging-myapp-config" \ --secret-string '{"DB_HOST":"localhost","DB_USER":"admin","DB_PASS":"secret"}' \ --region ${REGION} ``` ```yaml theme={null} apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: name: myapp-config namespace: myapp spec: refreshInterval: 1h secretStoreRef: name: aws-secrets-manager kind: ClusterSecretStore target: name: myapp-config data: - secretKey: DB_HOST remoteRef: key: ksk-use2-staging-myapp-config property: DB_HOST - secretKey: DB_USER remoteRef: key: ksk-use2-staging-myapp-config property: DB_USER - secretKey: DB_PASS remoteRef: key: ksk-use2-staging-myapp-config property: DB_PASS ``` ## Use Secrets in Pods Reference the synced Secret in your Deployment: ```yaml theme={null} apiVersion: apps/v1 kind: Deployment metadata: name: myapp spec: template: spec: containers: - name: myapp env: # Single environment variable - name: DATABASE_URL valueFrom: secretKeyRef: name: myapp-secrets key: DATABASE_URL # Or load all keys as env vars envFrom: - secretRef: name: myapp-config ``` ## Refresh and Sync Behavior ### Automatic Refresh ExternalSecrets periodically refresh from AWS based on `refreshInterval`: ```yaml theme={null} spec: refreshInterval: 1h # Check for updates every hour ``` ### Force Refresh To immediately sync a secret: ```bash theme={null} # Annotate to trigger refresh kubectl annotate externalsecret myapp-secrets -n myapp force-sync=$(date +%s) --overwrite ``` ### Reloader Integration The kit includes [Reloader](https://github.com/stakater/Reloader), which automatically restarts pods when their Secrets change: ```yaml theme={null} apiVersion: apps/v1 kind: Deployment metadata: name: myapp annotations: reloader.stakater.com/auto: "true" # Restart on any Secret/ConfigMap change ``` Or for specific secrets: ```yaml theme={null} metadata: annotations: secret.reloader.stakater.com/reload: "myapp-secrets" ``` ## Environment-Specific Secrets Use different secret paths for each environment: ```yaml theme={null} # values.yaml (staging) secrets: databaseUrlKey: ksk-use2-staging-myapp-db # values.production.yaml secrets: databaseUrlKey: ksk-use2-prod-myapp-db ``` Then template the ExternalSecret: ```yaml theme={null} apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: name: myapp-secrets spec: data: - secretKey: DATABASE_URL remoteRef: key: {{ .Values.secrets.databaseUrlKey }} ``` ## IAM Permissions The External Secrets service account needs permission to read secrets. This is configured via Pod Identity in the EKS Terraform module. By default, the External Secrets Pod Identity role has access to all secrets (`arn:aws:secretsmanager:*:*:secret:*`). To restrict access to specific prefixes, update the policy in `terraform/modules/eks/base-infra-resources.tf`: ```hcl theme={null} module "external_secrets_pod_identity" { # ... # Replace region and namespace prefix with your values external_secrets_secrets_manager_arns = [ "arn:aws:secretsmanager::*:secret:--staging-*", "arn:aws:secretsmanager::*:secret:--prod-*" ] } ``` ## Next Steps * [Database Operations](/usage/operations/07-database-operations) - Manage database credentials * [Bootstrapping a New Service](/usage/operations/04-bootstrapping-new-service) - Add secrets for new services # Database Operations Source: https://kubestarterkit.com/usage/operations/07-database-operations Manage CloudNativePG clusters and Atlas migrations ## Overview The kit uses two components for database management: * **[CloudNativePG](https://cloudnative-pg.io/)** - PostgreSQL operator for running databases in Kubernetes * **[Atlas](https://atlasgo.io/)** - Schema migration tool that runs as a Kubernetes Job with Argo CD sync wave ordering After Planetscale releases their [updated terraform provider](https://planetscale.com/changelog/terraform-provider-v1) (Jan 2026) I plan to replace CloudNativePG with that as the recommended approach for hosting application databases. ## Architecture ``` ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ │ Application │ │ Migration Job │ │ CloudNativePG │ │ Deployment │ │ (sync-wave: -1) │ │ Cluster │ └───────────────────┘ └───────────────────┘ └───────────────────┘ │ │ │ │ ▼ │ │ ┌───────────────┐ │ └────────────────>│ PostgreSQL │<────────────────┘ │ Primary │ └───────────────┘ │ ┌──────┴──────┐ │ │ ┌──────▼────┐ ┌──────▼────┐ │ Replica │ │ Replica │ └───────────┘ └───────────┘ ``` Argo CD sync waves ensure the migration Job completes before the application Deployment starts. The migration Job uses `sync-wave: -1` (or lower) to run first. ## CloudNativePG Clusters ### View Existing Clusters ```bash theme={null} # List all PostgreSQL clusters kubectl get clusters -A # Get cluster details kubectl describe cluster go-backend-cluster -n go-backend ``` ### Create a New Cluster Add a Cluster resource to your service's Kubernetes manifests: ```yaml theme={null} apiVersion: postgresql.cnpg.io/v1 kind: Cluster metadata: name: myapp-cluster namespace: myapp spec: instances: 3 storage: size: 10Gi storageClass: ebs-gp3-encrypted postgresql: parameters: max_connections: "100" shared_buffers: "256MB" bootstrap: initdb: database: myapp owner: myapp ``` ### Access the Database ```bash theme={null} # Get the connection password kubectl get secret myapp-cluster-app -n myapp -o jsonpath='{.data.password}' | base64 -d # Port-forward to the primary kubectl port-forward svc/myapp-cluster-rw -n myapp 5432:5432 # Connect with psql PGPASSWORD=$(kubectl get secret myapp-cluster-app -n myapp -o jsonpath='{.data.password}' | base64 -d) \ psql -h localhost -U myapp -d myapp ``` ### Connection Strings CloudNativePG creates services for different access patterns: | Service | Purpose | | ------------------ | ------------------------- | | `myapp-cluster-rw` | Read-write (primary only) | | `myapp-cluster-ro` | Read-only (replicas) | | `myapp-cluster-r` | Any instance | Connection string format: ``` postgres://myapp:@myapp-cluster-rw.myapp.svc:5432/myapp ``` ## Atlas Migrations ### Migration File Structure Migrations live in `services/{service}/migrations/`: ``` services/go-backend/ ├── migrations/ │ ├── 20251022011957_initial.sql │ ├── 20251022013319_add_id2.sql │ └── ... └── atlas.hcl ``` ### Create a New Migration Create a new file with timestamp prefix: ```bash theme={null} # Generate timestamp TIMESTAMP=$(date +%Y%m%d%H%M%S) # Create migration file touch services/go-backend/migrations/${TIMESTAMP}_add_email_column.sql ``` Write your migration: ```sql theme={null} -- Add email column to users table ALTER TABLE "users" ADD COLUMN "email" character varying; CREATE INDEX "users_email_idx" ON "users" ("email"); ``` Run the migration against your local database: ```bash theme={null} cd services/go-backend # Start local postgres mise run run-postgres # Apply migrations atlas migrate apply --env local ``` ```bash theme={null} git add services/go-backend/migrations/ git commit -m "feat: add email column to users" git push origin main ``` The migration Job runs automatically before the application starts, ordered via Argo CD sync waves. ### Migration Job Migrations run as a Kubernetes Job before the application starts. Argo CD sync waves ensure proper ordering: ```yaml theme={null} apiVersion: batch/v1 kind: Job metadata: name: myapp-migrations annotations: argocd.argoproj.io/sync-wave: "-1" # Run before Deployment (wave 0) argocd.argoproj.io/hook: Sync argocd.argoproj.io/hook-delete-policy: HookSucceeded spec: template: spec: containers: - name: migrate image: arigaio/atlas:latest command: ["atlas", "migrate", "apply", "--env", "kubernetes"] env: - name: DATABASE_URL valueFrom: secretKeyRef: name: myapp-cluster-app key: uri volumeMounts: - name: migrations mountPath: /migrations volumes: - name: migrations configMap: name: myapp-migrations ``` ### Atlas Configuration Configure Atlas in `atlas.hcl`: ```hcl theme={null} env "local" { url = "postgres://postgres:password@localhost:5432/myapp?sslmode=disable" migration { dir = "file://migrations" } } env "kubernetes" { url = getenv("DATABASE_URL") migration { dir = "file:///migrations" } } ``` ## Common Operations ### View Migration Status ```bash theme={null} # Check migration job status kubectl get jobs -n myapp # View migration logs kubectl logs job/myapp-migrations -n myapp ``` ### Rollback a Migration Atlas doesn't support automatic rollbacks. To rollback: 1. Create a new "down" migration that reverses the changes 2. Deploy the rollback migration ```sql theme={null} -- 20251023120000_rollback_email_column.sql DROP INDEX IF EXISTS "users_email_idx"; ALTER TABLE "users" DROP COLUMN IF EXISTS "email"; ``` ### Backup and Restore CloudNativePG supports continuous backup to S3: ```yaml theme={null} apiVersion: postgresql.cnpg.io/v1 kind: Cluster metadata: name: myapp-cluster spec: backup: barmanObjectStore: destinationPath: s3://myapp-backups/ s3Credentials: accessKeyId: name: aws-creds key: ACCESS_KEY_ID secretAccessKey: name: aws-creds key: SECRET_ACCESS_KEY wal: compression: gzip retentionPolicy: "30d" ``` Trigger a backup: ```bash theme={null} kubectl apply -f - <│ (DaemonSet) │────>│ │ └───────────────────┘ └───────────────────┘ └───────────────────┘ ▲ ┌───────────────────┐ │ │ Kubernetes │ │ │ Metrics/Logs │──────────────┘ └───────────────────┘ ``` ## SigNoz Setup Sign up at [signoz.io/teams](https://signoz.io/teams/) and create a new project. In SigNoz Cloud: 1. Go to **Settings → Ingestion Settings** 2. Copy the **Ingestion Key** 3. Note the **Region** (us or eu) The secret name follows the pattern `{prefix}-{region}-{env}-eks-signoz-apikey`. For example: ```bash theme={null} export REGION="us-east-2" # Your AWS region aws secretsmanager create-secret \ --name "ksk-use2-staging-eks-signoz-apikey" \ --secret-string '{"signoz-apikey": "your-ingestion-key"}' \ --region ${REGION} ``` Note the JSON format - the key name `signoz-apikey` must match the `apiKeyExistingSecretKey` value. The chart uses an umbrella wrapper around the upstream `k8s-infra` chart. Edit `kubernetes/src/infrastructure/signoz-k8s-infra/values.yaml` (base configuration): ```yaml theme={null} chartVersions: k8s-infra: "0.15.0" k8s-infra: global: cloud: aws otelCollectorEndpoint: ingest.us.signoz.cloud:443 otelInsecure: false apiKeyExistingSecretName: signoz-apikey apiKeyExistingSecretKey: signoz-apikey presets: otlpExporter: enabled: true logsCollection: enabled: true ``` Then configure environment-specific values in `values.staging.yaml`: ```yaml theme={null} signozApiKeyAwsSecret: ksk-use2-staging-eks-signoz-apikey k8s-infra: global: clusterName: ksk-use2-staging-eks deploymentEnvironment: staging ``` The ExternalSecret is already templated at `kubernetes/src/infrastructure/signoz-k8s-infra/templates/ExternalSecret.signoz-apikey.yaml`: ```yaml theme={null} apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: name: signoz-apikey namespace: {{.Release.Namespace}} spec: refreshInterval: 1h0m0s secretStoreRef: name: aws-secrets-manager kind: ClusterSecretStore target: name: signoz-apikey dataFrom: - extract: key: {{.Values.signozApiKeyAwsSecret}} ``` This extracts the secret from AWS Secrets Manager using the path defined in `signozApiKeyAwsSecret`. ## Instrument Your Application ### Go Applications The kit's `go-backend` example includes OpenTelemetry instrumentation: ```go theme={null} import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/sdk/trace" ) func initTracer() (*trace.TracerProvider, error) { exporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithEndpoint("k8s-infra-otel-agent.signoz.svc:4317"), otlptracegrpc.WithInsecure(), ) if err != nil { return nil, err } tp := trace.NewTracerProvider( trace.WithBatcher(exporter), trace.WithResource(resource.NewWithAttributes( semconv.ServiceNameKey.String("go-backend"), )), ) otel.SetTracerProvider(tp) return tp, nil } ``` ### Environment Variables Configure the OTel SDK via environment variables in your Deployment: ```yaml theme={null} env: - name: OTEL_EXPORTER_OTLP_ENDPOINT value: "http://k8s-infra-otel-agent.signoz.svc:4317" - name: OTEL_SERVICE_NAME value: "myapp" - name: OTEL_RESOURCE_ATTRIBUTES value: "service.namespace=myapp,deployment.environment=staging" ``` ## View Telemetry in SigNoz ### Traces 1. Open SigNoz Cloud dashboard 2. Navigate to **Traces** 3. Filter by service name or trace ID 4. Click a trace to see the waterfall view ### Metrics 1. Navigate to **Dashboards** 2. Create custom dashboards or use built-in templates 3. Query metrics using PromQL syntax ### Logs 1. Navigate to **Logs** 2. Filter by service, severity, or search text 3. Click a log line to see structured fields ## Kubernetes Metrics The signoz-k8s-infra collector automatically gathers: * Node metrics (CPU, memory, disk) * Pod metrics (resource usage, restarts) * Container metrics * Kubernetes events View in SigNoz under **Infrastructure → Kubernetes**. ## Custom Metrics Instrument your application with custom metrics: ```go theme={null} import ( "go.opentelemetry.io/otel/metric" ) var ( requestCounter metric.Int64Counter requestLatency metric.Float64Histogram ) func initMetrics(mp metric.MeterProvider) { meter := mp.Meter("myapp") requestCounter, _ = meter.Int64Counter("myapp.requests.total", metric.WithDescription("Total number of requests"), ) requestLatency, _ = meter.Float64Histogram("myapp.request.duration", metric.WithDescription("Request duration in milliseconds"), metric.WithUnit("ms"), ) } func handleRequest(w http.ResponseWriter, r *http.Request) { start := time.Now() defer func() { requestCounter.Add(r.Context(), 1) requestLatency.Record(r.Context(), float64(time.Since(start).Milliseconds())) }() // ... handle request } ``` ## Alerts Configure alerts in SigNoz Cloud: 1. Go to **Alerts → New Alert** 2. Define the condition (e.g., error rate > 5%) 3. Set severity and notification channels Supported channels: * Slack * PagerDuty * Email * Webhooks ## Local Development For local development with Tilt, signoz-k8s-infra is not deployed by default. You can: 1. **Skip observability locally** - Traces go nowhere, which is fine for development 2. **Run SigNoz locally** (optional): ```bash theme={null} docker run -d --name signoz \ -p 3301:3301 -p 4317:4317 \ signoz/signoz:latest ``` 3. **Configure the collector endpoint**: ```yaml theme={null} # values.local.yaml otelCollectorEndpoint: host.docker.internal:4317 ``` ## Troubleshooting ### No traces appearing in SigNoz 1. Verify the collector is running: ```bash theme={null} kubectl get pods -n signoz ``` 2. Check collector logs: ```bash theme={null} kubectl logs -n signoz -l app.kubernetes.io/name=otel-collector ``` 3. Verify the ingestion key secret exists: ```bash theme={null} kubectl get secret signoz-apikey -n signoz -o yaml ``` 4. Check application OTel configuration: ```bash theme={null} kubectl logs -n myapp deployment/myapp | grep -i otel ``` ### High cardinality warnings If you see cardinality warnings: 1. Review your metric labels/attributes 2. Avoid high-cardinality values (user IDs, request IDs) 3. Use buckets for continuous values ### Missing Kubernetes metrics 1. Verify the kube-state-metrics pod is running 2. Check RBAC permissions for the collector service account 3. Review collector configuration for kubelet access ## Switching Observability Providers The kit is designed to work with any OpenTelemetry-compatible backend. To switch from SigNoz: 1. **Disable signoz-k8s-infra** in `kubernetes/src/argocd/infrastructure/values.yaml` 2. **Add your provider's collector** (e.g., Datadog, Grafana Cloud) 3. **Update application OTLP endpoints** to point to the new collector ## Best Practices 1. **Use semantic conventions** - Follow OpenTelemetry naming standards 2. **Set service.name** - Always identify your service in traces 3. **Add context** - Include request IDs, user IDs (hashed) in spans 4. **Sample appropriately** - Use head-based sampling for high-volume services 5. **Alert on SLOs** - Focus alerts on user-impacting metrics