diff --git a/terraform/README.md b/terraform/README.md new file mode 100644 index 00000000..0c55e780 --- /dev/null +++ b/terraform/README.md @@ -0,0 +1,30 @@ + +First install Pomerium Ingress Controller + +```terraform +provider "kubernetes" { + +} + +module "pomerium_ingress_controller" { + source = "git:https://github.com/pomerium/ingress-controller//terraform?ref=v0.28.0" +} +``` + +Once Pomerium Ingress Controller is installed, you may reference additional configurations via the `Pomerium` CRD. +See https://www.pomerium.com/docs/k8s/configure + +```terraform +resource "kubernetes_manifest" "pomerium_config" { + manifest = { + apiVersion = "ingress.pomerium.io/v1" + kind = "Pomerium" + metadata = { + name = "global" + } + spec = { + secrets = "pomerium-ingress-controller/bootstrap" + } + } +} +``` diff --git a/terraform/cluster_role_bindings.tf b/terraform/cluster_role_bindings.tf new file mode 100644 index 00000000..2f611936 --- /dev/null +++ b/terraform/cluster_role_bindings.tf @@ -0,0 +1,37 @@ +resource "kubernetes_cluster_role_binding" "controller" { + metadata { + name = var.controller_cluster_role_name + labels = var.cluster_role_labels + } + + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "ClusterRole" + name = kubernetes_cluster_role.controller.metadata[0].name + } + + subject { + kind = "ServiceAccount" + name = kubernetes_service_account.controller.metadata[0].name + namespace = kubernetes_namespace.pomerium.metadata[0].name + } +} + +resource "kubernetes_cluster_role_binding" "gen_secrets" { + metadata { + name = var.gen_secrets_cluster_role_name + labels = var.cluster_role_labels + } + + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "ClusterRole" + name = kubernetes_cluster_role.gen_secrets.metadata[0].name + } + + subject { + kind = "ServiceAccount" + name = kubernetes_service_account.gen_secrets.metadata[0].name + namespace = kubernetes_namespace.pomerium.metadata[0].name + } +} diff --git a/terraform/cluster_roles.tf b/terraform/cluster_roles.tf new file mode 100644 index 00000000..e2b6b2cb --- /dev/null +++ b/terraform/cluster_roles.tf @@ -0,0 +1,61 @@ +resource "kubernetes_cluster_role" "controller" { + metadata { + name = var.controller_cluster_role_name + labels = var.cluster_role_labels + } + + rule { + api_groups = [""] + resources = ["services", "endpoints", "secrets"] + verbs = ["get", "list", "watch"] + } + + rule { + api_groups = [""] + resources = ["services/status", "secrets/status", "endpoints/status"] + verbs = ["get"] + } + + rule { + api_groups = ["networking.k8s.io"] + resources = ["ingresses", "ingressclasses"] + verbs = ["get", "list", "watch"] + } + + rule { + api_groups = ["networking.k8s.io"] + resources = ["ingresses/status"] + verbs = ["get", "patch", "update"] + } + + rule { + api_groups = ["ingress.pomerium.io"] + resources = ["pomerium"] + verbs = ["get", "list", "watch"] + } + + rule { + api_groups = ["ingress.pomerium.io"] + resources = ["pomerium/status"] + verbs = ["get", "update", "patch"] + } + + rule { + api_groups = [""] + resources = ["events"] + verbs = ["create", "patch"] + } +} + +resource "kubernetes_cluster_role" "gen_secrets" { + metadata { + name = var.gen_secrets_cluster_role_name + labels = var.cluster_role_labels + } + + rule { + api_groups = [""] + resources = ["secrets"] + verbs = ["create"] + } +} diff --git a/terraform/crd.tf b/terraform/crd.tf new file mode 100644 index 00000000..11ffccad --- /dev/null +++ b/terraform/crd.tf @@ -0,0 +1,3 @@ +resource "kubernetes_manifest" "pomerium_crd" { + manifest = yamldecode(file("${path.module}/crd.yaml")) +} diff --git a/terraform/crd.yaml b/terraform/crd.yaml new file mode 120000 index 00000000..5772f39e --- /dev/null +++ b/terraform/crd.yaml @@ -0,0 +1 @@ +../config/crd/bases/ingress.pomerium.io_pomerium.yaml \ No newline at end of file diff --git a/terraform/deployment.tf b/terraform/deployment.tf new file mode 100644 index 00000000..6fbdbbf1 --- /dev/null +++ b/terraform/deployment.tf @@ -0,0 +1,144 @@ +resource "kubernetes_deployment" "pomerium" { + metadata { + name = var.deployment_name + namespace = var.namespace_name + labels = var.deployment_labels + } + + lifecycle { + ignore_changes = [ + metadata[0].annotations + ] + } + + spec { + replicas = var.deployment_replicas + + selector { + match_labels = { + "app.kubernetes.io/name" = "pomerium-ingress-controller" + } + } + + template { + metadata { + labels = { + "app.kubernetes.io/name" = "pomerium-ingress-controller" + } + } + + spec { + service_account_name = kubernetes_service_account.controller.metadata[0].name + termination_grace_period_seconds = 10 + + security_context { + run_as_non_root = true + } + + node_selector = merge(local.default_node_selector, var.node_selector) + + container { + name = "pomerium-ingress-controller" + image = "${var.image_repository}:${var.image_tag}" + image_pull_policy = var.image_pull_policy + + args = compact([ + "all-in-one", + "--pomerium-config=${var.pomerium_config_name}", + "--update-status-from-service=${var.namespace_name}/pomerium-proxy", + "--metrics-bind-address=$(POD_IP):9090", + var.enable_databroker ? "--databroker-auto-tls=pomerium-databroker.${var.namespace_name}.svc" : null, + ]) + + env { + name = "TMPDIR" + value = "/tmp" + } + + env { + name = "XDG_CACHE_HOME" + value = "/tmp" + } + + env { + name = "POD_IP" + value_from { + field_ref { + field_path = "status.podIP" + } + } + } + + port { + container_port = 8443 + name = "https" + protocol = "TCP" + } + + port { + container_port = 8080 + name = "http" + protocol = "TCP" + } + + port { + container_port = 9090 + name = "metrics" + protocol = "TCP" + } + + dynamic "port" { + for_each = var.enable_databroker ? [1] : [] + content { + container_port = 5443 + name = "databroker" + protocol = "TCP" + } + } + + resources { + limits = { + cpu = var.resources_limits_cpu + memory = var.resources_limits_memory + } + + requests = { + cpu = var.resources_requests_cpu + memory = var.resources_requests_memory + } + } + + security_context { + allow_privilege_escalation = false + read_only_root_filesystem = true + run_as_group = 65532 + run_as_non_root = true + run_as_user = 65532 + } + + volume_mount { + name = "tmp" + mount_path = "/tmp" + } + } + + dynamic "toleration" { + for_each = var.tolerations + content { + key = lookup(toleration.value, "key", null) + operator = lookup(toleration.value, "operator", null) + value = lookup(toleration.value, "value", null) + effect = lookup(toleration.value, "effect", null) + toleration_seconds = lookup(toleration.value, "toleration_seconds", null) + } + } + + volume { + name = "tmp" + + empty_dir {} + } + } + } + } +} diff --git a/terraform/gen_secrets.tf b/terraform/gen_secrets.tf new file mode 100644 index 00000000..a9f074d7 --- /dev/null +++ b/terraform/gen_secrets.tf @@ -0,0 +1,61 @@ +resource "kubernetes_job" "gen_secrets" { + metadata { + name = var.job_name + namespace = var.namespace_name + labels = var.deployment_labels + } + + lifecycle { + ignore_changes = [ + metadata[0].annotations + ] + } + + spec { + template { + metadata { + name = var.job_name + labels = var.deployment_labels + } + + spec { + service_account_name = kubernetes_service_account.gen_secrets.metadata[0].name + restart_policy = "OnFailure" + + security_context { + fs_group = 1000 + run_as_non_root = true + run_as_user = 1000 + } + + node_selector = merge(local.default_node_selector, var.node_selector) + + container { + name = "gen-secrets" + image = "${var.image_repository}:${var.image_tag}" + image_pull_policy = "IfNotPresent" + + args = [ + "gen-secrets", + "--secrets=${var.namespace_name}/bootstrap", + ] + + security_context { + allow_privilege_escalation = false + } + } + + dynamic "toleration" { + for_each = var.tolerations + content { + key = lookup(toleration.value, "key", null) + operator = lookup(toleration.value, "operator", null) + value = lookup(toleration.value, "value", null) + effect = lookup(toleration.value, "effect", null) + toleration_seconds = lookup(toleration.value, "toleration_seconds", null) + } + } + } + } + } +} diff --git a/terraform/ingress_class.tf b/terraform/ingress_class.tf new file mode 100644 index 00000000..1b6eeb83 --- /dev/null +++ b/terraform/ingress_class.tf @@ -0,0 +1,10 @@ +resource "kubernetes_ingress_class" "pomerium" { + metadata { + name = var.ingress_class_name + labels = var.labels + } + + spec { + controller = "pomerium.io/ingress-controller" + } +} diff --git a/terraform/locals.tf b/terraform/locals.tf new file mode 100644 index 00000000..46cfeab2 --- /dev/null +++ b/terraform/locals.tf @@ -0,0 +1,5 @@ +locals { + default_node_selector = { + "kubernetes.io/os" = "linux" + } +} diff --git a/terraform/namespace.tf b/terraform/namespace.tf new file mode 100644 index 00000000..1dca53f6 --- /dev/null +++ b/terraform/namespace.tf @@ -0,0 +1,6 @@ +resource "kubernetes_namespace" "pomerium" { + metadata { + name = var.namespace_name + labels = var.labels + } +} diff --git a/terraform/service_accounts.tf b/terraform/service_accounts.tf new file mode 100644 index 00000000..fba96429 --- /dev/null +++ b/terraform/service_accounts.tf @@ -0,0 +1,15 @@ +resource "kubernetes_service_account" "controller" { + metadata { + name = var.controller_service_account_name + namespace = kubernetes_namespace.pomerium.metadata[0].name + labels = var.service_account_labels + } +} + +resource "kubernetes_service_account" "gen_secrets" { + metadata { + name = var.gen_secrets_service_account_name + namespace = kubernetes_namespace.pomerium.metadata[0].name + labels = var.service_account_labels + } +} diff --git a/terraform/services.tf b/terraform/services.tf new file mode 100644 index 00000000..976c652a --- /dev/null +++ b/terraform/services.tf @@ -0,0 +1,75 @@ +resource "kubernetes_service" "proxy" { + count = var.proxy_service_type == null ? 0 : 1 + + metadata { + name = "pomerium-proxy" + namespace = kubernetes_namespace.pomerium.metadata[0].name + labels = var.service_labels + } + + lifecycle { + ignore_changes = [ + metadata[0].annotations + ] + } + + spec { + selector = { + "app.kubernetes.io/name" = "pomerium-ingress-controller" + } + + external_traffic_policy = var.proxy_service_type == "LoadBalancer" ? "Local" : null + + port { + name = "https" + port = var.proxy_port_https + node_port = var.proxy_node_port_https + target_port = "https" + protocol = "TCP" + } + + dynamic "port" { + for_each = var.proxy_port_http != null ? [var.proxy_port_http] : [] + content { + name = "http" + port = port.value + node_port = var.proxy_node_port_http + target_port = "http" + protocol = "TCP" + } + } + + type = var.proxy_service_type + } +} + +resource "kubernetes_service" "databroker" { + count = var.enable_databroker ? 1 : 0 + + metadata { + name = "pomerium-databroker" + namespace = kubernetes_namespace.pomerium.metadata[0].name + labels = var.service_labels + } + + lifecycle { + ignore_changes = [ + metadata[0].annotations + ] + } + + spec { + selector = { + "app.kubernetes.io/name" = "pomerium-ingress-controller" + } + + port { + name = "databroker" + port = 443 + target_port = "databroker" + protocol = "TCP" + } + + type = "ClusterIP" + } +} diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 00000000..54b35bb3 --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,195 @@ +variable "namespace_name" { + description = "The name of the namespace to create" + type = string + default = "pomerium-ingress-controller" +} + +variable "labels" { + description = "Labels to apply to resources" + type = map(string) + default = { + "app.kubernetes.io/name" = "pomerium-ingress-controller" + } +} + +variable "image_repository" { + description = "Container image repository" + type = string + default = "pomerium/ingress-controller" +} + +variable "image_tag" { + description = "Container image tag" + type = string + default = "v0.27.0" +} + +variable "image_pull_policy" { + description = "Image pull policy" + type = string + default = "IfNotPresent" +} + +variable "controller_service_account_name" { + description = "Name of the controller service account" + type = string + default = "pomerium-ingress-controller" +} + +variable "gen_secrets_service_account_name" { + description = "Name of the gen-secrets service account" + type = string + default = "pomerium-ingress-controller-gen-secrets" +} + +variable "controller_cluster_role_name" { + description = "Name of the controller cluster role" + type = string + default = "pomerium-ingress-controller" +} + +variable "gen_secrets_cluster_role_name" { + description = "Name of the gen-secrets cluster role" + type = string + default = "pomerium-ingress-controller-gen-secrets" +} + +variable "deployment_name" { + description = "Name of the Deployment" + type = string + default = "pomerium-ingress-controller" +} + +variable "deployment_replicas" { + description = "Number of replicas for the Deployment" + type = number + default = 1 +} + +variable "resources_requests_cpu" { + description = "CPU requests for the Deployment" + type = string + default = "300m" +} + +variable "resources_requests_memory" { + description = "Memory requests for the Deployment" + type = string + default = "200Mi" +} + +variable "resources_limits_cpu" { + description = "CPU limits for the Deployment" + type = string + default = "5000m" +} + +variable "resources_limits_memory" { + description = "Memory limits for the Deployment" + type = string + default = "1Gi" +} + +variable "proxy_service_type" { + description = "Type of the Proxy Service" + type = string + default = "LoadBalancer" +} + +variable "ingress_class_name" { + description = "Name of the IngressClass" + type = string + default = "pomerium" +} + +variable "service_account_labels" { + description = "Labels to apply to service accounts" + type = map(string) + default = { + "app.kubernetes.io/name" = "pomerium-ingress-controller" + } +} + +variable "cluster_role_labels" { + description = "Labels to apply to cluster roles" + type = map(string) + default = { + "app.kubernetes.io/name" = "pomerium-ingress-controller" + } +} + +variable "service_labels" { + description = "Labels to apply to services" + type = map(string) + default = { + "app.kubernetes.io/name" = "pomerium-ingress-controller" + } +} + +variable "deployment_labels" { + description = "Labels to apply to the deployment" + type = map(string) + default = { + "app.kubernetes.io/name" = "pomerium-ingress-controller" + } +} + +variable "tolerations" { + description = "List of tolerations for the pods." + type = list(object({ + key = optional(string) + operator = optional(string, "Equal") + value = optional(string) + effect = optional(string) + toleration_seconds = optional(number) + })) + default = [] +} + +variable "job_name" { + description = "Name of the Job" + type = string + default = "pomerium-gen-secrets" +} + +variable "pomerium_config_name" { + description = "Name of the Pomerium CRD" + type = string + default = "global" +} + +variable "enable_databroker" { + description = "Enable the databroker" + type = bool + default = false +} + +variable "proxy_port_https" { + description = "Port for HTTPS" + type = number + default = 443 +} + +variable "proxy_port_http" { + description = "Port for HTTP" + type = number + default = 80 +} + +variable "proxy_node_port_https" { + description = "Node port for HTTPS, only used when proxy_service_type is NodePort" + type = number + default = null +} + +variable "proxy_node_port_http" { + description = "Host port for HTTP" + type = number + default = null +} + +variable "node_selector" { + description = "Node selector for the Deployment" + type = map(string) + default = {} +}