From 3e47d0f5b2abf4fc37fdfbf39657d7077ef68581 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Tue, 1 Oct 2024 19:49:42 +0200 Subject: [PATCH] upstream `tailscale` (#835) Co-authored-by: Jeremy White Co-authored-by: Igor Rodionov --- modules/eks/tailscale/README.md | 124 +++++++++ modules/eks/tailscale/context.tf | 279 +++++++++++++++++++ modules/eks/tailscale/main.tf | 265 ++++++++++++++++++ modules/eks/tailscale/outputs.tf | 4 + modules/eks/tailscale/provider-kubernetes.tf | 132 +++++++++ modules/eks/tailscale/providers.tf | 29 ++ modules/eks/tailscale/remote-state.tf | 20 ++ modules/eks/tailscale/variables.tf | 63 +++++ modules/eks/tailscale/versions.tf | 14 + 9 files changed, 930 insertions(+) create mode 100644 modules/eks/tailscale/README.md create mode 100644 modules/eks/tailscale/context.tf create mode 100644 modules/eks/tailscale/main.tf create mode 100644 modules/eks/tailscale/outputs.tf create mode 100644 modules/eks/tailscale/provider-kubernetes.tf create mode 100644 modules/eks/tailscale/providers.tf create mode 100644 modules/eks/tailscale/remote-state.tf create mode 100644 modules/eks/tailscale/variables.tf create mode 100644 modules/eks/tailscale/versions.tf diff --git a/modules/eks/tailscale/README.md b/modules/eks/tailscale/README.md new file mode 100644 index 000000000..74866021b --- /dev/null +++ b/modules/eks/tailscale/README.md @@ -0,0 +1,124 @@ +# Component: eks/tailscale + +## Usage + +**Stack Level**: Regional + +Use this in the catalog or use these variables to overwrite the catalog values. + +```yaml +components: + terraform: + eks/tailscale: + vars: + enabled: true + name: tailscale + create_namespace: true + kubernetes_namespace: "tailscale" + image_repo: tailscale/k8s-operator + image_tag: unstable +``` + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.7.1 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.7.1 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [store\_read](#module\_store\_read) | cloudposse/ssm-parameter-store/aws | 0.10.0 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [kubernetes_cluster_role.tailscale_operator](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/cluster_role) | resource | +| [kubernetes_cluster_role_binding.tailscale_operator](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/cluster_role_binding) | resource | +| [kubernetes_deployment.operator](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/deployment) | resource | +| [kubernetes_namespace.default](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace) | resource | +| [kubernetes_role.operator](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/role) | resource | +| [kubernetes_role.proxies](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/role) | resource | +| [kubernetes_role_binding.operator](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/role_binding) | resource | +| [kubernetes_role_binding.proxies](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/role_binding) | resource | +| [kubernetes_secret.operator_oauth](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret) | resource | +| [kubernetes_service_account.operator](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/service_account) | resource | +| [kubernetes_service_account.proxies](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/service_account) | resource | +| [aws_eks_cluster.kubernetes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster) | data source | +| [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | +| [aws_subnet.vpc_subnets](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/subnet) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [chart\_values](#input\_chart\_values) | Addition map values to yamlencode as `helm_release` values. | `any` | `{}` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [create\_namespace](#input\_create\_namespace) | Create the namespace if it does not yet exist. Defaults to `false`. | `bool` | `false` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [deployment\_name](#input\_deployment\_name) | Name of the tailscale deployment, defaults to `tailscale` if this is null | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [env](#input\_env) | Map of ENV vars in the format `key=value`. These ENV vars will be set in the `utils` provider before executing the data source | `map(string)` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [image\_repo](#input\_image\_repo) | Image repository for the deployment | `string` | `"ghcr.io/tailscale/tailscale"` | no | +| [image\_tag](#input\_image\_tag) | Image Tag for the deployment. | `string` | `"latest"` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | +| [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | +| [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | +| [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | +| [kube\_secret](#input\_kube\_secret) | Kube Secret Name for tailscale | `string` | `"tailscale"` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | +| [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | +| [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | +| [kubernetes\_namespace](#input\_kubernetes\_namespace) | The namespace to install the release into. | `string` | n/a | yes | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [routes](#input\_routes) | List of CIDR Ranges or IPs to allow Tailscale to connect to | `list(string)` | `[]` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [deployment](#output\_deployment) | Tail scale operator deployment K8S resource | + + + +## References + +- https://github.com/Ealenn/tailscale diff --git a/modules/eks/tailscale/context.tf b/modules/eks/tailscale/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/eks/tailscale/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/eks/tailscale/main.tf b/modules/eks/tailscale/main.tf new file mode 100644 index 000000000..f14108d47 --- /dev/null +++ b/modules/eks/tailscale/main.tf @@ -0,0 +1,265 @@ +locals { + enabled = module.this.enabled + create_namespace = local.enabled + + routes = join(",", concat(var.routes, [for k, v in data.aws_subnet.vpc_subnets : v.cidr_block])) +} + +module "store_read" { + source = "cloudposse/ssm-parameter-store/aws" + version = "0.10.0" + + parameter_read = [ + "/tailscale/client_id", + "/tailscale/client_secret", + ] +} + +resource "kubernetes_secret" "operator_oauth" { + metadata { + name = "operator-oauth" + namespace = var.kubernetes_namespace + } + data = { + client_id = module.store_read.map["/tailscale/client_id"] + client_secret = module.store_read.map["/tailscale/client_secret"] + } +} + +resource "kubernetes_namespace" "default" { + count = local.create_namespace ? 1 : 0 + + metadata { + name = var.kubernetes_namespace + + labels = module.this.tags + } +} + + +resource "kubernetes_service_account" "proxies" { + metadata { + name = "proxies" + namespace = var.kubernetes_namespace + } +} + +resource "kubernetes_role" "proxies" { + metadata { + name = "proxies" + namespace = var.kubernetes_namespace + } + + rule { + verbs = ["*"] + api_groups = [""] + resources = ["secrets"] + } +} + +resource "kubernetes_role_binding" "proxies" { + metadata { + name = "proxies" + namespace = var.kubernetes_namespace + } + + subject { + kind = "ServiceAccount" + name = "proxies" + namespace = var.kubernetes_namespace + } + + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "Role" + name = "proxies" + } +} + +resource "kubernetes_service_account" "operator" { + metadata { + name = "operator" + namespace = var.kubernetes_namespace + } +} + +resource "kubernetes_cluster_role" "tailscale_operator" { + metadata { + name = "tailscale-operator" + } + + rule { + verbs = ["*"] + api_groups = [""] + resources = ["services", "services/status"] + } +} + +resource "kubernetes_cluster_role_binding" "tailscale_operator" { + metadata { + name = "tailscale-operator" + } + + subject { + kind = "ServiceAccount" + name = "operator" + namespace = var.kubernetes_namespace + } + + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "ClusterRole" + name = "tailscale-operator" + } +} + +resource "kubernetes_role" "operator" { + metadata { + name = "operator" + namespace = var.kubernetes_namespace + } + + rule { + verbs = ["*"] + api_groups = [""] + resources = ["secrets"] + } + + rule { + verbs = ["*"] + api_groups = ["apps"] + resources = ["statefulsets"] + } +} + +resource "kubernetes_role_binding" "operator" { + metadata { + name = "operator" + namespace = var.kubernetes_namespace + } + + subject { + kind = "ServiceAccount" + name = "operator" + namespace = var.kubernetes_namespace + } + + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "Role" + name = "operator" + } +} + +resource "kubernetes_deployment" "operator" { + metadata { + name = coalesce(var.deployment_name, "tailscale-operator") + namespace = var.kubernetes_namespace + labels = { + app = "tailscale" + } + } + + spec { + replicas = 1 + + selector { + match_labels = { + app = "operator" + } + } + + template { + metadata { + labels = { + app = "operator" + } + } + + spec { + volume { + name = "oauth" + + secret { + secret_name = "operator-oauth" + } + } + + container { + image = format("%s:%s", var.image_repo, var.image_tag) + name = "tailscale" + + env { + name = "OPERATOR_HOSTNAME" + value = format("%s-%s-%s-%s", "tailscale-operator", var.tenant, var.environment, var.stage) + } + + env { + name = "OPERATOR_SECRET" + value = "operator" + } + + env { + name = "OPERATOR_LOGGING" + value = "info" + } + + env { + name = "OPERATOR_NAMESPACE" + + value_from { + field_ref { + field_path = "metadata.namespace" + } + } + } + + env { + name = "CLIENT_ID_FILE" + value = "/oauth/client_id" + } + + env { + name = "CLIENT_SECRET_FILE" + value = "/oauth/client_secret" + } + + env { + name = "PROXY_IMAGE" + value = "tailscale/tailscale:unstable" + } + + env { + name = "PROXY_TAGS" + value = "tag:k8s" + } + + env { + name = "AUTH_PROXY" + value = "false" + } + + resources { + requests = { + cpu = "500m" + + memory = "100Mi" + } + } + + volume_mount { + name = "oauth" + read_only = true + mount_path = "/oauth" + } + } + + service_account_name = "operator" + } + } + + strategy { + type = "Recreate" + } + } +} diff --git a/modules/eks/tailscale/outputs.tf b/modules/eks/tailscale/outputs.tf new file mode 100644 index 000000000..811fe1ff4 --- /dev/null +++ b/modules/eks/tailscale/outputs.tf @@ -0,0 +1,4 @@ +output "deployment" { + value = kubernetes_deployment.operator + description = "Tail scale operator deployment K8S resource" +} diff --git a/modules/eks/tailscale/provider-kubernetes.tf b/modules/eks/tailscale/provider-kubernetes.tf new file mode 100644 index 000000000..00cfd1542 --- /dev/null +++ b/modules/eks/tailscale/provider-kubernetes.tf @@ -0,0 +1,132 @@ +################## +# +# This file is a drop-in to provide a helm provider. +# +# All the following variables are just about configuring the Kubernetes provider +# to be able to modify EKS cluster. The reason there are so many options is +# because at various times, each one of them has had problems, so we give you a choice. +# +# The reason there are so many "enabled" inputs rather than automatically +# detecting whether or not they are enabled based on the value of the input +# is that any logic based on input values requires the values to be known during +# the "plan" phase of Terraform, and often they are not, which causes problems. +# +variable "kubeconfig_file_enabled" { + type = bool + default = false + description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" +} + +variable "kubeconfig_file" { + type = string + default = "" + description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" +} + +variable "kubeconfig_context" { + type = string + default = "" + description = "Context to choose from the Kubernetes kube config file" +} + +variable "kube_data_auth_enabled" { + type = bool + default = false + description = <<-EOT + If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. + EOT +} + +variable "kube_exec_auth_enabled" { + type = bool + default = true + description = <<-EOT + If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. + EOT +} + +variable "kube_exec_auth_role_arn" { + type = string + default = "" + description = "The role ARN for `aws eks get-token` to use" +} + +variable "kube_exec_auth_role_arn_enabled" { + type = bool + default = true + description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" +} + +variable "kube_exec_auth_aws_profile" { + type = string + default = "" + description = "The AWS config profile for `aws eks get-token` to use" +} + +variable "kube_exec_auth_aws_profile_enabled" { + type = bool + default = false + description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" +} + +variable "kubeconfig_exec_auth_api_version" { + type = string + default = "client.authentication.k8s.io/v1beta1" + description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" +} + +variable "helm_manifest_experiment_enabled" { + type = bool + default = true + description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" +} + +locals { + kubeconfig_file_enabled = var.kubeconfig_file_enabled + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + + # Eventually we might try to get this from an environment variable + kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version + + exec_profile = local.kube_exec_auth_enabled && var.kube_exec_auth_aws_profile_enabled ? [ + "--profile", var.kube_exec_auth_aws_profile + ] : [] + + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ + "--role-arn", local.kube_exec_auth_role_arn + ] : [] + + certificate_authority_data = module.eks.outputs.eks_cluster_certificate_authority_data + eks_cluster_id = module.eks.outputs.eks_cluster_id + eks_cluster_endpoint = module.eks.outputs.eks_cluster_endpoint +} + +data "aws_eks_cluster_auth" "eks" { + count = local.kube_data_auth_enabled ? 1 : 0 + name = local.eks_cluster_id +} + +provider "kubernetes" { + host = local.eks_cluster_endpoint + cluster_ca_certificate = base64decode(local.certificate_authority_data) + token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster + # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. + config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + config_context = var.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } +} diff --git a/modules/eks/tailscale/providers.tf b/modules/eks/tailscale/providers.tf new file mode 100644 index 000000000..c2419aabb --- /dev/null +++ b/modules/eks/tailscale/providers.tf @@ -0,0 +1,29 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/eks/tailscale/remote-state.tf b/modules/eks/tailscale/remote-state.tf new file mode 100644 index 000000000..42155e076 --- /dev/null +++ b/modules/eks/tailscale/remote-state.tf @@ -0,0 +1,20 @@ +module "eks" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = var.eks_component_name + + context = module.this.context +} + +data "aws_eks_cluster" "kubernetes" { + count = local.enabled ? 1 : 0 + + name = module.eks.outputs.eks_cluster_id +} + +data "aws_subnet" "vpc_subnets" { + for_each = local.enabled ? data.aws_eks_cluster.kubernetes[0].vpc_config[0].subnet_ids : [] + + id = each.value +} diff --git a/modules/eks/tailscale/variables.tf b/modules/eks/tailscale/variables.tf new file mode 100644 index 000000000..90be9cc2a --- /dev/null +++ b/modules/eks/tailscale/variables.tf @@ -0,0 +1,63 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "eks_component_name" { + type = string + description = "The name of the eks component" + default = "eks/cluster" +} + +variable "chart_values" { + type = any + description = "Addition map values to yamlencode as `helm_release` values." + default = {} +} + +variable "deployment_name" { + type = string + description = "Name of the tailscale deployment, defaults to `tailscale` if this is null" + default = null +} + +variable "image_repo" { + type = string + description = "Image repository for the deployment" + default = "ghcr.io/tailscale/tailscale" +} + +variable "image_tag" { + type = string + description = "Image Tag for the deployment." + default = "latest" +} + +variable "create_namespace" { + type = bool + description = "Create the namespace if it does not yet exist. Defaults to `false`." + default = false +} + +variable "kubernetes_namespace" { + type = string + description = "The namespace to install the release into." +} + +variable "kube_secret" { + type = string + description = "Kube Secret Name for tailscale" + default = "tailscale" +} + +variable "routes" { + type = list(string) + description = "List of CIDR Ranges or IPs to allow Tailscale to connect to" + default = [] +} + +variable "env" { + type = map(string) + description = "Map of ENV vars in the format `key=value`. These ENV vars will be set in the `utils` provider before executing the data source" + default = null +} diff --git a/modules/eks/tailscale/versions.tf b/modules/eks/tailscale/versions.tf new file mode 100644 index 000000000..8b70f9f52 --- /dev/null +++ b/modules/eks/tailscale/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.7.1" + } + } +}