diff --git a/docs/patterns/gitops-multi-cluster-hub-spoke-argocd.md b/docs/patterns/gitops-multi-cluster-hub-spoke-argocd.md
new file mode 100644
index 0000000000..0ef45bb13a
--- /dev/null
+++ b/docs/patterns/gitops-multi-cluster-hub-spoke-argocd.md
@@ -0,0 +1,7 @@
+---
+title: GitOps Multi-Cluster Hub-Spoke Topology (ArgoCD)
+---
+
+{%
+ include-markdown "../../patterns/gitops/multi-cluster-hub-spoke-argocd/README.md"
+%}
diff --git a/patterns/gitops/getting-started-argocd/README.md b/patterns/gitops/getting-started-argocd/README.md
index 80c0e06663..3cdf8e6d16 100644
--- a/patterns/gitops/getting-started-argocd/README.md
+++ b/patterns/gitops/getting-started-argocd/README.md
@@ -2,7 +2,7 @@
This tutorial guides you through deploying an Amazon EKS cluster with addons configured via ArgoCD, employing the [GitOps Bridge Pattern](https://github.com/gitops-bridge-dev).
-
+
The [GitOps Bridge Pattern](https://github.com/gitops-bridge-dev) enables Kubernetes administrators to utilize Infrastructure as Code (IaC) and GitOps tools for deploying Kubernetes Addons and Workloads. Addons often depend on Cloud resources that are external to the cluster. The configuration metadata for these external resources is required by the Addons' Helm charts. While IaC is used to create these cloud resources, it is not used to install the Helm charts. Instead, the IaC tool stores this metadata either within GitOps resources in the cluster or in a Git repository. The GitOps tool then extracts these metadata values and passes them to the Helm chart during the Addon installation process. This mechanism forms the bridge between IaC and GitOps, hence the term "GitOps Bridge."
@@ -38,7 +38,7 @@ terraform apply -target="module.vpc" -auto-approve
terraform apply -target="module.eks" -auto-approve
terraform apply -auto-approve
```
-Retrieve `kubectl` config, then execute the output command:
+To retrieve `kubectl` config, execute the terraform output command:
```shell
terraform output -raw configure_kubectl
```
@@ -110,7 +110,7 @@ Wait until all the ArgoCD applications' `HEALTH STATUS` is `Healthy`.
Use `Ctrl+C` or `Cmd+C` to exit the `watch` command. ArgoCD Applications
can take a couple of minutes in order to achieve the Healthy status.
```shell
-watch kubectl get applications -n argocd
+kubectl get applications -n argocd -w
```
The expected output should look like the following:
```text
diff --git a/patterns/gitops/multi-cluster-hub-spoke-argocd/README.md b/patterns/gitops/multi-cluster-hub-spoke-argocd/README.md
new file mode 100644
index 0000000000..c61d2d2197
--- /dev/null
+++ b/patterns/gitops/multi-cluster-hub-spoke-argocd/README.md
@@ -0,0 +1,191 @@
+# Multi-Cluster centralized hub-spoke topology
+
+This tutorial guides you through deploying an Amazon EKS cluster with addons configured via ArgoCD in a Multi-Cluster Hub-Spoke topoloy, employing the [GitOps Bridge Pattern](https://github.com/gitops-bridge-dev).
+
+
+
+
+This example deploys ArgoCD on the Hub cluster (i.e. management/control-plane cluster).
+The spoke clusters are registered as remote clusters in the Hub Cluster's ArgoCD
+The ArgoCD on the Hub Cluster deploys addons and workloads to the spoke clusters
+
+Each spoke cluster gets deployed an app of apps ArgoCD Application with the name `workloads-${env}`
+
+## Prerequisites
+Before you begin, make sure you have the following command line tools installed:
+- git
+- terraform
+- kubectl
+- argocd
+
+## (Optional) Fork the GitOps git repositories
+See the appendix section [Fork GitOps Repositories](#fork-gitops-repositories) for more info on the terraform variables to override.
+
+## Deploy the Hub EKS Cluster
+Change directory to `hub`
+```shell
+cd hub
+```
+Initialize Terraform and deploy the EKS cluster:
+```shell
+terraform init
+terraform apply -target="module.vpc" -auto-approve
+terraform apply -target="module.eks" -auto-approve
+terraform apply -auto-approve
+```
+To retrieve `kubectl` config, execute the terraform output command:
+```shell
+terraform output -raw configure_kubectl
+```
+The expected output will have two lines you run in your terminal
+```text
+export KUBECONFIG="/tmp/hub-spoke"
+aws eks --region us-west-2 update-kubeconfig --name getting-started-gitops --alias hub
+```
+>The first line sets the `KUBECONFIG` environment variable to a temporary file
+that includes the cluster name. The second line uses the `aws` CLI to populate
+that temporary file with the `kubectl` configuration. This approach offers the
+advantage of not altering your existing `kubectl` context, allowing you to work
+in other terminal windows without interference.
+
+### Monitor GitOps Progress for Addons
+Wait until all the ArgoCD applications' `HEALTH STATUS` is `Healthy`.
+Use `Ctrl+C` or `Cmd+C` to exit the `watch` command. ArgoCD Applications
+can take a couple of minutes in order to achieve the Healthy status.
+```shell
+kubectl --context hub get applications -n argocd -w
+```
+The expected output should look like the following:
+```text
+NAME SYNC STATUS HEALTH STATUS
+addon-in-cluster-argo-cd Synced Healthy
+addon-in-cluster-aws-load-balancer-controller Synced Healthy
+addon-in-cluster-metrics-server Synced Healthy
+cluster-addons Synced Healthy
+```
+
+## (Optional) Access ArgoCD
+Access to the ArgoCD's UI is completely optional, if you want to do it,
+run the commands shown in the Terraform output as the example below:
+```shell
+terraform output -raw access_argocd
+```
+The expected output should contain the `kubectl` config followed by `kubectl` command to retrieve
+the URL, username, password to login into ArgoCD UI or CLI.
+```text
+echo "ArgoCD Username: admin"
+echo "ArgoCD Password: $(kubectl --context hub get secrets argocd-initial-admin-secret -n argocd --template="{{index .data.password | base64decode}}")"
+echo "ArgoCD URL: https://$(kubectl --context hub get svc -n argocd argo-cd-argocd-server -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')"
+```
+
+
+## Verify that ArgoCD Service Accouts has the annotation for IRSA
+```shell
+kubectl --context hub get sa -n argocd argocd-application-controller -o json | jq '.metadata.annotations."eks.amazonaws.com/role-arn"'
+kubectl --context hub get sa -n argocd argocd-server -o json | jq '.metadata.annotations."eks.amazonaws.com/role-arn"'
+```
+The output should match the `arn` for the IAM Role that will assume the IAM Role in spoke/remote clusters
+```text
+arn:aws:iam::0123456789:role/argocd-hub-0123abc..
+arn:aws:iam::0123456789:role/argocd-hub-0123abc..
+```
+
+## Deploy the Spoke EKS Cluster
+Use the `deploy.sh` script to create terraform workspace, initialize Terraform, and deploy the EKS clusters:
+```shell
+cd ../spokes
+./deploy.sh dev
+./deploy.sh staging
+./deploy.sh prod
+```
+Each environment uses a Terraform workspace
+
+To retrieve `kubectl` config, execute the terraform output command:
+```shell
+terraform workspace select dev
+terraform output -raw configure_kubectl
+```
+```shell
+terraform workspace select staging
+terraform output -raw configure_kubectl
+```
+```shell
+terraform workspace select prod
+terraform output -raw configure_kubectl
+```
+
+
+### Verify ArgoCD Cluster Secret for Spokes have the correct IAM Role to be assume by Hub Cluster
+```shell
+for i in dev staging prod ; do echo $i && kubectl --context hub get secret -n argocd spoke-$i --template='{{index .data.config | base64decode}}' ; done
+```
+The output have a section `awsAuthConfig` with the `clusterName` and the `roleARN` that has write access to the spoke cluster
+```json
+{
+ "tlsClientConfig": {
+ "insecure": false,
+ "caData" : "LS0tL...."
+ },
+ "awsAuthConfig" : {
+ "clusterName": "hub-spoke-dev",
+ "roleARN": "arn:aws:iam::0123456789:role/hub-spoke-dev-argocd-spoke"
+ }
+}
+```
+
+
+### Verify the Addons on Spoke Clusters
+Verify that the addons are ready:
+```shell
+for i in dev staging prod ; do echo $i && kubectl --context $i get deployment -n kube-system ; done
+```
+
+
+### Monitor GitOps Progress for Workloads from Hub Cluster (run on Hub Cluster context)
+Watch until **all* the Workloads ArgoCD Applications are `Healthy`
+```shell
+kubectl --context hub get -n argocd applications -w
+```
+Wait until the ArgoCD Applications `HEALTH STATUS` is `Healthy`. Crl+C to exit the `watch` command
+
+
+### Verify the Application
+Verify that the application configuration is present and the pod is running:
+```shell
+for i in dev staging prod ; do echo $i && kubectl --context $i get all -n workload ; done
+```
+
+### Container Metrics
+Check the application's CPU and memory metrics:
+```shell
+for i in dev staging prod ; do echo $i && kubectl --context $i top pods -n workload ; done
+```
+
+## Destroy the Spoke EKS Clusters
+To tear down all the resources and the EKS cluster, run the following command:
+```shell
+./destroy.sh dev
+./destroy.sh staging
+./destroy.sh prod
+```
+
+## Destroy the Hub EKS Clusters
+To tear down all the resources and the EKS cluster, run the following command:
+Destroy Hub Clusters
+```shell
+cd ../hub
+./destroy.sh
+```
+
+## Appendix
+
+## Fork GitOps Repositories
+To modify the `values.yaml` file or the helm chart version for addons, you'll need to fork tthe repository [aws-samples/eks-blueprints-add-ons](https://github.com/aws-samples/eks-blueprints-add-ons).
+
+After forking, update the following environment variables to point to your forks, replacing the default values.
+```shell
+export TF_VAR_gitops_addons_org=https://github.com/aws-samples
+export TF_VAR_gitops_addons_repo=eks-blueprints-add-ons
+export TF_VAR_gitops_addons_revision=main
+
+```
diff --git a/patterns/gitops/multi-cluster-hub-spoke-argocd/hub/bootstrap/addons.yaml b/patterns/gitops/multi-cluster-hub-spoke-argocd/hub/bootstrap/addons.yaml
new file mode 100644
index 0000000000..89f3602844
--- /dev/null
+++ b/patterns/gitops/multi-cluster-hub-spoke-argocd/hub/bootstrap/addons.yaml
@@ -0,0 +1,32 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ApplicationSet
+metadata:
+ name: cluster-addons
+ namespace: argocd
+spec:
+ syncPolicy:
+ preserveResourcesOnDeletion: true
+ generators:
+ - clusters:
+ selector:
+ matchExpressions:
+ - key: akuity.io/argo-cd-cluster-name
+ operator: NotIn
+ values: [in-cluster]
+ template:
+ metadata:
+ name: cluster-addons
+ spec:
+ project: default
+ source:
+ repoURL: '{{metadata.annotations.addons_repo_url}}'
+ path: '{{metadata.annotations.addons_repo_basepath}}{{metadata.annotations.addons_repo_path}}'
+ targetRevision: '{{metadata.annotations.addons_repo_revision}}'
+ directory:
+ recurse: true
+ exclude: exclude/*
+ destination:
+ namespace: 'argocd'
+ name: '{{name}}'
+ syncPolicy:
+ automated: {}
diff --git a/patterns/gitops/multi-cluster-hub-spoke-argocd/hub/bootstrap/workloads.yaml b/patterns/gitops/multi-cluster-hub-spoke-argocd/hub/bootstrap/workloads.yaml
new file mode 100644
index 0000000000..c399039367
--- /dev/null
+++ b/patterns/gitops/multi-cluster-hub-spoke-argocd/hub/bootstrap/workloads.yaml
@@ -0,0 +1,34 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ApplicationSet
+metadata:
+ name: workloads
+ namespace: argocd
+spec:
+ syncPolicy:
+ preserveResourcesOnDeletion: false
+ generators:
+ - clusters:
+ selector:
+ matchExpressions:
+ - key: akuity.io/argo-cd-cluster-name
+ operator: NotIn
+ values: [in-cluster]
+ - key: environment
+ operator: NotIn
+ values: [control-plane]
+ template:
+ metadata:
+ name: 'workload-{{metadata.labels.environment}}'
+ spec:
+ project: default
+ source:
+ repoURL: '{{metadata.annotations.workload_repo_url}}'
+ path: '{{metadata.annotations.workload_repo_basepath}}{{metadata.annotations.workload_repo_path}}'
+ targetRevision: '{{metadata.annotations.workload_repo_revision}}'
+ destination:
+ namespace: 'workload'
+ name: '{{name}}'
+ syncPolicy:
+ automated: {}
+ syncOptions:
+ - CreateNamespace=true
diff --git a/patterns/gitops/multi-cluster-hub-spoke-argocd/hub/destroy.sh b/patterns/gitops/multi-cluster-hub-spoke-argocd/hub/destroy.sh
new file mode 100755
index 0000000000..8ad29aaebb
--- /dev/null
+++ b/patterns/gitops/multi-cluster-hub-spoke-argocd/hub/destroy.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+set -uo pipefail
+
+SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+ROOTDIR="$(cd ${SCRIPTDIR}/../..; pwd )"
+[[ -n "${DEBUG:-}" ]] && set -x
+
+# Delete the Ingress/SVC before removing the addons
+TMPFILE=$(mktemp)
+terraform -chdir=$SCRIPTDIR output -raw configure_kubectl > "$TMPFILE"
+# check if TMPFILE contains the string "No outputs found"
+if [[ ! $(cat $TMPFILE) == *"No outputs found"* ]]; then
+ source "$TMPFILE"
+ kubectl delete -n argocd applicationset workloads
+ kubectl delete -n argocd applicationset cluster-addons
+ kubectl delete -n argocd applicationset addons-argocd
+ kubectl delete -n argocd svc argo-cd-argocd-server
+fi
+
+terraform destroy -target="module.gitops_bridge_bootstrap" -auto-approve
+terraform destroy -target="module.eks_blueprints_addons" -auto-approve
+terraform destroy -target="module.eks" -auto-approve
+terraform destroy -target="module.vpc" -auto-approve
+terraform destroy -auto-approve
diff --git a/patterns/gitops/multi-cluster-hub-spoke-argocd/hub/main.tf b/patterns/gitops/multi-cluster-hub-spoke-argocd/hub/main.tf
new file mode 100644
index 0000000000..254499b4ec
--- /dev/null
+++ b/patterns/gitops/multi-cluster-hub-spoke-argocd/hub/main.tf
@@ -0,0 +1,294 @@
+provider "aws" {
+ region = local.region
+}
+data "aws_caller_identity" "current" {}
+data "aws_availability_zones" "available" {}
+
+provider "helm" {
+ kubernetes {
+ host = module.eks.cluster_endpoint
+ cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data)
+
+ exec {
+ api_version = "client.authentication.k8s.io/v1beta1"
+ command = "aws"
+ # This requires the awscli to be installed locally where Terraform is executed
+ args = ["eks", "get-token", "--cluster-name", module.eks.cluster_name, "--region", local.region]
+ }
+ }
+}
+
+provider "kubernetes" {
+ host = module.eks.cluster_endpoint
+ cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data)
+
+ exec {
+ api_version = "client.authentication.k8s.io/v1beta1"
+ command = "aws"
+ # This requires the awscli to be installed locally where Terraform is executed
+ args = ["eks", "get-token", "--cluster-name", module.eks.cluster_name, "--region", local.region]
+ }
+}
+
+locals {
+ name = "hub-${local.environment}"
+ environment = "control-plane"
+ region = var.region
+
+ cluster_version = var.kubernetes_version
+
+ vpc_cidr = var.vpc_cidr
+ azs = slice(data.aws_availability_zones.available.names, 0, 3)
+
+ gitops_addons_url = "${var.gitops_addons_org}/${var.gitops_addons_repo}"
+ gitops_addons_basepath = var.gitops_addons_basepath
+ gitops_addons_path = var.gitops_addons_path
+ gitops_addons_revision = var.gitops_addons_revision
+
+ argocd_namespace = "argocd"
+
+ aws_addons = {
+ enable_cert_manager = try(var.addons.enable_cert_manager, false)
+ enable_aws_efs_csi_driver = try(var.addons.enable_aws_efs_csi_driver, false)
+ enable_aws_fsx_csi_driver = try(var.addons.enable_aws_fsx_csi_driver, false)
+ enable_aws_cloudwatch_metrics = try(var.addons.enable_aws_cloudwatch_metrics, false)
+ enable_aws_privateca_issuer = try(var.addons.enable_aws_privateca_issuer, false)
+ enable_cluster_autoscaler = try(var.addons.enable_cluster_autoscaler, false)
+ enable_external_dns = try(var.addons.enable_external_dns, false)
+ enable_external_secrets = try(var.addons.enable_external_secrets, false)
+ enable_aws_load_balancer_controller = try(var.addons.enable_aws_load_balancer_controller, false)
+ enable_fargate_fluentbit = try(var.addons.enable_fargate_fluentbit, false)
+ enable_aws_for_fluentbit = try(var.addons.enable_aws_for_fluentbit, false)
+ enable_aws_node_termination_handler = try(var.addons.enable_aws_node_termination_handler, false)
+ enable_karpenter = try(var.addons.enable_karpenter, false)
+ enable_velero = try(var.addons.enable_velero, false)
+ enable_aws_gateway_api_controller = try(var.addons.enable_aws_gateway_api_controller, false)
+ enable_aws_ebs_csi_resources = try(var.addons.enable_aws_ebs_csi_resources, false)
+ enable_aws_secrets_store_csi_driver_provider = try(var.addons.enable_aws_secrets_store_csi_driver_provider, false)
+ enable_ack_apigatewayv2 = try(var.addons.enable_ack_apigatewayv2, false)
+ enable_ack_dynamodb = try(var.addons.enable_ack_dynamodb, false)
+ enable_ack_s3 = try(var.addons.enable_ack_s3, false)
+ enable_ack_rds = try(var.addons.enable_ack_rds, false)
+ enable_ack_prometheusservice = try(var.addons.enable_ack_prometheusservice, false)
+ enable_ack_emrcontainers = try(var.addons.enable_ack_emrcontainers, false)
+ enable_ack_sfn = try(var.addons.enable_ack_sfn, false)
+ enable_ack_eventbridge = try(var.addons.enable_ack_eventbridge, false)
+ enable_aws_argocd = try(var.addons.enable_aws_argocd, false)
+ }
+ oss_addons = {
+ enable_argocd = try(var.addons.enable_argocd, false)
+ enable_argo_rollouts = try(var.addons.enable_argo_rollouts, false)
+ enable_argo_events = try(var.addons.enable_argo_events, false)
+ enable_argo_workflows = try(var.addons.enable_argo_workflows, false)
+ enable_cluster_proportional_autoscaler = try(var.addons.enable_cluster_proportional_autoscaler, false)
+ enable_gatekeeper = try(var.addons.enable_gatekeeper, false)
+ enable_gpu_operator = try(var.addons.enable_gpu_operator, false)
+ enable_ingress_nginx = try(var.addons.enable_ingress_nginx, false)
+ enable_kyverno = try(var.addons.enable_kyverno, false)
+ enable_kube_prometheus_stack = try(var.addons.enable_kube_prometheus_stack, false)
+ enable_metrics_server = try(var.addons.enable_metrics_server, false)
+ enable_prometheus_adapter = try(var.addons.enable_prometheus_adapter, false)
+ enable_secrets_store_csi_driver = try(var.addons.enable_secrets_store_csi_driver, false)
+ enable_vpa = try(var.addons.enable_vpa, false)
+ }
+ addons = merge(
+ local.aws_addons,
+ local.oss_addons,
+ { kubernetes_version = local.cluster_version },
+ { aws_cluster_name = module.eks.cluster_name }
+ )
+
+ addons_metadata = merge(
+ module.eks_blueprints_addons.gitops_metadata,
+ {
+ aws_cluster_name = module.eks.cluster_name
+ aws_region = local.region
+ aws_account_id = data.aws_caller_identity.current.account_id
+ aws_vpc_id = module.vpc.vpc_id
+ },
+ {
+ argocd_iam_role_arn = module.argocd_irsa.iam_role_arn
+ argocd_namespace = local.argocd_namespace
+ },
+ {
+ addons_repo_url = local.gitops_addons_url
+ addons_repo_basepath = local.gitops_addons_basepath
+ addons_repo_path = local.gitops_addons_path
+ addons_repo_revision = local.gitops_addons_revision
+ }
+ )
+
+ argocd_apps = {
+ addons = file("${path.module}/bootstrap/addons.yaml")
+ workloads = file("${path.module}/bootstrap/workloads.yaml")
+ }
+
+ tags = {
+ Blueprint = local.name
+ GithubRepo = "github.com/gitops-bridge-dev/gitops-bridge"
+ }
+}
+
+################################################################################
+# GitOps Bridge: Bootstrap
+################################################################################
+module "gitops_bridge_bootstrap" {
+ source = "github.com/gitops-bridge-dev/gitops-bridge-argocd-bootstrap-terraform?ref=v2.0.0"
+
+ cluster = {
+ cluster_name = module.eks.cluster_name
+ environment = local.environment
+ metadata = local.addons_metadata
+ addons = local.addons
+ }
+ apps = local.argocd_apps
+ argocd = {
+ namespace = local.argocd_namespace
+ }
+}
+
+################################################################################
+# ArgoCD EKS Access
+################################################################################
+module "argocd_irsa" {
+ source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
+ version = "~> 5.20"
+
+ role_name_prefix = "argocd-hub-"
+ assume_role_condition_test = "StringLike"
+ role_policy_arns = {
+ ArgoCD_EKS_Policy = aws_iam_policy.irsa_policy.arn
+ }
+ oidc_providers = {
+ main = {
+ provider_arn = module.eks.oidc_provider_arn
+ namespace_service_accounts = ["${local.argocd_namespace}:argocd-*"]
+ }
+ }
+
+ tags = local.tags
+}
+
+
+resource "aws_iam_policy" "irsa_policy" {
+ name = "${module.eks.cluster_name}-argocd-irsa"
+ description = "IAM Policy for ArgoCD Hub"
+ policy = data.aws_iam_policy_document.irsa_policy.json
+ tags = local.tags
+}
+
+data "aws_iam_policy_document" "irsa_policy" {
+ statement {
+ effect = "Allow"
+ resources = ["*"]
+ actions = ["sts:AssumeRole"]
+ }
+}
+
+################################################################################
+# EKS Blueprints Addons
+################################################################################
+module "eks_blueprints_addons" {
+ source = "aws-ia/eks-blueprints-addons/aws"
+ version = "~> 1.0"
+
+ cluster_name = module.eks.cluster_name
+ cluster_endpoint = module.eks.cluster_endpoint
+ cluster_version = module.eks.cluster_version
+ oidc_provider_arn = module.eks.oidc_provider_arn
+
+ # Using GitOps Bridge
+ create_kubernetes_resources = false
+
+ # EKS Blueprints Addons
+ enable_cert_manager = local.aws_addons.enable_cert_manager
+ enable_aws_efs_csi_driver = local.aws_addons.enable_aws_efs_csi_driver
+ enable_aws_fsx_csi_driver = local.aws_addons.enable_aws_fsx_csi_driver
+ enable_aws_cloudwatch_metrics = local.aws_addons.enable_aws_cloudwatch_metrics
+ enable_aws_privateca_issuer = local.aws_addons.enable_aws_privateca_issuer
+ enable_cluster_autoscaler = local.aws_addons.enable_cluster_autoscaler
+ enable_external_dns = local.aws_addons.enable_external_dns
+ enable_external_secrets = local.aws_addons.enable_external_secrets
+ enable_aws_load_balancer_controller = local.aws_addons.enable_aws_load_balancer_controller
+ enable_fargate_fluentbit = local.aws_addons.enable_fargate_fluentbit
+ enable_aws_for_fluentbit = local.aws_addons.enable_aws_for_fluentbit
+ enable_aws_node_termination_handler = local.aws_addons.enable_aws_node_termination_handler
+ enable_karpenter = local.aws_addons.enable_karpenter
+ enable_velero = local.aws_addons.enable_velero
+ enable_aws_gateway_api_controller = local.aws_addons.enable_aws_gateway_api_controller
+
+ tags = local.tags
+}
+
+################################################################################
+# EKS Cluster
+################################################################################
+#tfsec:ignore:aws-eks-enable-control-plane-logging
+module "eks" {
+ source = "terraform-aws-modules/eks/aws"
+ version = "~> 19.13"
+
+ cluster_name = local.name
+ cluster_version = local.cluster_version
+ cluster_endpoint_public_access = true
+
+
+ vpc_id = module.vpc.vpc_id
+ subnet_ids = module.vpc.private_subnets
+
+ eks_managed_node_groups = {
+ initial = {
+ instance_types = ["t3.medium"]
+
+ min_size = 3
+ max_size = 10
+ desired_size = 3
+ }
+ }
+ # EKS Addons
+ cluster_addons = {
+ vpc-cni = {
+ # Specify the VPC CNI addon should be deployed before compute to ensure
+ # the addon is configured before data plane compute resources are created
+ # See README for further details
+ before_compute = true
+ most_recent = true # To ensure access to the latest settings provided
+ configuration_values = jsonencode({
+ env = {
+ # Reference docs https://docs.aws.amazon.com/eks/latest/userguide/cni-increase-ip-addresses.html
+ ENABLE_PREFIX_DELEGATION = "true"
+ WARM_PREFIX_TARGET = "1"
+ }
+ })
+ }
+ }
+ tags = local.tags
+}
+
+################################################################################
+# Supporting Resources
+################################################################################
+module "vpc" {
+ source = "terraform-aws-modules/vpc/aws"
+ version = "~> 5.0"
+
+ name = local.name
+ cidr = local.vpc_cidr
+
+ azs = local.azs
+ private_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 4, k)]
+ public_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 48)]
+
+ enable_nat_gateway = true
+ single_nat_gateway = true
+
+ public_subnet_tags = {
+ "kubernetes.io/role/elb" = 1
+ }
+
+ private_subnet_tags = {
+ "kubernetes.io/role/internal-elb" = 1
+ }
+
+ tags = local.tags
+}
diff --git a/patterns/gitops/multi-cluster-hub-spoke-argocd/hub/outputs.tf b/patterns/gitops/multi-cluster-hub-spoke-argocd/hub/outputs.tf
new file mode 100644
index 0000000000..406672e261
--- /dev/null
+++ b/patterns/gitops/multi-cluster-hub-spoke-argocd/hub/outputs.tf
@@ -0,0 +1,57 @@
+output "configure_kubectl" {
+ description = "Configure kubectl: make sure you're logged in with the correct AWS profile and run the following command to update your kubeconfig"
+ value = <<-EOT
+ export KUBECONFIG="/tmp/hup-spoke"
+ aws eks --region ${local.region} update-kubeconfig --name ${module.eks.cluster_name} --alias hub
+ EOT
+}
+
+
+output "configure_argocd" {
+ description = "Terminal Setup"
+ value = <<-EOT
+ export KUBECONFIG="/tmp/hup-spoke"
+ aws eks --region ${local.region} update-kubeconfig --name ${module.eks.cluster_name} --alias hub
+ export ARGOCD_OPTS="--port-forward --port-forward-namespace argocd --grpc-web"
+ kubectl --context hub config set-context --current --namespace argocd
+ argocd login --port-forward --username admin --password $(argocd admin initial-password | head -1)
+ echo "ArgoCD Username: admin"
+ echo "ArgoCD Password: $(kubectl get secrets argocd-initial-admin-secret -n argocd --template="{{index .data.password | base64decode}}")"
+ echo Port Forward: http://localhost:8080
+ kubectl port-forward -n argocd svc/argo-cd-argocd-server 8080:80
+ EOT
+}
+
+output "access_argocd" {
+ description = "ArgoCD Access"
+ value = <<-EOT
+ export KUBECONFIG="/tmp/hup-spoke"
+ aws eks --region ${local.region} update-kubeconfig --name ${module.eks.cluster_name} --alias hub
+ echo "ArgoCD Username: admin"
+ echo "ArgoCD Password: $(kubectl --context hub get secrets argocd-initial-admin-secret -n argocd --template="{{index .data.password | base64decode}}")"
+ echo "ArgoCD URL: https://$(kubectl --context hub get svc -n argocd argo-cd-argocd-server -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')"
+ EOT
+}
+
+
+output "argocd_iam_role_arn" {
+ description = "IAM Role for ArgoCD Cluster Hub, use to connect to spoke clusters"
+ value = module.argocd_irsa.iam_role_arn
+}
+
+output "cluster_name" {
+ description = "Cluster Hub name"
+ value = module.eks.cluster_name
+}
+output "cluster_endpoint" {
+ description = "Cluster Hub endpoint"
+ value = module.eks.cluster_endpoint
+}
+output "cluster_certificate_authority_data" {
+ description = "Cluster Hub certificate_authority_data"
+ value = module.eks.cluster_certificate_authority_data
+}
+output "cluster_region" {
+ description = "Cluster Hub region"
+ value = local.region
+}
diff --git a/patterns/gitops/multi-cluster-hub-spoke-argocd/hub/variables.tf b/patterns/gitops/multi-cluster-hub-spoke-argocd/hub/variables.tf
new file mode 100644
index 0000000000..2e2c8462ee
--- /dev/null
+++ b/patterns/gitops/multi-cluster-hub-spoke-argocd/hub/variables.tf
@@ -0,0 +1,53 @@
+variable "vpc_cidr" {
+ description = "VPC CIDR"
+ type = string
+ default = "10.0.0.0/16"
+}
+variable "region" {
+ description = "AWS region"
+ type = string
+ default = "us-west-2"
+}
+variable "kubernetes_version" {
+ description = "Kubernetes version"
+ type = string
+ default = "1.28"
+}
+variable "addons" {
+ description = "Kubernetes addons"
+ type = any
+ default = {
+ enable_aws_load_balancer_controller = true
+ enable_metrics_server = true
+ # Enable argocd with IRSA
+ enable_aws_argocd = true
+ # Disable argocd without IRSA
+ enable_argocd = false
+ }
+}
+# Addons Git
+variable "gitops_addons_org" {
+ description = "Git repository org/user contains for addons"
+ type = string
+ default = "https://github.com/gitops-bridge-dev"
+}
+variable "gitops_addons_repo" {
+ description = "Git repository contains for addons"
+ type = string
+ default = "gitops-bridge-argocd-control-plane-template"
+}
+variable "gitops_addons_revision" {
+ description = "Git repository revision/branch/ref for addons"
+ type = string
+ default = "main"
+}
+variable "gitops_addons_basepath" {
+ description = "Git repository base path for addons"
+ type = string
+ default = ""
+}
+variable "gitops_addons_path" {
+ description = "Git repository path for addons"
+ type = string
+ default = "bootstrap/control-plane/addons"
+}
diff --git a/patterns/gitops/multi-cluster-hub-spoke-argocd/hub/versions.tf b/patterns/gitops/multi-cluster-hub-spoke-argocd/hub/versions.tf
new file mode 100644
index 0000000000..2de60d58ee
--- /dev/null
+++ b/patterns/gitops/multi-cluster-hub-spoke-argocd/hub/versions.tf
@@ -0,0 +1,25 @@
+terraform {
+ required_version = ">= 1.0"
+
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = ">= 4.67.0"
+ }
+ helm = {
+ source = "hashicorp/helm"
+ version = ">= 2.10.1"
+ }
+ kubernetes = {
+ source = "hashicorp/kubernetes"
+ version = "2.22.0"
+ }
+ }
+
+ # ## Used for end-to-end testing on project; update to suit your needs
+ # backend "s3" {
+ # bucket = "terraform-ssp-github-actions-state"
+ # region = "us-west-2"
+ # key = "e2e/ipv4-prefix-delegation/terraform.tfstate"
+ # }
+}
diff --git a/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/.gitignore b/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/.gitignore
new file mode 100644
index 0000000000..e0cc55d252
--- /dev/null
+++ b/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/.gitignore
@@ -0,0 +1,4 @@
+# Include override files you do wish to add to version control using negated pattern
+#
+# !example_override.tf
+!workspaces/*.tfvars
diff --git a/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/deploy.sh b/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/deploy.sh
new file mode 100755
index 0000000000..766cffd8d8
--- /dev/null
+++ b/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/deploy.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+set -uo pipefail
+
+SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+ROOTDIR="$(cd ${SCRIPTDIR}/../..; pwd )"
+[[ -n "${DEBUG:-}" ]] && set -x
+
+
+if [[ $# -eq 0 ]] ; then
+ echo "No arguments supplied"
+ echo "Usage: destroy.sh "
+ echo "Example: destroy.sh dev"
+ exit 1
+fi
+env=$1
+echo "Deploying $env with "workspaces/${env}.tfvars" ..."
+
+
+if terraform workspace list | grep -q $env; then
+ echo "Workspace $env already exists."
+else
+ terraform workspace new $env
+fi
+
+terraform workspace select $env
+terraform init
+terraform apply -var-file="workspaces/${env}.tfvars"
diff --git a/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/destroy.sh b/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/destroy.sh
new file mode 100755
index 0000000000..cf44b4d9a2
--- /dev/null
+++ b/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/destroy.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+
+set -uo pipefail
+
+SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+ROOTDIR="$(cd ${SCRIPTDIR}/../..; pwd )"
+[[ -n "${DEBUG:-}" ]] && set -x
+
+
+if [[ $# -eq 0 ]] ; then
+ echo "No arguments supplied"
+ echo "Usage: destroy.sh "
+ echo "Example: destroy.sh dev"
+ exit 1
+fi
+env=$1
+echo "Destroying $env ..."
+
+terraform workspace select $env
+terraform destroy -auto-approve -var-file="workspaces/${env}.tfvars" -target="module.gitops_bridge_bootstrap" -auto-approve
+terraform destroy -auto-approve -var-file="workspaces/${env}.tfvars" -target="module.eks_blueprints_addons" -auto-approve
+terraform destroy -auto-approve -var-file="workspaces/${env}.tfvars" -target="module.eks" -auto-approve
+terraform destroy -auto-approve -var-file="workspaces/${env}.tfvars" -target="module.vpc" -auto-approve
+terraform destroy -auto-approve -var-file="workspaces/${env}.tfvars"
diff --git a/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/main.tf b/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/main.tf
new file mode 100644
index 0000000000..617769acb7
--- /dev/null
+++ b/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/main.tf
@@ -0,0 +1,323 @@
+provider "aws" {
+ region = local.region
+}
+data "aws_caller_identity" "current" {}
+data "aws_availability_zones" "available" {}
+
+
+data "terraform_remote_state" "cluster_hub" {
+ backend = "local"
+
+ config = {
+ path = "${path.module}/../hub/terraform.tfstate"
+ }
+}
+
+################################################################################
+# Kubernetes Access for Hub Cluster
+################################################################################
+
+provider "kubernetes" {
+ host = data.terraform_remote_state.cluster_hub.outputs.cluster_endpoint
+ cluster_ca_certificate = base64decode(data.terraform_remote_state.cluster_hub.outputs.cluster_certificate_authority_data)
+
+ exec {
+ api_version = "client.authentication.k8s.io/v1beta1"
+ command = "aws"
+ # This requires the awscli to be installed locally where Terraform is executed
+ args = ["eks", "get-token", "--cluster-name", data.terraform_remote_state.cluster_hub.outputs.cluster_name, "--region", data.terraform_remote_state.cluster_hub.outputs.cluster_region]
+ }
+ alias = "hub"
+}
+
+################################################################################
+# Kubernetes Access for Spoke Cluster
+################################################################################
+
+provider "kubernetes" {
+ host = module.eks.cluster_endpoint
+ cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data)
+
+ exec {
+ api_version = "client.authentication.k8s.io/v1beta1"
+ command = "aws"
+ # This requires the awscli to be installed locally where Terraform is executed
+ args = ["eks", "get-token", "--cluster-name", module.eks.cluster_name, "--region", local.region]
+ }
+}
+
+
+
+locals {
+ name = "spoke-${terraform.workspace}"
+ environment = terraform.workspace
+ region = var.region
+
+ cluster_version = var.kubernetes_version
+
+ vpc_cidr = var.vpc_cidr
+ azs = slice(data.aws_availability_zones.available.names, 0, 3)
+
+ gitops_addons_url = "${var.gitops_addons_org}/${var.gitops_addons_repo}"
+ gitops_addons_basepath = var.gitops_addons_basepath
+ gitops_addons_path = var.gitops_addons_path
+ gitops_addons_revision = var.gitops_addons_revision
+
+ gitops_workload_org = var.gitops_workload_org
+ gitops_workload_repo = var.gitops_workload_repo
+ gitops_workload_basepath = var.gitops_workload_basepath
+ gitops_workload_path = var.gitops_workload_path
+ gitops_workload_revision = var.gitops_workload_revision
+ gitops_workload_url = "${local.gitops_workload_org}/${local.gitops_workload_repo}"
+
+ aws_addons = {
+ enable_cert_manager = try(var.addons.enable_cert_manager, false)
+ enable_aws_efs_csi_driver = try(var.addons.enable_aws_efs_csi_driver, false)
+ enable_aws_fsx_csi_driver = try(var.addons.enable_aws_fsx_csi_driver, false)
+ enable_aws_cloudwatch_metrics = try(var.addons.enable_aws_cloudwatch_metrics, false)
+ enable_aws_privateca_issuer = try(var.addons.enable_aws_privateca_issuer, false)
+ enable_cluster_autoscaler = try(var.addons.enable_cluster_autoscaler, false)
+ enable_external_dns = try(var.addons.enable_external_dns, false)
+ enable_external_secrets = try(var.addons.enable_external_secrets, false)
+ enable_aws_load_balancer_controller = try(var.addons.enable_aws_load_balancer_controller, false)
+ enable_fargate_fluentbit = try(var.addons.enable_fargate_fluentbit, false)
+ enable_aws_for_fluentbit = try(var.addons.enable_aws_for_fluentbit, false)
+ enable_aws_node_termination_handler = try(var.addons.enable_aws_node_termination_handler, false)
+ enable_karpenter = try(var.addons.enable_karpenter, false)
+ enable_velero = try(var.addons.enable_velero, false)
+ enable_aws_gateway_api_controller = try(var.addons.enable_aws_gateway_api_controller, false)
+ enable_aws_ebs_csi_resources = try(var.addons.enable_aws_ebs_csi_resources, false)
+ enable_aws_secrets_store_csi_driver_provider = try(var.addons.enable_aws_secrets_store_csi_driver_provider, false)
+ enable_ack_apigatewayv2 = try(var.addons.enable_ack_apigatewayv2, false)
+ enable_ack_dynamodb = try(var.addons.enable_ack_dynamodb, false)
+ enable_ack_s3 = try(var.addons.enable_ack_s3, false)
+ enable_ack_rds = try(var.addons.enable_ack_rds, false)
+ enable_ack_prometheusservice = try(var.addons.enable_ack_prometheusservice, false)
+ enable_ack_emrcontainers = try(var.addons.enable_ack_emrcontainers, false)
+ enable_ack_sfn = try(var.addons.enable_ack_sfn, false)
+ enable_ack_eventbridge = try(var.addons.enable_ack_eventbridge, false)
+ enable_aws_argocd = try(var.addons.enable_aws_argocd, false)
+ }
+ oss_addons = {
+ enable_argocd = try(var.addons.enable_argocd, false)
+ enable_argo_rollouts = try(var.addons.enable_argo_rollouts, false)
+ enable_argo_events = try(var.addons.enable_argo_events, false)
+ enable_argo_workflows = try(var.addons.enable_argo_workflows, false)
+ enable_cluster_proportional_autoscaler = try(var.addons.enable_cluster_proportional_autoscaler, false)
+ enable_gatekeeper = try(var.addons.enable_gatekeeper, false)
+ enable_gpu_operator = try(var.addons.enable_gpu_operator, false)
+ enable_ingress_nginx = try(var.addons.enable_ingress_nginx, false)
+ enable_kyverno = try(var.addons.enable_kyverno, false)
+ enable_kube_prometheus_stack = try(var.addons.enable_kube_prometheus_stack, false)
+ enable_metrics_server = try(var.addons.enable_metrics_server, false)
+ enable_prometheus_adapter = try(var.addons.enable_prometheus_adapter, false)
+ enable_secrets_store_csi_driver = try(var.addons.enable_secrets_store_csi_driver, false)
+ enable_vpa = try(var.addons.enable_vpa, false)
+ }
+ addons = merge(
+ local.aws_addons,
+ local.oss_addons,
+ { kubernetes_version = local.cluster_version },
+ { aws_cluster_name = module.eks.cluster_name }
+ )
+
+ addons_metadata = merge(
+ module.eks_blueprints_addons.gitops_metadata,
+ {
+ aws_cluster_name = module.eks.cluster_name
+ aws_region = local.region
+ aws_account_id = data.aws_caller_identity.current.account_id
+ aws_vpc_id = module.vpc.vpc_id
+ },
+ {
+ addons_repo_url = local.gitops_addons_url
+ addons_repo_basepath = local.gitops_addons_basepath
+ addons_repo_path = local.gitops_addons_path
+ addons_repo_revision = local.gitops_addons_revision
+ },
+ {
+ workload_repo_url = local.gitops_workload_url
+ workload_repo_basepath = local.gitops_workload_basepath
+ workload_repo_path = local.gitops_workload_path
+ workload_repo_revision = local.gitops_workload_revision
+ }
+ )
+
+ tags = {
+ Blueprint = local.name
+ GithubRepo = "github.com/gitops-bridge-dev/gitops-bridge"
+ }
+}
+
+################################################################################
+# GitOps Bridge: Bootstrap for Hub Cluster
+################################################################################
+module "gitops_bridge_bootstrap_hub" {
+ source = "github.com/gitops-bridge-dev/gitops-bridge-argocd-bootstrap-terraform?ref=v2.0.0"
+
+ # The ArgoCD remote cluster secret is deploy on hub cluster not on spoke clusters
+ providers = {
+ kubernetes = kubernetes.hub
+ }
+
+ install = false # We are not installing argocd via helm on hub cluster
+ cluster = {
+ cluster_name = module.eks.cluster_name
+ environment = local.environment
+ metadata = local.addons_metadata
+ addons = local.addons
+ server = module.eks.cluster_endpoint
+ config = <<-EOT
+ {
+ "tlsClientConfig": {
+ "insecure": false,
+ "caData" : "${module.eks.cluster_certificate_authority_data}"
+ },
+ "awsAuthConfig" : {
+ "clusterName": "${module.eks.cluster_name}",
+ "roleARN": "${aws_iam_role.spoke.arn}"
+ }
+ }
+ EOT
+ }
+}
+
+################################################################################
+# ArgoCD EKS Access
+################################################################################
+resource "aws_iam_role" "spoke" {
+ name = "${module.eks.cluster_name}-argocd-spoke"
+ assume_role_policy = data.aws_iam_policy_document.assume_role_policy.json
+}
+
+data "aws_iam_policy_document" "assume_role_policy" {
+ statement {
+ actions = ["sts:AssumeRole"]
+ principals {
+ type = "AWS"
+ identifiers = [data.terraform_remote_state.cluster_hub.outputs.argocd_iam_role_arn]
+ }
+ }
+}
+
+
+
+################################################################################
+# EKS Blueprints Addons
+################################################################################
+module "eks_blueprints_addons" {
+ source = "aws-ia/eks-blueprints-addons/aws"
+ version = "~> 1.0"
+
+ cluster_name = module.eks.cluster_name
+ cluster_endpoint = module.eks.cluster_endpoint
+ cluster_version = module.eks.cluster_version
+ oidc_provider_arn = module.eks.oidc_provider_arn
+
+ # Using GitOps Bridge
+ create_kubernetes_resources = false
+
+ # EKS Blueprints Addons
+ enable_cert_manager = local.aws_addons.enable_cert_manager
+ enable_aws_efs_csi_driver = local.aws_addons.enable_aws_efs_csi_driver
+ enable_aws_fsx_csi_driver = local.aws_addons.enable_aws_fsx_csi_driver
+ enable_aws_cloudwatch_metrics = local.aws_addons.enable_aws_cloudwatch_metrics
+ enable_aws_privateca_issuer = local.aws_addons.enable_aws_privateca_issuer
+ enable_cluster_autoscaler = local.aws_addons.enable_cluster_autoscaler
+ enable_external_dns = local.aws_addons.enable_external_dns
+ enable_external_secrets = local.aws_addons.enable_external_secrets
+ enable_aws_load_balancer_controller = local.aws_addons.enable_aws_load_balancer_controller
+ enable_fargate_fluentbit = local.aws_addons.enable_fargate_fluentbit
+ enable_aws_for_fluentbit = local.aws_addons.enable_aws_for_fluentbit
+ enable_aws_node_termination_handler = local.aws_addons.enable_aws_node_termination_handler
+ enable_karpenter = local.aws_addons.enable_karpenter
+ enable_velero = local.aws_addons.enable_velero
+ enable_aws_gateway_api_controller = local.aws_addons.enable_aws_gateway_api_controller
+
+ tags = local.tags
+}
+
+################################################################################
+# EKS Cluster
+################################################################################
+#tfsec:ignore:aws-eks-enable-control-plane-logging
+module "eks" {
+ source = "terraform-aws-modules/eks/aws"
+ version = "~> 19.13"
+
+ cluster_name = local.name
+ cluster_version = local.cluster_version
+ cluster_endpoint_public_access = true
+
+
+ vpc_id = module.vpc.vpc_id
+ subnet_ids = module.vpc.private_subnets
+
+ manage_aws_auth_configmap = true
+ aws_auth_roles = [
+ # Granting access to ArgoCD from hub cluster
+ {
+ rolearn = aws_iam_role.spoke.arn
+ username = "gitops-role"
+ groups = [
+ "system:masters"
+ ]
+ },
+ ]
+
+ eks_managed_node_groups = {
+ initial = {
+ instance_types = ["t3.medium"]
+
+ min_size = 3
+ max_size = 10
+ desired_size = 3
+ }
+ }
+ # EKS Addons
+ cluster_addons = {
+ vpc-cni = {
+ # Specify the VPC CNI addon should be deployed before compute to ensure
+ # the addon is configured before data plane compute resources are created
+ # See README for further details
+ before_compute = true
+ most_recent = true # To ensure access to the latest settings provided
+ configuration_values = jsonencode({
+ env = {
+ # Reference docs https://docs.aws.amazon.com/eks/latest/userguide/cni-increase-ip-addresses.html
+ ENABLE_PREFIX_DELEGATION = "true"
+ WARM_PREFIX_TARGET = "1"
+ }
+ })
+ }
+ }
+ tags = local.tags
+}
+
+################################################################################
+# Supporting Resources
+################################################################################
+module "vpc" {
+ source = "terraform-aws-modules/vpc/aws"
+ version = "~> 5.0"
+
+ name = local.name
+ cidr = local.vpc_cidr
+
+ azs = local.azs
+ private_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 4, k)]
+ public_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 48)]
+
+ enable_nat_gateway = true
+ single_nat_gateway = true
+
+ public_subnet_tags = {
+ "kubernetes.io/role/elb" = 1
+ }
+
+ private_subnet_tags = {
+ "kubernetes.io/role/internal-elb" = 1
+ }
+
+ tags = local.tags
+}
diff --git a/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/outputs.tf b/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/outputs.tf
new file mode 100644
index 0000000000..18b5a74f23
--- /dev/null
+++ b/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/outputs.tf
@@ -0,0 +1,7 @@
+output "configure_kubectl" {
+ description = "Configure kubectl: make sure you're logged in with the correct AWS profile and run the following command to update your kubeconfig"
+ value = <<-EOT
+ export KUBECONFIG="/tmp/hup-spoke"
+ aws eks --region ${local.region} update-kubeconfig --name ${module.eks.cluster_name} --alias ${local.environment}
+ EOT
+}
diff --git a/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/variables.tf b/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/variables.tf
new file mode 100644
index 0000000000..570325119c
--- /dev/null
+++ b/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/variables.tf
@@ -0,0 +1,69 @@
+variable "region" {
+ description = "AWS region"
+ type = string
+}
+variable "vpc_cidr" {
+ description = "VPC CIDR"
+ type = string
+}
+variable "kubernetes_version" {
+ description = "EKS version"
+ type = string
+}
+variable "addons" {
+ description = "Kubernetes addons"
+ type = any
+}
+# Addons Git
+variable "gitops_addons_org" {
+ description = "Git repository org/user contains for addons"
+ type = string
+ default = "https://github.com/gitops-bridge-dev"
+}
+variable "gitops_addons_repo" {
+ description = "Git repository contains for addons"
+ type = string
+ default = "gitops-bridge-argocd-control-plane-template"
+}
+variable "gitops_addons_revision" {
+ description = "Git repository revision/branch/ref for addons"
+ type = string
+ default = "main"
+}
+variable "gitops_addons_basepath" {
+ description = "Git repository base path for addons"
+ type = string
+ default = ""
+}
+variable "gitops_addons_path" {
+ description = "Git repository path for addons"
+ type = string
+ default = "bootstrap/control-plane/addons"
+}
+
+# Workloads Git
+variable "gitops_workload_org" {
+ description = "Git repository org/user contains for workload"
+ type = string
+ default = "https://github.com/argoproj"
+}
+variable "gitops_workload_repo" {
+ description = "Git repository contains for workload"
+ type = string
+ default = "argocd-example-apps"
+}
+variable "gitops_workload_revision" {
+ description = "Git repository revision/branch/ref for workload"
+ type = string
+ default = "master"
+}
+variable "gitops_workload_basepath" {
+ description = "Git repository base path for workload"
+ type = string
+ default = ""
+}
+variable "gitops_workload_path" {
+ description = "Git repository path for workload"
+ type = string
+ default = "helm-guestbook"
+}
diff --git a/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/versions.tf b/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/versions.tf
new file mode 100644
index 0000000000..6ff7c991ec
--- /dev/null
+++ b/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/versions.tf
@@ -0,0 +1,21 @@
+terraform {
+ required_version = ">= 1.0"
+
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = ">= 4.67.0"
+ }
+ kubernetes = {
+ source = "hashicorp/kubernetes"
+ version = "2.22.0"
+ }
+ }
+
+ # ## Used for end-to-end testing on project; update to suit your needs
+ # backend "s3" {
+ # bucket = "terraform-ssp-github-actions-state"
+ # region = "us-west-2"
+ # key = "e2e/ipv4-prefix-delegation/terraform.tfstate"
+ # }
+}
diff --git a/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/workspaces/dev.tfvars b/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/workspaces/dev.tfvars
new file mode 100644
index 0000000000..706f9b8432
--- /dev/null
+++ b/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/workspaces/dev.tfvars
@@ -0,0 +1,10 @@
+vpc_cidr = "10.1.0.0/16"
+region = "us-west-2"
+kubernetes_version = "1.28"
+addons = {
+ enable_aws_load_balancer_controller = true
+ enable_metrics_server = true
+ # Disable argocd on spoke clusters
+ enable_aws_argocd = false
+ enable_argocd = false
+}
\ No newline at end of file
diff --git a/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/workspaces/prod.tfvars b/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/workspaces/prod.tfvars
new file mode 100644
index 0000000000..f1fdaa1ce9
--- /dev/null
+++ b/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/workspaces/prod.tfvars
@@ -0,0 +1,10 @@
+vpc_cidr = "10.3.0.0/16"
+region = "us-west-2"
+kubernetes_version = "1.28"
+addons = {
+ enable_aws_load_balancer_controller = true
+ enable_metrics_server = true
+ # Disable argocd on spoke clusters
+ enable_aws_argocd = false
+ enable_argocd = false
+}
\ No newline at end of file
diff --git a/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/workspaces/staging.tfvars b/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/workspaces/staging.tfvars
new file mode 100644
index 0000000000..5fc3593f28
--- /dev/null
+++ b/patterns/gitops/multi-cluster-hub-spoke-argocd/spokes/workspaces/staging.tfvars
@@ -0,0 +1,10 @@
+vpc_cidr = "10.2.0.0/16"
+region = "us-west-2"
+kubernetes_version = "1.28"
+addons = {
+ enable_aws_load_balancer_controller = true
+ enable_metrics_server = true
+ # Disable argocd on spoke clusters
+ enable_aws_argocd = false
+ enable_argocd = false
+}
\ No newline at end of file
diff --git a/patterns/gitops/multi-cluster-hub-spoke-argocd/static/gitops-bridge-multi-cluster-hup-spoke.drawio b/patterns/gitops/multi-cluster-hub-spoke-argocd/static/gitops-bridge-multi-cluster-hup-spoke.drawio
new file mode 100644
index 0000000000..083d637330
--- /dev/null
+++ b/patterns/gitops/multi-cluster-hub-spoke-argocd/static/gitops-bridge-multi-cluster-hup-spoke.drawio
@@ -0,0 +1 @@
+7Vvbcps6FP0aPyaDwPjyGGPnnE7bmc7JQ+NHxcigRkZUyDHu1x8JhEESvSWxIRMSZ4yWLoi1tLe2duyRF+zyfxhM4880RGTkOmE+8pYj13Wd8Vi8SeRYIgDM/BKJGA4VVgN3+AdSoKPQPQ5RpjXklBKOUx3c0CRBG65hkDF60JttKdHvmsIIWcDdBhIb/YpDHpfozHdq/F+Eo7i6M3BUzQ5WjRWQxTCkhwbkrUZewCjl5dUuDxCR7FW8lP1uf1J7mhhDCf+TDph9/b7+L/h8e3+TTO7Xa/QD7K68cpQnSPbqgdVk+bFigNF9EiI5iDPyFocYc3SXwo2sPQjRBRbzHRElIC7VcIhxlP90nuD09GLdILpDnB1FE9VhqvhSK2ZcrYRDTT+YKSxuUj9WIFSSR6eha1bEhSLmL0ga/56k7BHxTaw4SilOeDEJfyFeYlpB+eeLpoFErl2/BWzDpjYI7GbiDbTdwQTbsKkNAruZLFWz1sE2bOrbMzZ7g5bewOgtXt6C7jnBCQpOFi453tKEB5RQVvDvid9bqegiYjDESKu7nc5WzrhRt8RMDIRpIuoTyuSqWmwxIY0+S8cPwFTgGWf0ETVqtsWPqAlhFp/MQq54LLzGJ/iAyBeaYTX8A+Wc7hoNbgiOZAWn0nKgKm3ErBDTTUk+ofKHwK3KasXJW8IsLenY4lzOYyEcTCord3kknfE1PGTja4Yyumcb9GEj57MQxfJKb4Ues4sZr99iu/65TNe3TBeGIU2yzr0ccA2mpjZT8xamvHMxNbGYOlD2SCgM+0eW37KsLkrWtGVHmBCurFQja/J9T6uKq6yw5xvRwAVpXpBT1YurSL6vPt5VY4mplcOVNZYMglCuc617q4QmyHBtCrL8jumedjgM5W1axdXlfw19PcMYXFtf75L6VgM3yEahiAtVUe4ZNKIJJKsaNWip23yi0tcX+nxDnB+VU4d7TnX1BFvseC/7i21RFddquKKwzLXSsbEXqkHdYpwc88YworSu7iCu60Fk4aipKB/y1xoKTor95Bfkzct2HLII8d/FnvaaYIhAjp/0eby+wuAtK9yBWmDcqVx2yP2G5HojBgnmL5RYdf0izz+1d/fmunf3pobbLh9A9TIWymkaz18785bNuuOAxnN0SlzH3vAufMitQqrhlDuccodT7svN96LHXNCWxxsOJM8XeGII3HLgvOiBBPxBDrLrTawHmVpg53uGTWzYxIZN7Jnme9lNzE5B9iRZ6/kGVV0na4GdgOxPttZkq/NsLZgN0dE5o6PO07Wgh0d838xpdx8dVTIN0dEQHQ3R0cvN96LRUfUI/YuO/FnPoqPKrfUyOjLZ6jw6cofc0atGR/68Z9GRa+eO4v1DqZxwoYJlcpUSKLjspyblPatPX552EOvfoa96GgYtTqw1XDufbHYuK0sFEaVwIXp6z3IZnzPzJ52rZZ/IG2plHEY4id6zYqaB9UAyOy3QkCxlNHzPepkBcA/0sk/5NyyiwbLzmM7MeLnni4BFsf7KQPkJifqbF97qfw==
\ No newline at end of file
diff --git a/patterns/gitops/multi-cluster-hub-spoke-argocd/static/gitops-bridge-multi-cluster-hup-spoke.drawio.png b/patterns/gitops/multi-cluster-hub-spoke-argocd/static/gitops-bridge-multi-cluster-hup-spoke.drawio.png
new file mode 100644
index 0000000000..7ac6de28b8
Binary files /dev/null and b/patterns/gitops/multi-cluster-hub-spoke-argocd/static/gitops-bridge-multi-cluster-hup-spoke.drawio.png differ