Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ override.tf.json

# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
# example: *tfplan*
*tfplan*

# Ignore CLI configuration files
.terraformrc
Expand Down
42 changes: 42 additions & 0 deletions src/config/hub-and-spoke.tfvars
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,40 @@ connectivity = {
# allowed_network_ranges = ["0.0.0.0/0"]
# }

# platform_kubernetes = {
# "eu01" = {
# region = "eu01"
# network = {
# mode = "sna"
# }
# cluster = {
# name = "pltfmk8s"
# kubernetes_version_min = "1.35"
# node_pools = [
# {
# name = "small-a"
# machine_type = "g3i.4"
# minimum = 2
# maximum = 2
# availability_zones = ["eu01-1"]
# },
# {
# name = "small-b"
# machine_type = "g3i.4"
# minimum = 2
# maximum = 2
# availability_zones = ["eu01-2"]
# }
# ]
# }
#
# # Defaults to disabled. Set true to enable encrypted storage class setup.
# encrypted_volumes = {
# enabled = false
# }
# }
# }

###############
## SANDBOXES ##
###############
Expand All @@ -112,6 +146,14 @@ landing_zones = {
# Set corporate = true for network area connectivity, false for public internet
corporate = true
network_prefix_length = 24

# Optional: create namespace service in central platform Kubernetes cluster
# namespace_service = {
# enabled = true
# namespace = "data-prod"
# dns_subdomain = "app"
# secretsmanager = true
# }
}

# Public landing zone — no network area, uses STACKIT's default public networking
Expand Down
32 changes: 32 additions & 0 deletions src/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,37 @@ module "devops" {
allowed_network_ranges = var.devops.allowed_network_ranges
}

#########################
## PLATFORM KUBERNETES ##
#########################

module "platform_kubernetes" {
source = "./modules/platform-kubernetes"
for_each = var.platform_kubernetes

owner_email = var.owner_email
naming_pattern = "${var.company_code}-pltfm-k8s-${each.value.region}"
parent_container_id = module.governance.folder_container_ids["platform"]
labels = var.labels
region = each.value.region
role_assignments = each.value.role_assignments
cluster = each.value.cluster
observability = each.value.observability
encrypted_volumes = each.value.encrypted_volumes
debug_bastion = each.value.debug_bastion

network = {
mode = each.value.network.mode
sna_network_area_id = each.value.network.sna_network_area_id != null ? each.value.network.sna_network_area_id : try(module.connectivity[0].network_area_id, null)
}

dns = {
enabled = each.value.dns.enabled
create_zones = each.value.dns.create_zones
zones = length(each.value.dns.zones) > 0 ? each.value.dns.zones : compact(distinct([for lz in values(module.landing_zone) : try(lz.dns_zone_dns_name, null)]))
}
}

###############
## SANDBOXES ##
###############
Expand Down Expand Up @@ -98,5 +129,6 @@ module "landing_zone" {
role_assignments = each.value.role_assignments
network_prefix_length = each.value.network_prefix_length
custom_roles = each.value.custom_roles
observability = each.value.observability
firewall_next_hop_ip = var.connectivity != null && var.connectivity.firewall != null ? module.connectivity[0].firewall_next_hop_ip : null # if firewall is enabled, pass the next hop IP to the landing zones for route configuration
}
2 changes: 1 addition & 1 deletion src/modules/connectivity/5-firewall.tf
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ resource "stackit_image" "firewall" {

project_id = stackit_resourcemanager_project.this.project_id
name = var.firewall.name
local_file_path = "./firewall-image.qcow2"
local_file_path = var.firewall != null ? "${path.root}/firewall-image.qcow2" : null
disk_format = "qcow2"
min_disk_size = 16
min_ram = 2
Expand Down
12 changes: 12 additions & 0 deletions src/modules/landing-zone/8-observability.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
###################
## OBSERVABILITY ##
###################

resource "stackit_observability_instance" "this" {
count = var.observability.enabled ? 1 : 0

project_id = stackit_resourcemanager_project.this.project_id
name = var.observability.name != null ? var.observability.name : "${var.naming_pattern}-obs"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

for the other resources there is no suffix referencing the resource type, like -obs here. Maybe -general or -default or -common or no suffix

plan_name = var.observability.plan_name
acl = var.observability.acl
}
31 changes: 31 additions & 0 deletions src/modules/landing-zone/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,35 @@ output "connected_network_area_id" {
output "landing_zone_type" {
description = "The type of the landing zone, either 'corporate' or 'public'."
value = var.corporate ? "corporate" : "public"
}

output "secretsmanager_instance_id" {
description = "The ID of the landing zone Secrets Manager instance."
value = stackit_secretsmanager_instance.this.instance_id
}

output "observability_instance_id" {
description = "The optional observability instance ID in the landing zone project."
value = var.observability.enabled ? stackit_observability_instance.this[0].instance_id : null
}

output "observability_grafana_url" {
description = "The Grafana URL of the optional landing zone observability instance."
value = var.observability.enabled ? stackit_observability_instance.this[0].grafana_url : null
}

output "observability_grafana_admin_user" {
description = "The initial Grafana admin user of the optional landing zone observability instance."
value = var.observability.enabled ? stackit_observability_instance.this[0].grafana_initial_admin_user : null
}

output "observability_grafana_admin_password" {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

the user + password should not be an output since it will not be used later on in the terraform

description = "The initial Grafana admin password of the optional landing zone observability instance."
value = var.observability.enabled ? stackit_observability_instance.this[0].grafana_initial_admin_password : null
sensitive = true
}

output "observability_metrics_push_url" {
description = "The Prometheus remote-write URL of the optional landing zone observability instance."
value = var.observability.enabled ? stackit_observability_instance.this[0].metrics_push_url : null
}
11 changes: 11 additions & 0 deletions src/modules/landing-zone/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,15 @@ variable "secretsmanager_acls" {
type = list(string)
description = "List of ACL rules for the Secrets Manager instance. Set to empty list for no ACLs or null to skip Secrets Manager creation."
default = []
}

variable "observability" {
type = object({
enabled = optional(bool, false)
plan_name = optional(string, "Observability-Starter-EU01")
acl = optional(list(string), [])
name = optional(string, null)
})
description = "Optional observability instance configuration in the landing zone project."
default = {}
}
22 changes: 22 additions & 0 deletions src/modules/platform-kubernetes/1-project.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
locals {
project_labels = merge(
{ "region" = var.region },
var.network.mode == "sna" && var.network.sna_network_area_id != null ? { "networkArea" = var.network.sna_network_area_id } : {},
var.labels
)
}

resource "stackit_resourcemanager_project" "this" {
parent_container_id = var.parent_container_id
name = var.project_name != null ? var.project_name : var.naming_pattern
owner_email = var.owner_email
labels = length(local.project_labels) > 0 ? local.project_labels : null
}

resource "stackit_authorization_project_role_assignment" "this" {
for_each = { for assignment in var.role_assignments : "${assignment.role}-${assignment.subject}" => assignment }

resource_id = stackit_resourcemanager_project.this.project_id
role = each.value.role
subject = each.value.subject
}
12 changes: 12 additions & 0 deletions src/modules/platform-kubernetes/2-dns-zones.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
locals {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I would put 2-dns-zones.tf, 2-network-area-membership and 2-sna-network in one 2-network.tf

dns_extension_zones = distinct(compact(var.dns.zones))
}

resource "stackit_dns_zone" "ske_extension" {
for_each = var.dns.create_zones ? { for zone in local.dns_extension_zones : zone => zone } : {}

project_id = stackit_resourcemanager_project.this.project_id
name = each.value
dns_name = each.value
contact_email = var.owner_email
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
resource "time_sleep" "wait_for_network_area_membership" {
count = var.network.mode == "sna" ? 1 : 0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

input variable should be a boolean


# Allow backend propagation after project label updates before SKE SNA validation.
create_duration = "30s"

depends_on = [stackit_resourcemanager_project.this]
}
8 changes: 8 additions & 0 deletions src/modules/platform-kubernetes/2-observability.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
resource "stackit_observability_instance" "this" {
count = var.observability.enabled ? 1 : 0

project_id = stackit_resourcemanager_project.this.project_id
name = var.observability.name != null ? var.observability.name : "${var.naming_pattern}-obs"
plan_name = var.observability.plan_name
acl = var.observability.acl
}
8 changes: 8 additions & 0 deletions src/modules/platform-kubernetes/2-sna-network.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
resource "stackit_network" "sna" {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

no routing table here. I´m assuming that even if it is connected to sna, the traffic will not go through the firewall but directly to the internet

count = local.use_sna ? 1 : 0

project_id = stackit_resourcemanager_project.this.project_id
name = "${var.cluster.name}-sna"
ipv4_prefix_length = var.network.sna_network_prefix_length
routed = true
}
82 changes: 82 additions & 0 deletions src/modules/platform-kubernetes/3-cluster.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
locals {
use_sna = lower(var.network.mode) == "sna"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this local is not neccessary if the input variable would be a boolean already eg "sna_enabled"

also for the time_sleep resource this is used: count = var.network.mode == "sna" ? 1 : 0

which would fail for "SNA" anyway


effective_observability_instance_id = var.observability.enabled ? stackit_observability_instance.this[0].instance_id : null

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

no local needed since the value is not reused, can be defined inline in line 65 --> local.effective_observability_instance_id

effective_dns_zones = var.dns.create_zones ? sort([
for zone in values(stackit_dns_zone.ske_extension) : zone.dns_name
]) : local.dns_extension_zones
default_node_pools = [

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

the default_node_pools should go into the var.cluster variable as default
also allow_system_components is not set, I would split it up in "system" and "application" node pool

{
name = "ha-a"
machine_type = "g3i.4"
minimum = 2
maximum = 2
availability_zones = ["${var.region}-1"]
volume_size = 20
volume_type = "storage_premium_perf1"
os_name = "flatcar"
labels = {}
},
{
name = "ha-b"
machine_type = "g3i.4"
minimum = 2
maximum = 2
availability_zones = ["${var.region}-2"]
volume_size = 20
volume_type = "storage_premium_perf1"
os_name = "flatcar"
labels = {}
}
]
effective_node_pools = length(var.cluster.node_pools) > 0 ? var.cluster.node_pools : local.default_node_pools
}

resource "stackit_ske_cluster" "this" {
project_id = stackit_resourcemanager_project.this.project_id
region = var.region
name = var.cluster.name
depends_on = [time_sleep.wait_for_network_area_membership, stackit_dns_zone.ske_extension]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

depends_on always at the bottom for readability: https://developer.hashicorp.com/terraform/language/style#resource-order

kubernetes_version_min = var.cluster.kubernetes_version_min
node_pools = local.effective_node_pools

maintenance = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

since all key value pairs are equal: maintenance = var.cluster.maintenance should work, no need for mapping

enable_kubernetes_version_updates = var.cluster.maintenance.enable_kubernetes_version_updates
enable_machine_image_version_updates = var.cluster.maintenance.enable_machine_image_version_updates
start = var.cluster.maintenance.start
end = var.cluster.maintenance.end
}

network = local.use_sna ? {
id = stackit_network.sna[0].network_id
control_plane = {
access_scope = "SNA"
}
} : {
id = null
control_plane = {
access_scope = "PUBLIC"
}
}

extensions = {
observability = {
enabled = var.observability.enabled
instance_id = local.effective_observability_instance_id
}
dns = {
enabled = var.dns.enabled && length(local.effective_dns_zones) > 0
zones = local.effective_dns_zones
}
}
}

resource "stackit_ske_kubeconfig" "this" {
project_id = stackit_resourcemanager_project.this.project_id
region = var.region
cluster_name = stackit_ske_cluster.this.name

refresh = true
expiration = 7200
refresh_before = 1800
}
50 changes: 50 additions & 0 deletions src/modules/platform-kubernetes/4-encrypted-volumes.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
data "stackit_service_accounts" "ske_internal" {
count = var.encrypted_volumes.enabled ? 1 : 0

project_id = stackit_resourcemanager_project.this.project_id
email_suffix = "@ske.sa.stackit.cloud"

depends_on = [stackit_ske_cluster.this]
}

resource "stackit_kms_keyring" "this" {
count = var.encrypted_volumes.enabled ? 1 : 0

project_id = stackit_resourcemanager_project.this.project_id
display_name = "${var.naming_pattern}-${var.encrypted_volumes.kms_keyring_name}"
}

resource "stackit_kms_key" "this" {
count = var.encrypted_volumes.enabled ? 1 : 0

project_id = stackit_resourcemanager_project.this.project_id
keyring_id = stackit_kms_keyring.this[0].keyring_id
display_name = "${var.naming_pattern}-${var.encrypted_volumes.kms_key_name}"
protection = "software"
algorithm = "aes_256_gcm"
purpose = "symmetric_encrypt_decrypt"
}

resource "stackit_service_account" "kms_manager" {
count = var.encrypted_volumes.enabled ? 1 : 0

project_id = stackit_resourcemanager_project.this.project_id
# STACKIT service account names are limited to 20 characters.
name = "${substr(var.naming_pattern, 0, 8)}-kms-mgr"
}

resource "stackit_authorization_project_role_assignment" "kms_admin" {
count = var.encrypted_volumes.enabled ? 1 : 0

resource_id = stackit_resourcemanager_project.this.project_id
role = "kms.admin"
subject = stackit_service_account.kms_manager[0].email
}

resource "stackit_authorization_service_account_role_assignment" "ske_impersonation" {
count = var.encrypted_volumes.enabled ? 1 : 0

resource_id = stackit_service_account.kms_manager[0].service_account_id
role = "user"
subject = data.stackit_service_accounts.ske_internal[0].items[0].email
}
Loading
Loading