Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
59b3f84
terraform: add aws-eks
clstokes Dec 19, 2025
9bb7067
rename aws-eks to aws-eks-operator
clstokes Dec 19, 2025
dc27e4b
Merge branch 'main' into clstokes/eks-operator
clstokes Dec 20, 2025
1b49a5b
cleanup and tflint fixes
clstokes Dec 20, 2025
386d6c6
terraform fmt
clstokes Dec 20, 2025
849beaa
address most copilot feedback
clstokes Dec 21, 2025
b4741ac
rename terraform-tflint to terraform-check-tflint
clstokes Dec 21, 2025
42dded2
tflint
clstokes Dec 21, 2025
110e277
address more copilot feedback
clstokes Dec 21, 2025
657078d
move ha proxy to external kubectl manifest, and various cleanup
clstokes Dec 21, 2025
431d846
add 'terraform-fmt' to Makefile
clstokes Dec 21, 2025
9c9b5c9
add operator_name output
clstokes Dec 21, 2025
e51384a
provision ha proxy with null_resource local-exec provisioner
clstokes Dec 21, 2025
6ac0452
cleanup
clstokes Dec 22, 2025
0fadf5b
Update Makefile
clstokes Dec 22, 2025
ea8dc02
use a random suffix for ha proxy name
clstokes Dec 23, 2025
d0b1672
fix tflint issue
clstokes Dec 23, 2025
d0fdb41
use aws_eks_cluster_versions data source for latest version
clstokes Dec 23, 2025
e3a880c
update prerequisites
clstokes Dec 23, 2025
835bf8f
add enable_ha_proxy_service local variable
clstokes Dec 23, 2025
2982944
remove beta from api_version for aws auth call
clstokes Dec 23, 2025
3ac53c2
use a data source instead of aws cli
clstokes Dec 23, 2025
473ecfc
Update terraform/aws/aws-eks-operator/README.md
clstokes Dec 23, 2025
c0dbf1c
Update README.md
clstokes Dec 23, 2025
2c00fca
Merge branch 'clstokes/eks-operator' of github.com:tailscale-dev/exam…
clstokes Dec 23, 2025
dcc6e08
Apply suggestions from code review
clstokes Jan 5, 2026
31e21c7
remove unused helm chart config
clstokes Jan 5, 2026
d456cb3
terraform fmt
clstokes Jan 5, 2026
6022c67
set more cluster properties, remove unused outputs
clstokes Jan 6, 2026
9bd1ea3
Change kubectl logs example
clstokes Jan 6, 2026
7b0b9e6
Merge branch 'main' into clstokes/eks-operator
clstokes Jan 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/terraform-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:

jobs:

terraform-tflint:
terraform-check-tflint:
runs-on: ubuntu-latest
steps:
- name: Check out code
Expand Down
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
default: help

.PHONY: terraform-tflint
terraform-tflint: ## Run 'terraform-tflint' github actions with https://github.com/nektos/act
act -j terraform-tflint
.PHONY: terraform-check-tflint
terraform-check-tflint: ## Run 'terraform-check-tflint' github actions with https://github.com/nektos/act
act -j terraform-check-tflint

.PHONY: check-terraform-examples
Comment thread
clstokes marked this conversation as resolved.
Outdated
terraform-check-examples: ## Run specific 'check' github actions with https://github.com/nektos/act
Expand Down
82 changes: 82 additions & 0 deletions terraform/aws/aws-eks-operator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# aws-eks-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
Comment thread
clstokes marked this conversation as resolved.
Outdated

## 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

# execute the output from `terraform output cmd_kubectl_ha_proxy_apply` to deploy the HA proxy
```

#### 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 ${terraform output -raw operator_name}
Comment thread
clstokes marked this conversation as resolved.
Outdated
Comment thread
rajsinghtech marked this conversation as resolved.
Outdated
```

```shell
kubectl get pods -n tailscale
```

## To destroy

```shell
# execute the output from `terraform output cmd_kubectl_ha_proxy_delete` to delete the HA proxy

terraform destroy
```
1 change: 1 addition & 0 deletions terraform/aws/aws-eks-operator/data.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
data "aws_region" "current" {}
122 changes: 122 additions & 0 deletions terraform/aws/aws-eks-operator/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
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?
Comment thread
clstokes marked this conversation as resolved.
Outdated
node_instance_type = "t3.medium"
desired_size = 2
max_size = 2
min_size = 1

