From 4d9102a6e09a90ce5a2d18067726374e59dd32af Mon Sep 17 00:00:00 2001 From: Rowan Stein Date: Tue, 24 Feb 2026 02:07:01 +0000 Subject: [PATCH 1/3] feat(system+ci): add Istio/Argo CD system stack and CI to apply stacks in order --- .github/workflows/bootstrap.yml | 55 +++++++++++++++++++++ stacks/system/main.tf | 86 +++++++++++++++++++++++++++++++++ stacks/system/namespaces.tf | 11 +++++ stacks/system/outputs.tf | 18 +++++++ stacks/system/providers.tf | 9 ++++ stacks/system/variables.tf | 17 +++++++ stacks/system/versions.tf | 16 ++++++ 7 files changed, 212 insertions(+) create mode 100644 .github/workflows/bootstrap.yml create mode 100644 stacks/system/main.tf create mode 100644 stacks/system/namespaces.tf create mode 100644 stacks/system/outputs.tf create mode 100644 stacks/system/providers.tf create mode 100644 stacks/system/variables.tf create mode 100644 stacks/system/versions.tf diff --git a/.github/workflows/bootstrap.yml b/.github/workflows/bootstrap.yml new file mode 100644 index 00000000..8a67872c --- /dev/null +++ b/.github/workflows/bootstrap.yml @@ -0,0 +1,55 @@ +name: bootstrap +on: + pull_request: + branches: [ main ] + push: + branches: [ main ] + +jobs: + full-apply: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Verify Docker + run: | + docker version || (echo "Docker is required" && exit 1) + + - name: Install k3d + run: | + curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash + k3d version + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.6.6 + + - name: Apply k8s stack + working-directory: stacks/k8s + run: | + terraform init -input=false + terraform apply -auto-approve -input=false + + - name: Apply system stack (Istio + Argo CD) + working-directory: stacks/system + env: + TF_VAR_kubeconfig_path: "../k8s/.kube/agyn-local-kubeconfig.yaml" + run: | + terraform init -input=false + terraform apply -auto-approve -input=false + + - name: Destroy system stack (PR only) + if: ${{ github.event_name == 'pull_request' }} + working-directory: stacks/system + env: + TF_VAR_kubeconfig_path: "../k8s/.kube/agyn-local-kubeconfig.yaml" + run: | + terraform destroy -auto-approve -input=false || true + + - name: Destroy k8s stack (PR only) + if: ${{ github.event_name == 'pull_request' }} + working-directory: stacks/k8s + run: | + terraform destroy -auto-approve -input=false || true diff --git a/stacks/system/main.tf b/stacks/system/main.tf new file mode 100644 index 00000000..c1aa52b8 --- /dev/null +++ b/stacks/system/main.tf @@ -0,0 +1,86 @@ +# Helm repositories +resource "helm_repository" "istio" { + name = "istio" + url = "https://istio-release.storage.googleapis.com/charts" +} + +resource "helm_repository" "argo" { + name = "argo" + url = "https://argoproj.github.io/argo-helm" +} + +# Istio base (CRDs) +resource "helm_release" "istio_base" { + name = "istio-base" + repository = helm_repository.istio.url + chart = "base" + version = var.istio_chart_version + namespace = kubernetes_namespace.istio_system.metadata[0].name +} + +# Istio control plane +resource "helm_release" "istiod" { + name = "istiod" + repository = helm_repository.istio.url + chart = "istiod" + version = var.istio_chart_version + namespace = kubernetes_namespace.istio_system.metadata[0].name + + depends_on = [helm_release.istio_base] + + values = [ + yamlencode({ + pilot = { + traceSampling = 1.0 + } + }) + ] +} + +# Istio gateway (minimal) +resource "helm_release" "istio_gateway" { + name = "istio-gateway" + repository = helm_repository.istio.url + chart = "gateway" + version = var.istio_chart_version + namespace = kubernetes_namespace.istio_gateway.metadata[0].name + + depends_on = [helm_release.istiod] + + values = [ + yamlencode({ + name = "istio-ingressgateway", + service = { + type = "ClusterIP", + ports = [{ + name = "http2", + port = 80, + targetPort = 8080 + },{ + name = "https", + port = 443, + targetPort = 8443 + }] + } + }) + ] +} + +# Argo CD +resource "helm_release" "argo_cd" { + name = "argo-cd" + repository = helm_repository.argo.url + chart = "argo-cd" + version = var.argocd_chart_version + namespace = kubernetes_namespace.argocd.metadata[0].name + + values = [ + yamlencode({ + server = { + service = { + type = "ClusterIP" + } + } + }) + ] +} diff --git a/stacks/system/namespaces.tf b/stacks/system/namespaces.tf new file mode 100644 index 00000000..a5bd094c --- /dev/null +++ b/stacks/system/namespaces.tf @@ -0,0 +1,11 @@ +resource "kubernetes_namespace" "istio_system" { + metadata { name = "istio-system" } +} + +resource "kubernetes_namespace" "istio_gateway" { + metadata { name = "istio-gateway" } +} + +resource "kubernetes_namespace" "argocd" { + metadata { name = "argocd" } +} diff --git a/stacks/system/outputs.tf b/stacks/system/outputs.tf new file mode 100644 index 00000000..423181d3 --- /dev/null +++ b/stacks/system/outputs.tf @@ -0,0 +1,18 @@ +output "installed_namespaces" { + value = [ + kubernetes_namespace.istio_system.metadata[0].name, + kubernetes_namespace.istio_gateway.metadata[0].name, + kubernetes_namespace.argocd.metadata[0].name, + ] + description = "Installed namespaces" +} + +output "releases" { + value = [ + helm_release.istio_base.name, + helm_release.istiod.name, + helm_release.istio_gateway.name, + helm_release.argo_cd.name, + ] + description = "Installed Helm releases" +} diff --git a/stacks/system/providers.tf b/stacks/system/providers.tf new file mode 100644 index 00000000..08fb4a2f --- /dev/null +++ b/stacks/system/providers.tf @@ -0,0 +1,9 @@ +provider "kubernetes" { + config_path = var.kubeconfig_path +} + +provider "helm" { + kubernetes { + config_path = var.kubeconfig_path + } +} diff --git a/stacks/system/variables.tf b/stacks/system/variables.tf new file mode 100644 index 00000000..dbf108b4 --- /dev/null +++ b/stacks/system/variables.tf @@ -0,0 +1,17 @@ +variable "kubeconfig_path" { + type = string + description = "Path to kubeconfig for connecting to the cluster" + default = "../k8s/.kube/agyn-local-kubeconfig.yaml" +} + +variable "istio_chart_version" { + type = string + description = "Istio chart version" + default = "1.21.0" +} + +variable "argocd_chart_version" { + type = string + description = "Argo CD chart version" + default = "5.33.0" +} diff --git a/stacks/system/versions.tf b/stacks/system/versions.tf new file mode 100644 index 00000000..12b1db4e --- /dev/null +++ b/stacks/system/versions.tf @@ -0,0 +1,16 @@ +terraform { + required_version = ">= 1.5.0" + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.27" + } + helm = { + source = "hashicorp/helm" + version = "~> 2.13" + } + } + backend "local" { + path = "./state/terraform.tfstate" + } +} From 63899054c5f20565d8c6af069e531639f60cfc67 Mon Sep 17 00:00:00 2001 From: Casey Brooks Date: Tue, 24 Feb 2026 02:30:19 +0000 Subject: [PATCH 2/3] fix(stacks): restore kubeconfig and locks --- .github/workflows/bootstrap.yml | 10 +++--- stacks/k8s/.terraform.lock.hcl | 36 +++++++++++++++++++++ stacks/k8s/main.tf | 53 ++++++++++++++++++++++++------- stacks/k8s/outputs.tf | 4 +-- stacks/k8s/versions.tf | 5 +++ stacks/system/.terraform.lock.hcl | 42 ++++++++++++++++++++++++ stacks/system/main.tf | 21 +++++------- stacks/system/outputs.tf | 4 +-- 8 files changed, 142 insertions(+), 33 deletions(-) create mode 100644 stacks/k8s/.terraform.lock.hcl create mode 100644 stacks/system/.terraform.lock.hcl diff --git a/.github/workflows/bootstrap.yml b/.github/workflows/bootstrap.yml index 8a67872c..c83a3b20 100644 --- a/.github/workflows/bootstrap.yml +++ b/.github/workflows/bootstrap.yml @@ -41,15 +41,17 @@ jobs: terraform apply -auto-approve -input=false - name: Destroy system stack (PR only) - if: ${{ github.event_name == 'pull_request' }} + if: ${{ always() && github.event_name == 'pull_request' }} working-directory: stacks/system env: TF_VAR_kubeconfig_path: "../k8s/.kube/agyn-local-kubeconfig.yaml" run: | - terraform destroy -auto-approve -input=false || true + terraform init -input=false + terraform destroy -auto-approve -input=false - name: Destroy k8s stack (PR only) - if: ${{ github.event_name == 'pull_request' }} + if: ${{ always() && github.event_name == 'pull_request' }} working-directory: stacks/k8s run: | - terraform destroy -auto-approve -input=false || true + terraform init -input=false + terraform destroy -auto-approve -input=false diff --git a/stacks/k8s/.terraform.lock.hcl b/stacks/k8s/.terraform.lock.hcl new file mode 100644 index 00000000..aef4d3aa --- /dev/null +++ b/stacks/k8s/.terraform.lock.hcl @@ -0,0 +1,36 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/agynio/k3d" { + version = "0.1.0" + constraints = "~> 0.1.0" + hashes = [ + "h1:v+217qQKC+6q+lDovZxuOvH3gJauUUMjVgpoQfZJyhE=", + "zh:2021f433bf155271c7563f17f8029c5db7993a8dd1da7e8cf37390923e52a6d4", + "zh:614895eba80d4418ff28b53c96bd7c214fe07fd8680023ee8043ccc650e0d82c", + "zh:6465f43a71bbd7185fe839d6856741b5db7215c8fd12408be926e9125aca148c", + "zh:691e65575d82ae456ff0e3822515dce6ac5499df02990b4f3c885a4e1955b59f", + "zh:80bf5414c85bd1c5cc3cd9186018117b44aff9a9548df3f2f1091642d74b6ac4", + "zh:c12a9a85cab5efe80dde8f8e6f8310c25adcb1a67019e3064b3781dc3d361bd0", + ] +} + +provider "registry.terraform.io/hashicorp/local" { + version = "2.7.0" + constraints = "~> 2.5" + hashes = [ + "h1:2RYa3j7m/0WmET2fqotY4CHxE1Hpk0fgn47/126l+Og=", + "zh:261fec71bca13e0a7812dc0d8ae9af2b4326b24d9b2e9beab3d2400fab5c5f9a", + "zh:308da3b5376a9ede815042deec5af1050ec96a5a5410a2206ae847d82070a23e", + "zh:3d056924c420464dc8aba10e1915956b2e5c4d55b11ffff79aa8be563fbfe298", + "zh:643256547b155459c45e0a3e8aab0570db59923c68daf2086be63c444c8c445b", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7aa4d0b853f84205e8cf79f30c9b2c562afbfa63592f7231b6637e5d7a6b5b27", + "zh:7dc251bbc487d58a6ab7f5b07ec9edc630edb45d89b761dba28e0e2ba6b1c11f", + "zh:7ee0ca546cd065030039168d780a15cbbf1765a4c70cd56d394734ab112c93da", + "zh:b1d5d80abb1906e6c6b3685a52a0192b4ca6525fe090881c64ec6f67794b1300", + "zh:d81ea9856d61db3148a4fc6c375bf387a721d78fc1fea7a8823a027272a47a78", + "zh:df0a1f0afc947b8bfc88617c1ad07a689ce3bd1a29fd97318392e6bdd32b230b", + "zh:dfbcad800240e0c68c43e0866f2a751cff09777375ec701918881acf67a268da", + ] +} diff --git a/stacks/k8s/main.tf b/stacks/k8s/main.tf index a18947d3..cd1fcee8 100644 --- a/stacks/k8s/main.tf +++ b/stacks/k8s/main.tf @@ -1,5 +1,6 @@ locals { - kubeconfig_path = "${path.module}/.kube/${var.cluster_name}-kubeconfig.yaml" + kubeconfig_dir = "${path.module}/.kube" + kubeconfig_path = "${local.kubeconfig_dir}/${var.cluster_name}-kubeconfig.yaml" } resource "k3d_cluster" "this" { @@ -7,19 +8,47 @@ resource "k3d_cluster" "this" { servers = var.servers agents = var.agents - k3s_version = var.k3s_version - k3s_extra_args = var.k3s_extra_args + image = "rancher/k3s:${var.k3s_version}" - expose_api = var.expose_api - api_port = var.api_port + dynamic "kube_api" { + for_each = var.expose_api ? [true] : [] - ports = [ - for p in var.ports : { - container_port = p.container_port - host_port = p.host_port - protocol = p.protocol + content { + host = "127.0.0.1" + host_ip = "127.0.0.1" + host_port = var.api_port } - ] + } - kubeconfig_path = local.kubeconfig_path + dynamic "k3s" { + for_each = length(var.k3s_extra_args) > 0 ? [true] : [] + + content { + dynamic "extra_args" { + for_each = var.k3s_extra_args + + content { + arg = extra_args.value + } + } + } + } + + dynamic "port" { + for_each = var.ports + + content { + host = try(port.value.host, "") + host_port = port.value.host_port + container_port = port.value.container_port + protocol = upper(port.value.protocol) + } + } +} + +resource "local_sensitive_file" "kubeconfig" { + filename = local.kubeconfig_path + content = one(k3d_cluster.this.credentials).raw + file_permission = "0600" + directory_permission = "0700" } diff --git a/stacks/k8s/outputs.tf b/stacks/k8s/outputs.tf index a4f4350d..97aa2ff2 100644 --- a/stacks/k8s/outputs.tf +++ b/stacks/k8s/outputs.tf @@ -4,7 +4,7 @@ output "cluster_name" { } output "kubeconfig_path" { - value = k3d_cluster.this.kubeconfig_path + value = local.kubeconfig_path description = "Local kubeconfig path" } @@ -19,6 +19,6 @@ output "agents" { } output "kube_api_endpoint" { - value = var.expose_api ? "https://127.0.0.1:${var.api_port}" : null + value = var.expose_api ? format("https://127.0.0.1:%d", var.api_port) : null description = "Local Kubernetes API endpoint (if exposed)" } diff --git a/stacks/k8s/versions.tf b/stacks/k8s/versions.tf index ca3e9c88..49719252 100644 --- a/stacks/k8s/versions.tf +++ b/stacks/k8s/versions.tf @@ -5,6 +5,11 @@ terraform { source = "agynio/k3d" version = "~> 0.1.0" } + + local = { + source = "hashicorp/local" + version = "~> 2.5" + } } backend "local" { path = "./state/terraform.tfstate" diff --git a/stacks/system/.terraform.lock.hcl b/stacks/system/.terraform.lock.hcl new file mode 100644 index 00000000..33329500 --- /dev/null +++ b/stacks/system/.terraform.lock.hcl @@ -0,0 +1,42 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/helm" { + version = "2.17.0" + constraints = "~> 2.13" + hashes = [ + "h1:K5FEjxvDnxb1JF1kG1xr8J3pNGxoaR3Z0IBG9Csm/Is=", + "zh:06fb4e9932f0afc1904d2279e6e99353c2ddac0d765305ce90519af410706bd4", + "zh:104eccfc781fc868da3c7fec4385ad14ed183eb985c96331a1a937ac79c2d1a7", + "zh:129345c82359837bb3f0070ce4891ec232697052f7d5ccf61d43d818912cf5f3", + "zh:3956187ec239f4045975b35e8c30741f701aa494c386aaa04ebabffe7749f81c", + "zh:66a9686d92a6b3ec43de3ca3fde60ef3d89fb76259ed3313ca4eb9bb8c13b7dd", + "zh:88644260090aa621e7e8083585c468c8dd5e09a3c01a432fb05da5c4623af940", + "zh:a248f650d174a883b32c5b94f9e725f4057e623b00f171936dcdcc840fad0b3e", + "zh:aa498c1f1ab93be5c8fbf6d48af51dc6ef0f10b2ea88d67bcb9f02d1d80d3930", + "zh:bf01e0f2ec2468c53596e027d376532a2d30feb72b0b5b810334d043109ae32f", + "zh:c46fa84cc8388e5ca87eb575a534ebcf68819c5a5724142998b487cb11246654", + "zh:d0c0f15ffc115c0965cbfe5c81f18c2e114113e7a1e6829f6bfd879ce5744fbb", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "2.38.0" + constraints = "~> 2.27" + hashes = [ + "h1:5CkveFo5ynsLdzKk+Kv+r7+U9rMrNjfZPT3a0N/fhgE=", + "zh:0af928d776eb269b192dc0ea0f8a3f0f5ec117224cd644bdacdc682300f84ba0", + "zh:1be998e67206f7cfc4ffe77c01a09ac91ce725de0abaec9030b22c0a832af44f", + "zh:326803fe5946023687d603f6f1bab24de7af3d426b01d20e51d4e6fbe4e7ec1b", + "zh:4a99ec8d91193af961de1abb1f824be73df07489301d62e6141a656b3ebfff12", + "zh:5136e51765d6a0b9e4dbcc3b38821e9736bd2136cf15e9aac11668f22db117d2", + "zh:63fab47349852d7802fb032e4f2b6a101ee1ce34b62557a9ad0f0f0f5b6ecfdc", + "zh:924fb0257e2d03e03e2bfe9c7b99aa73c195b1f19412ca09960001bee3c50d15", + "zh:b63a0be5e233f8f6727c56bed3b61eb9456ca7a8bb29539fba0837f1badf1396", + "zh:d39861aa21077f1bc899bc53e7233262e530ba8a3a2d737449b100daeb303e4d", + "zh:de0805e10ebe4c83ce3b728a67f6b0f9d18be32b25146aa89116634df5145ad4", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + "zh:faf23e45f0090eef8ba28a8aac7ec5d4fdf11a36c40a8d286304567d71c1e7db", + ] +} diff --git a/stacks/system/main.tf b/stacks/system/main.tf index c1aa52b8..0ff6f43b 100644 --- a/stacks/system/main.tf +++ b/stacks/system/main.tf @@ -1,18 +1,13 @@ # Helm repositories -resource "helm_repository" "istio" { - name = "istio" - url = "https://istio-release.storage.googleapis.com/charts" -} - -resource "helm_repository" "argo" { - name = "argo" - url = "https://argoproj.github.io/argo-helm" +locals { + istio_repository_url = "https://istio-release.storage.googleapis.com/charts" + argo_repository_url = "https://argoproj.github.io/argo-helm" } # Istio base (CRDs) resource "helm_release" "istio_base" { name = "istio-base" - repository = helm_repository.istio.url + repository = local.istio_repository_url chart = "base" version = var.istio_chart_version namespace = kubernetes_namespace.istio_system.metadata[0].name @@ -21,7 +16,7 @@ resource "helm_release" "istio_base" { # Istio control plane resource "helm_release" "istiod" { name = "istiod" - repository = helm_repository.istio.url + repository = local.istio_repository_url chart = "istiod" version = var.istio_chart_version namespace = kubernetes_namespace.istio_system.metadata[0].name @@ -40,7 +35,7 @@ resource "helm_release" "istiod" { # Istio gateway (minimal) resource "helm_release" "istio_gateway" { name = "istio-gateway" - repository = helm_repository.istio.url + repository = local.istio_repository_url chart = "gateway" version = var.istio_chart_version namespace = kubernetes_namespace.istio_gateway.metadata[0].name @@ -56,7 +51,7 @@ resource "helm_release" "istio_gateway" { name = "http2", port = 80, targetPort = 8080 - },{ + }, { name = "https", port = 443, targetPort = 8443 @@ -69,7 +64,7 @@ resource "helm_release" "istio_gateway" { # Argo CD resource "helm_release" "argo_cd" { name = "argo-cd" - repository = helm_repository.argo.url + repository = local.argo_repository_url chart = "argo-cd" version = var.argocd_chart_version namespace = kubernetes_namespace.argocd.metadata[0].name diff --git a/stacks/system/outputs.tf b/stacks/system/outputs.tf index 423181d3..5109cb67 100644 --- a/stacks/system/outputs.tf +++ b/stacks/system/outputs.tf @@ -1,5 +1,5 @@ output "installed_namespaces" { - value = [ + value = [ kubernetes_namespace.istio_system.metadata[0].name, kubernetes_namespace.istio_gateway.metadata[0].name, kubernetes_namespace.argocd.metadata[0].name, @@ -8,7 +8,7 @@ output "installed_namespaces" { } output "releases" { - value = [ + value = [ helm_release.istio_base.name, helm_release.istiod.name, helm_release.istio_gateway.name, From 9f3644f2290d8cce53b8b645263baefb09e474f6 Mon Sep 17 00:00:00 2001 From: Casey Brooks Date: Tue, 24 Feb 2026 03:05:52 +0000 Subject: [PATCH 3/3] fix(ci): use default kubeconfig var --- .github/workflows/bootstrap.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/bootstrap.yml b/.github/workflows/bootstrap.yml index c83a3b20..da7a1e7d 100644 --- a/.github/workflows/bootstrap.yml +++ b/.github/workflows/bootstrap.yml @@ -34,8 +34,6 @@ jobs: - name: Apply system stack (Istio + Argo CD) working-directory: stacks/system - env: - TF_VAR_kubeconfig_path: "../k8s/.kube/agyn-local-kubeconfig.yaml" run: | terraform init -input=false terraform apply -auto-approve -input=false @@ -43,8 +41,6 @@ jobs: - name: Destroy system stack (PR only) if: ${{ always() && github.event_name == 'pull_request' }} working-directory: stacks/system - env: - TF_VAR_kubeconfig_path: "../k8s/.kube/agyn-local-kubeconfig.yaml" run: | terraform init -input=false terraform destroy -auto-approve -input=false