diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b754cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +# intellij +*.iml +idea/ + diff --git a/exposition.tf b/exposition.tf new file mode 100644 index 0000000..feb5f23 --- /dev/null +++ b/exposition.tf @@ -0,0 +1,138 @@ +locals { + service_hostname = format("%s.localdomain", var.application_name) + at_least_one_port = length(var.ports) > 0 ? 1 : 0 + ports_map = { + for index, port in var.ports : format("port-%s", index) => port + } + exposed_ports_map = { + for index, port in var.ports : + format("port-%s", index) => port if port.expose == true + } + certificate_secret_name = format("%s-certificate", var.application_name) + at_least_one_port_exposed = length(local.exposed_ports_map) > 0 ? 1 : 0 +} + +resource "kubernetes_service_v1" "service" { + count = local.at_least_one_port + metadata { + name = var.application_name + namespace = var.namespace + labels = { + "app.kubernetes.io/part-of" = var.application_name + "app.kubernetes.io/managed-by" = "terraform" + } + } + spec { + type = "ClusterIP" + + dynamic port { + for_each = local.ports_map + content { + name = format("service-%s", port.key) + port = port.value.container_port + target_port = port.key + } + } + selector = { + "app.kubernetes.io/name" = local.label_name + } + } +} + +resource "kubernetes_manifest" "certificate" { + count = local.at_least_one_port_exposed + + manifest = { + apiVersion = "cert-manager.io/v1" + kind = "Certificate" + metadata = { + name = var.application_name + namespace = var.namespace + labels = { + "app.kubernetes.io/part-of" = var.application_name + "app.kubernetes.io/managed-by" = "terraform" + } + } + spec = { + secretName = local.certificate_secret_name + dnsNames = [ + local.service_hostname, + format("*.%s", local.service_hostname) + ] + issuerRef = { + kind = "ClusterIssuer" + name = "localdomain-issuer" + group = "cfssl-issuer.wikimedia.org" + } + } + } +} + +resource "kubernetes_ingress_v1" "ingress" { + for_each = local.exposed_ports_map + + metadata { + name = var.application_name + namespace = var.namespace + labels = { + "app.kubernetes.io/part-of" = var.application_name + "app.kubernetes.io/managed-by" = "terraform" + } + annotations = { + "traefik.ingress.kubernetes.io/router.middlewares" = "kube-ingress-gzip-compress@kubernetescrd" + "traefik.ingress.kubernetes.io/router.entrypoints" = "websecure" + } + } + spec { + rule { + host = local.service_hostname + http { + path { + path = "/" + backend { + service { + name = kubernetes_service_v1.service[0].metadata.0.name + port { + name = format("service-%s", each.key) + } + } + } + } + } + } + tls { + hosts = [local.service_hostname] + secret_name = local.certificate_secret_name + } + } +} + +# {{ application_name }}.localdomain IN CNAME internal-lb +resource "kubernetes_manifest" "record" { + count = local.at_least_one_port_exposed + + manifest = { + apiVersion = "externaldns.k8s.io/v1alpha1" + kind = "DNSEndpoint" + metadata = { + name = var.application_name + namespace = var.namespace + labels = { + "app.kubernetes.io/part-of" = var.application_name + "app.kubernetes.io/managed-by" = "terraform" + } + } + spec = { + endpoints = [ + { + dnsName = local.service_hostname + recordTTL = "180" + recordType = "CNAME" + targets = [ + "internal-lb.localdomain" + ] + } + ] + } + } +} diff --git a/input.tf b/input.tf new file mode 100644 index 0000000..19c714f --- /dev/null +++ b/input.tf @@ -0,0 +1,55 @@ +variable "application_name" { + type = string +} + +variable "namespace" { + default = "default" + type = string +} + +variable "image" { + type = object({ + name = string + tag = optional(string, "latest") + pull-policy = optional(string, "Always") + }) +} + +variable "args" { + type = list(string) + default = null +} + +variable "ports" { + type = list( + object({ + container_port = number + expose = optional(bool, false) + }) + ) + default = [] +} + +variable "env" { + type = list( + object({ + name = string + value = string + }) + ) + default = [] +} + +variable "config_content" { + type = string + default = null +} + +variable "volumes" { + type = map( + object({ + path = string + }) + ) + default = {} +} diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..2a4709e --- /dev/null +++ b/main.tf @@ -0,0 +1,121 @@ +resource "random_uuid" "identifier" { +} + +locals { + label_name = format("%s-%s", var.application_name, random_uuid.identifier.result) +} + +resource "kubernetes_persistent_volume_claim_v1" "storage" { + for_each = var.volumes + metadata { + name = format("%s-volumes-%s", var.application_name, each.key) + namespace = var.namespace + } + spec { + access_modes = ["ReadWriteMany"] + resources { + requests = { + storage = "2Gi" + } + } + } +} + +resource "kubernetes_config_map_v1" "configuration_file" { + count = var.config_content == null ? 0 : 1 + + metadata { + name = "configuration-files" + namespace = var.namespace + } + data = { + "config" = var.config_content + } +} + +resource "kubernetes_deployment_v1" "deployment" { + metadata { + name = var.application_name + namespace = var.namespace + labels = { + "app.kubernetes.io/part-of" = var.application_name + "app.kubernetes.io/managed-by" = "terraform" + } + } + spec { + selector { + match_labels = { + "app.kubernetes.io/name" = local.label_name + } + } + template { + metadata { + labels = { + "app.kubernetes.io/name" = local.label_name + } + } + spec { + container { + name = var.application_name + image = format("%s:%s", var.image.name, var.image.tag) + image_pull_policy = var.image.pull-policy + args = var.args + + dynamic port { + for_each = var.ports + content { + name = format("port-%s", port.key) + container_port = port.value.container_port + } + } + + dynamic env { + for_each = var.env + content { + name = env.value.name + value = env.value.value + } + } + + dynamic volume_mount { + for_each = var.config_content != null ? [ + kubernetes_config_map_v1.configuration_file.0.metadata.0.name + ] : [] + content { + name = "config-volume" + mount_path = "/application/config.json" + sub_path = "config" + } + } + + dynamic volume_mount { + for_each = var.volumes + content { + name = volume_mount.key + mount_path = volume_mount.value.path + } + } + } + + dynamic volume { + for_each = var.volumes + content { + name = volume.key + persistent_volume_claim { + claim_name = format("%s-volumes-%s", var.application_name, volume.key) + } + } + } + dynamic volume { + for_each = var.config_content != null ? [kubernetes_config_map_v1.configuration_file.0.metadata.0.name] : [] + content { + name = "config-volume" + config_map { + name = volume.value + } + } + } + } + } + } +} diff --git a/output.tf b/output.tf new file mode 100644 index 0000000..dd8b27a --- /dev/null +++ b/output.tf @@ -0,0 +1,8 @@ +output "deployment" { + value = kubernetes_deployment_v1.deployment +} + +output "record" { + value = format("Access to the application at https://%s", local.service_hostname) + depends_on = [kubernetes_manifest.record] +}