# Tailscale Operator configuration
namespace_name = "tailscale"
operator_name = local.name
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 = {
Comment thread
clstokes marked this conversation as resolved.
# 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 = local.namespace_name
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"
Comment thread
clstokes marked this conversation as resolved.
Outdated
tag = "v${local.operator_version}"
}
hostname = local.operator_name
}
apiServerProxyConfig = {
mode = true
Comment thread
clstokes marked this conversation as resolved.
Outdated
tags = "tag:k8s-operator,tag:k8s-api-server"
Comment thread
clstokes marked this conversation as resolved.
Outdated
}
})
]

set_sensitive = [
{
name = "oauth.clientId"
value = local.tailscale_oauth_client_id
},
{
name = "oauth.clientSecret"
value = local.tailscale_oauth_client_secret
},
]
}
34 changes: 34 additions & 0 deletions terraform/aws/aws-eks-operator/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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" {
description = "Command to configure kubeconfig for Tailscale access to the EKS cluster"
value = "tailscale configure kubeconfig ${helm_release.tailscale_operator.name}"
}

output "cmd_kubeconfig_aws" {
description = "Command to configure kubeconfig for public access to the EKS cluster"
value = "aws eks update-kubeconfig --region ${data.aws_region.current.region} --name ${module.eks.cluster_name}"
}

output "cmd_kubectl_ha_proxy_apply" {
description = "Command to deploy the Tailscale high availability API server proxy - https://tailscale.com/kb/1437/kubernetes-operator-api-server-proxy#configuring-a-high-availability-api-server-proxy"
value = "OPERATOR_NAME=${helm_release.tailscale_operator.name} envsubst < tailscale-api-server-ha-proxy.yaml | kubectl apply -f -"
}

output "cmd_kubectl_ha_proxy_delete" {
description = "Command to delete the Tailscale high availability API server proxy - https://tailscale.com/kb/1437/kubernetes-operator-api-server-proxy#configuring-a-high-availability-api-server-proxy"
Comment thread
clstokes marked this conversation as resolved.
Outdated
value = "OPERATOR_NAME=${helm_release.tailscale_operator.name} envsubst < tailscale-api-server-ha-proxy.yaml | kubectl delete -f -"
Comment thread
clstokes marked this conversation as resolved.
Outdated
Comment thread
clstokes marked this conversation as resolved.
Outdated
}
10 changes: 10 additions & 0 deletions terraform/aws/aws-eks-operator/tailscale-api-server-ha-proxy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: tailscale.com/v1alpha1
kind: ProxyGroup
metadata:
name: ${OPERATOR_NAME}-ha
spec:
type: kube-apiserver
replicas: 2
tags: ["tag:k8s"]
kubeAPIServer:
mode: auth
21 changes: 21 additions & 0 deletions terraform/aws/aws-eks-operator/variables.tf
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."
Comment thread
clstokes marked this conversation as resolved.
}
}

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."
Comment thread
clstokes marked this conversation as resolved.
}
}
42 changes: 42 additions & 0 deletions terraform/aws/aws-eks-operator/versions.tf
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"
Comment thread
clstokes marked this conversation as resolved.
Outdated
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API version "client.authentication.k8s.io/v1beta1" is deprecated. For EKS 1.24 and later, use "client.authentication.k8s.io/v1" instead to avoid future compatibility issues.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API version "client.authentication.k8s.io/v1beta1" for EKS token authentication has been deprecated since Kubernetes 1.24 and removed in 1.27. Since AWS EKS now supports versions 1.28+, you should use "client.authentication.k8s.io/v1" instead.

Copilot uses AI. Check for mistakes.
Comment thread
clstokes marked this conversation as resolved.
Outdated
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"
Comment thread
clstokes marked this conversation as resolved.
Outdated
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API version "client.authentication.k8s.io/v1beta1" is deprecated. For EKS 1.24 and later, use "client.authentication.k8s.io/v1" instead to avoid future compatibility issues.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API version "client.authentication.k8s.io/v1beta1" for EKS token authentication has been deprecated since Kubernetes 1.24 and removed in 1.27. Since AWS EKS now supports versions 1.28+, you should use "client.authentication.k8s.io/v1" instead.

Copilot uses AI. Check for mistakes.
Comment thread
clstokes marked this conversation as resolved.
Outdated
command = "aws"
args = ["eks", "get-token", "--cluster-name", module.eks.cluster_name]
}
}
}
Loading