-
Notifications
You must be signed in to change notification settings - Fork 3
terraform: add aws-eks-operator #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| # aws-eks-tailscale-operator | ||
|
|
||
| This example creates the following: | ||
|
|
||
| - a VPC and related resources including a NAT Gateway | ||
| - an EKS cluster with a managed node group | ||
| - a Kubernetes namespace for the [Tailscale operator](https://tailscale.com/kb/1236/kubernetes-operator) | ||
| - the Tailscale Kubernetes Operator deployed via [Helm](https://tailscale.com/kb/1236/kubernetes-operator#helm) | ||
|
|
||
| ## Considerations | ||
|
|
||
| - The EKS cluster is configured with both public and private API server access for flexibility | ||
| - The Tailscale operator is deployed in a dedicated `tailscale` namespace | ||
| - The operator will create a Tailscale device for API server proxy access | ||
| - Any additional Tailscale resources (like ingress controllers) created by the operator will appear in your Tailnet | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - Create a [Tailscale OAuth Client](https://tailscale.com/kb/1215/oauth-clients#setting-up-an-oauth-client) with appropriate scopes | ||
| - Ensure you have AWS CLI configured with appropriate permissions for EKS | ||
| - Install `kubectl` for cluster access after deployment | ||
|
|
||
| ## To use | ||
|
|
||
| Follow the documentation to configure the Terraform providers: | ||
|
|
||
| - [AWS](https://registry.terraform.io/providers/hashicorp/aws/latest/docs) | ||
| - [Kubernetes](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs) | ||
| - [Helm](https://registry.terraform.io/providers/hashicorp/helm/latest/docs) | ||
|
|
||
| ### Configure variables | ||
|
|
||
| Create a `terraform.tfvars` file with your Tailscale OAuth credentials: | ||
|
|
||
| ```hcl | ||
| tailscale_oauth_client_id = "your-oauth-client-id" | ||
| tailscale_oauth_client_secret = "your-oauth-client-secret" | ||
| ``` | ||
|
|
||
| ### Deploy | ||
|
|
||
| ```shell | ||
| terraform init | ||
| terraform apply | ||
| ``` | ||
|
|
||
| #### Verify deployment | ||
|
|
||
| After deployment, configure kubectl to access your cluster: | ||
|
|
||
| ```shell | ||
| aws eks update-kubeconfig --region $AWS_REGION --name $(terraform output -raw cluster_name) | ||
| ``` | ||
|
|
||
| Check that the Tailscale operator is running: | ||
|
|
||
| ```shell | ||
| kubectl get pods -n tailscale | ||
| kubectl logs -n tailscale -l app.kubernetes.io/name=tailscale-operator | ||
| ``` | ||
|
|
||
| #### Verify connectivity via the [API server proxy](https://tailscale.com/kb/1437/kubernetes-operator-api-server-proxy) | ||
|
|
||
| After deployment, configure kubectl to access your cluster using Tailscale: | ||
|
|
||
| ```shell | ||
| tailscale configure kubeconfig ${local.operator_name} | ||
| ``` | ||
|
|
||
| ```shell | ||
| kubectl get pods -n tailscale | ||
| ``` | ||
|
|
||
| ## To destroy | ||
|
|
||
| ```shell | ||
| terraform destroy | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,152 @@ | ||||||||||
| locals { | ||||||||||
| name = "example-${basename(path.cwd)}" | ||||||||||
|
|
||||||||||
| aws_tags = { | ||||||||||
| Name = local.name | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // Modify these to use your own VPC | ||||||||||
| vpc_id = module.vpc.vpc_id | ||||||||||
| subnet_ids = module.vpc.private_subnets | ||||||||||
|
|
||||||||||
| # EKS cluster configuration | ||||||||||
| cluster_version = "1.34" # TODO: omit this? | ||||||||||
| node_instance_type = "t3.medium" | ||||||||||
| desired_size = 2 | ||||||||||
| max_size = 2 | ||||||||||
| min_size = 1 | ||||||||||
|
|
||||||||||
| # Tailscale Operator configuration | ||||||||||
| operator_name = "${local.name}-operator" | ||||||||||
| operator_version = "1.92.4" | ||||||||||
| tailscale_oauth_client_id = var.tailscale_oauth_client_id | ||||||||||
| tailscale_oauth_client_secret = var.tailscale_oauth_client_secret | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // Remove this to use your own VPC. | ||||||||||
| module "vpc" { | ||||||||||
| source = "../internal-modules/aws-vpc" | ||||||||||
|
|
||||||||||
| name = local.name | ||||||||||
| tags = local.aws_tags | ||||||||||
| } | ||||||||||
|
|
||||||||||
| module "eks" { | ||||||||||
| source = "terraform-aws-modules/eks/aws" | ||||||||||
| version = ">= 21.0, < 22.0" | ||||||||||
|
|
||||||||||
| name = local.name | ||||||||||
| kubernetes_version = local.cluster_version | ||||||||||
|
|
||||||||||
| addons = { | ||||||||||
| coredns = {} | ||||||||||
| eks-pod-identity-agent = { | ||||||||||
| before_compute = true | ||||||||||
| } | ||||||||||
| kube-proxy = {} | ||||||||||
| vpc-cni = { | ||||||||||
| before_compute = true | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| # Once the Tailscale operator is installed, `endpoint_public_access` can be disabled. | ||||||||||
| # This is left enabled for the sake of easy adoption. | ||||||||||
| endpoint_public_access = true | ||||||||||
|
|
||||||||||
| # Optional: Adds the current caller identity as an administrator via cluster access entry | ||||||||||
| enable_cluster_creator_admin_permissions = true | ||||||||||
|
|
||||||||||
| vpc_id = local.vpc_id | ||||||||||
| subnet_ids = local.subnet_ids | ||||||||||
|
|
||||||||||
| eks_managed_node_groups = { | ||||||||||
| main = { | ||||||||||
| # Starting on 1.30, AL2023 is the default AMI type for EKS managed node groups | ||||||||||
| # ami_type = "AL2023_x86_64_STANDARD" | ||||||||||
| instance_types = [local.node_instance_type] | ||||||||||
|
|
||||||||||
| desired_size = local.desired_size | ||||||||||
| max_size = local.max_size | ||||||||||
| min_size = local.min_size | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| tags = local.aws_tags | ||||||||||
| } | ||||||||||
|
|
||||||||||
| # Kubernetes namespace for Tailscale operator | ||||||||||
| resource "kubernetes_namespace_v1" "tailscale_operator" { | ||||||||||
| metadata { | ||||||||||
| name = "tailscale" | ||||||||||
| labels = { | ||||||||||
| "pod-security.kubernetes.io/enforce" = "privileged" | ||||||||||
| } | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| resource "helm_release" "tailscale_operator" { | ||||||||||
| name = local.operator_name | ||||||||||
| namespace = kubernetes_namespace_v1.tailscale_operator.metadata[0].name | ||||||||||
|
|
||||||||||
| repository = "https://pkgs.tailscale.com/helmcharts" | ||||||||||
| chart = "tailscale-operator" | ||||||||||
| version = local.operator_version | ||||||||||
|
|
||||||||||
| values = [ | ||||||||||
| yamlencode({ | ||||||||||
| operatorConfig = { | ||||||||||
| image = { | ||||||||||
| repo = "tailscale/k8s-operator" | ||||||||||
| tag = "v${local.operator_version}" | ||||||||||
| } | ||||||||||
| hostname = local.operator_name | ||||||||||
| } | ||||||||||
| apiServerProxyConfig = { | ||||||||||
| mode = "true" | ||||||||||
|
||||||||||
| mode = "true" | |
| mode = true |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The sensitive values are already being set through the set_sensitive block below (lines 115-124). Including them in the yamlencode values block on lines 109-110 will expose these sensitive values in the Terraform state and logs. Remove the oauth block from the yamlencode section since set_sensitive will handle these values securely.
| oauth = { | |
| clientId = local.tailscale_oauth_client_id | |
| clientSecret = local.tailscale_oauth_client_secret | |
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,24 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| output "vpc_id" { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| description = "VPC ID where the EKS cluster is deployed" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| value = module.vpc.vpc_id | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| output "cluster_name" { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| description = "EKS cluster name" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| value = module.eks.cluster_name | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| output "tailscale_operator_namespace" { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| description = "Kubernetes namespace where Tailscale operator is deployed" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| value = kubernetes_namespace_v1.tailscale_operator.metadata[0].name | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| output "cmd_kubeconfig_tailscale" { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| value = "tailscale configure kubeconfig ${local.operator_name}" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| output "cmd_kubeconfig_aws" { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| value = "aws eks update-kubeconfig --region ${data.aws_region.current.region} --name ${module.eks.cluster_name}" | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+17
to
+21
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| value = "tailscale configure kubeconfig ${local.operator_name}" | |
| } | |
| output "cmd_kubeconfig_aws" { | |
| value = "aws eks update-kubeconfig --region ${data.aws_region.current.region} --name ${module.eks.cluster_name}" | |
| description = "Command to configure kubeconfig for the EKS cluster using the Tailscale operator" | |
| value = "tailscale configure kubeconfig ${local.operator_name}" | |
| } | |
| output "cmd_kubeconfig_aws" { | |
| description = "AWS CLI command to configure kubeconfig for the EKS cluster" | |
| value = "aws eks update-kubeconfig --region ${data.aws_region.current.region} --name ${module.eks.cluster_name}" |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing description for output. All outputs should include a description field to document their purpose and usage, especially for user-facing command outputs.
| value = "tailscale configure kubeconfig ${local.operator_name}" | |
| } | |
| output "cmd_kubeconfig_aws" { | |
| value = "aws eks update-kubeconfig --region ${data.aws_region.current.region} --name ${module.eks.cluster_name}" | |
| description = "Command to configure kubectl using the Tailscale operator" | |
| value = "tailscale configure kubeconfig ${local.operator_name}" | |
| } | |
| output "cmd_kubeconfig_aws" { | |
| description = "AWS CLI command to configure kubectl for this EKS cluster" | |
| value = "aws eks update-kubeconfig --region ${data.aws_region.current.region} --name ${module.eks.cluster_name}" |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The data source is defined in the outputs file but would be more appropriately placed in a data.tf file or at the beginning of main.tf. Placing data sources at the end of an outputs file is unconventional and can make the code harder to maintain.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| variable "tailscale_oauth_client_id" { | ||
| description = "Tailscale OAuth client ID" | ||
| type = string | ||
| sensitive = true | ||
|
|
||
| validation { | ||
| condition = length(var.tailscale_oauth_client_id) > 0 | ||
| error_message = "Tailscale OAuth client ID must not be empty." | ||
| } | ||
| } | ||
|
|
||
| variable "tailscale_oauth_client_secret" { | ||
| description = "Tailscale OAuth client secret" | ||
| type = string | ||
| sensitive = true | ||
|
|
||
| validation { | ||
| condition = length(var.tailscale_oauth_client_secret) > 0 | ||
| error_message = "Tailscale OAuth client secret must not be empty." | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| terraform { | ||
| required_version = ">= 1.0" | ||
|
|
||
| required_providers { | ||
| aws = { | ||
| source = "hashicorp/aws" | ||
| version = ">= 6.0, < 7.0" | ||
| } | ||
| kubernetes = { | ||
| source = "hashicorp/kubernetes" | ||
| version = ">= 3.0.1, < 4.0" | ||
| } | ||
| helm = { | ||
| source = "hashicorp/helm" | ||
| version = ">= 3.1.1, < 4.0" | ||
| } | ||
| } | ||
| } | ||
|
|
||
| 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" | ||
| args = ["eks", "get-token", "--cluster-name", module.eks.cluster_name] | ||
| } | ||
| } | ||
|
|
||
| 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" | ||
| args = ["eks", "get-token", "--cluster-name", module.eks.cluster_name] | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The variable reference ${local.operator_name} in the README will not be interpolated when users read the documentation. This should be replaced with a concrete example value or instruction like "example-aws-eks-operator-operator" or use the actual terraform output command reference.