feature(iac): enable dns module to create one or many zones with none or many records

This commit is contained in:
Florian Heuer 2025-08-22 20:04:34 +02:00
parent 0fc20b8741
commit fcbd49b302
5 changed files with 282 additions and 229 deletions

View file

@ -1,47 +1,160 @@
# Terraform module to create DNS zone or record
# Terraform STACKIT DNS Zone and Record Set Module
## Example for main.tf
This module allows you to declaratively manage DNS zones and their associated record sets in STACKIT.
```tf
locals {
stackit_project_id = "fb06b3bf-70b6-45bf-b1a4-e84708b26f92"
region = "eu01"
env = "dev"
It supports:
- Creating one or more new DNS zones.
- Using pre-existing DNS zones by providing a `zone_id`.
- Creating one or more record sets within any managed zone.
## Usage Example
Here is an example of how to use the module to create one or many new zones and use another pre-existing one.
```terraform
terraform {
required_providers {
stackit = {
source = "stackitcloud/stackit"
version = "~> 0.59.0"
}
}
backend "s3" {
endpoints = {
s3 = "https://object.storage.eu01.onstackit.cloud"
}
bucket = "test-bucket"
key = "state/test/dns"
region = "eu01"
skip_region_validation = true
skip_metadata_api_check = true
skip_credentials_validation = true
skip_requesting_account_id = true
skip_s3_checksum = true
use_path_style = true
}
}
provider "stackit" {
# Configure your provider credentials, i.e.:
default_region = local.region
enable_beta_resources = true
service_account_key_path = "sa_key.json"
}
module "dns" {
source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public//terraform-modules/dns"
project_id = "my-stackit-project-id"
source = "./path/to/your/module" # Or a Git URL git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public//terraform-modules/dns
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
zone_name = "example-zone"
dns_name = "example.com"
zones = {
# EXAMPLE 1: Create multiple zones with multiple A records
"primary_domain" = {
name = "first domain"
dns_name = "test-b.stackit.rocks"
record_sets = {
"qa-test" = {
name = "qa-test"
type = "A"
ttl = 3600
records = ["192.0.1.1", "192.0.1.2"]
}
"beta-test" = {
name = "beta-test"
type = "A"
ttl = 3600
records = ["192.0.2.10", "192.0.2.20", "192.0.2.30"]
}
}
},
"secondary_domain" = {
name = "second domain"
dns_name = "test-a.stackit.rocks"
record_sets = {
"alpha-records" = {
name = "alpha-test"
type = "A"
ttl = 3600
records = ["192.10.2.10", "192.10.2.20", "192.10.2.30"]
}
}
},
contact_email = "admin@example.com"
description = "Main DNS zone - managed via Terraform"
default_ttl = 3600
# EXAMPLE 2: Use a pre-existing zone and add a new TXT and A record to it
"existing_domain" = {
zone_id = "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz"
record_sets = {
"spf_txt" = {
name = "@"
type = "TXT"
records = ["v=spf1 mx -all"]
ttl = 7200
comment = "this is a test txt record"
},
"spf_cname" = {
name = "exampledomain"
type = "A"
ttl = 3600
records = ["192.0.29.1"]
}
}
},
record_name = "www.example.com"
record_type = "A"
records = ["192.0.29.1"]
ttl = 3600
comment = "My example records - managed by Terraform"
# EXAMPLE 3: Create a new zone with no initial records
"empty_domain" = {
name = "My Empty Domain"
dns_name = "empty-example.com"
}
}
}
```
## Usage Options
## Inputs
### Use an Existing DNS Zone
| Variable Name | Description | Type | Required |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------- |
| `project_id` | STACKIT project ID to which the DNS resources are associated. | `string` | Yes |
| `zones` | A map of DNS zones to be created or managed. Each zone contains a `name`, `dns_name`, and `record_sets` map. | `map` | Yes |
| `record_sets` | A map of DNS record sets to create within this zone. Each record set contains a `name`, `type`, `records`, `ttl`, `comment`, and `active` attribute. | `map` | Optional |
#### If you already have a DNS zone created in STACKIT, simply provide the `zone_id`:
### Values for zones
```hcl
module "dns" {
source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public//terraform-modules/dns"
project_id = "your-project-id"
| Key | Description | Type | Required |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------- | ----------------------------------- |
| `zone_id` | The ID of an existing DNS zone to manage. If provided, the module will use the existing zone. If not provided, a new zone will be created. | `string` | Optional |
| `name` | The descriptive name of the DNS zone. | `string` | Optional |
| `dns_name` | The DNS name of the DNS zone. | `string` | Optional |
| `contact_email` | The contact email for the DNS zone. | `string` | Optional |
| `description` | A description of the DNS zone. | `string` | Optional |
| `acl` | The Access Control List (ACL) for the DNS zone. | `string` | Optional |
| `active` | Whether the DNS zone is active or not. | `bool` | Optional (currently non-functional) |
| `default_ttl` | The default Time-to-Live (TTL) for records in the DNS zone. | `number` | Optional |
| `expire_time` | The expiration time for the DNS zone. | `number` | Optional |
| `is_reverse_zone` | Whether the DNS zone is a reverse zone or not. | `bool` | Optional |
| `negative_cache` | The negative cache duration for the DNS zone. | `number` | Optional |
| `primaries` | A list of primary name servers for the DNS zone. | `list(string)` | Optional |
| `refresh_time` | The refresh time for the DNS zone. | `number` | Optional |
| `retry_time` | The retry time for the DNS zone. | `number` | Optional |
| `type` | The type of the DNS zone. Defaults to "primary" if not provided. | `string` | Optional |
zone_id = "preexisting-zone-id"
record_name = "www.example.com"
record_type = "A"
records = ["192.0.29.1"]
}
```
### Values for record_sets
| Key | Description | Type | Required |
| --------- | ------------------------------------------------------------- | -------------- | ----------------------------------- |
| `name` | The name of the DNS record set. | `string` | Yes |
| `type` | The type of the DNS record set. | `string` | Yes |
| `records` | A list of DNS records for the record set. | `list(string)` | Yes |
| `ttl` | The Time-to-Live (TTL) for the DNS records in the record set. | `number` | Optional |
| `comment` | A comment for the DNS record set. | `string` | Optional |
| `active` | Whether the DNS record set is active or not. | `bool` | Optional (currently non-functional) |
## Outputs
| Name | Description |
| ------------- | ------------------------------------------------------------------------------------------ |
| `zones` | A map of all managed DNS zone objects, including those created and those referenced by ID. |
| `record_sets` | A map of all created DNS record set objects. |
## Notes
Setting a zone or record to inactive by using `active = false` is currently not possible due to a bug in the provider. It is active by default.

View file

@ -1,34 +1,94 @@
resource "stackit_dns_zone" "this" {
count = var.zone_id == null ? 1 : 0
project_id = var.stackit_project_id
name = var.dns_zone_name
dns_name = var.dns_zone_dns_name
# main.tf
# Optional attributes
acl = var.dns_zone_acl
active = var.dns_zone_active
contact_email = var.dns_zone_contact_email
default_ttl = var.dns_zone_default_ttl
description = var.dns_zone_description
expire_time = var.dns_zone_expire_time
is_reverse_zone = var.dns_zone_is_reverse_zone
negative_cache = var.dns_zone_negative_cache
primaries = var.dns_zone_primaries
refresh_time = var.dns_zone_refresh_time
retry_time = var.dns_zone_retry_time
type = var.dns_zone_type
# --------------------------------------------------------------------------------------------------
# LOCAL VARIABLES
# --------------------------------------------------------------------------------------------------
locals {
# Create a map of zones to be created (where zone_id is not specified)
zones_to_create = { for k, v in var.zones : k => v if try(v.zone_id, null) == null }
# Create a map of zones to be referenced via data source (where zone_id is specified)
zones_to_read = { for k, v in var.zones : k => v if try(v.zone_id, null) != null }
# Merge the created resources and data sources into a single, unified map.
# This allows record sets to reference a zone regardless of whether it was created or read.
all_zones = merge(
{
for k, zone in stackit_dns_zone.this : k => zone
},
{
for k, zone in data.stackit_dns_zone.this : k => zone
}
)
# Flatten the nested record_sets structure into a single list, making it easy to iterate with for_each.
# Each item in the list retains a reference to its parent zone key.
flat_record_sets = flatten([
for zone_key, zone_config in var.zones : [
for record_key, record_config in try(zone_config.record_sets, {}) : {
zone_key = zone_key
record_key = record_key
name = record_config.name
type = record_config.type
records = record_config.records
ttl = try(record_config.ttl, null)
comment = try(record_config.comment, null)
active = try(record_config.active, null)
}
]
])
}
# --------------------------------------------------------------------------------------------------
# DNS ZONE RESOURCES (CREATE OR READ)
# --------------------------------------------------------------------------------------------------
# Create new DNS zones for configurations that do not have a zone_id
resource "stackit_dns_zone" "this" {
for_each = local.zones_to_create
project_id = var.project_id
name = each.value.name
dns_name = each.value.dns_name
contact_email = try(each.value.contact_email, null)
description = try(each.value.description, null)
acl = try(each.value.acl, null)
active = try(each.value.active, null)
default_ttl = try(each.value.default_ttl, null)
expire_time = try(each.value.expire_time, null)
is_reverse_zone = try(each.value.is_reverse_zone, null)
negative_cache = try(each.value.negative_cache, null)
primaries = try(each.value.primaries, null)
refresh_time = try(each.value.refresh_time, null)
retry_time = try(each.value.retry_time, null)
type = try(each.value.type, "primary")
}
# Read existing DNS zones for configurations that provide a zone_id
data "stackit_dns_zone" "this" {
for_each = local.zones_to_read
project_id = var.project_id
zone_id = each.value.zone_id
}
# --------------------------------------------------------------------------------------------------
# DNS RECORD SET RESOURCES
# --------------------------------------------------------------------------------------------------
resource "stackit_dns_record_set" "this" {
project_id = var.stackit_project_id
name = var.dns_record_set_name
records = var.dns_record_set_records
type = var.dns_record_set_type
#zone_id = var.dns_record_set_zone_id
zone_id = var.zone_id != null ? var.zone_id : stackit_dns_zone.this[0].zone_id
# The key is a unique combination of the zone and record keys for a stable address.
for_each = { for record in local.flat_record_sets : "${record.zone_key}.${record.record_key}" => record }
# Optional
active = var.dns_record_set_active
comment = var.dns_record_set_comment
ttl = var.dns_record_set_ttl
project_id = var.project_id
# Look up the correct zone_id from the unified 'all_zones' map
zone_id = local.all_zones[each.value.zone_key].zone_id
name = each.value.name
type = each.value.type
records = each.value.records
ttl = each.value.ttl
comment = each.value.comment
active = each.value.active
}

View file

@ -1,46 +1,11 @@
# === DNS Zone Outputs ===
output "zone_id" {
value = var.zone_id != null ? var.zone_id : stackit_dns_zone.this[0].zone_id
description = "The DNS zone ID used or created"
# outputs.tf
output "zones" {
description = "A map of all managed DNS zone objects, including those created and those referenced by ID."
value = local.all_zones
}
output "zone_primary_name_server" {
value = stackit_dns_zone.this.primary_name_server
description = "Primary name server"
}
output "zone_state" {
value = stackit_dns_zone.this.state
description = "State of the DNS zone"
}
output "zone_visibility" {
value = stackit_dns_zone.this.visibility
description = "Zone visibility"
}
output "zone_record_count" {
value = stackit_dns_zone.this.record_count
description = "Number of DNS records in the zone"
}
# === DNS Record Set Outputs ===
output "record_set_fqdn" {
value = stackit_dns_record_set.this.fqdn
description = "Fully qualified domain name of the record"
}
output "record_set_id" {
value = stackit_dns_record_set.this.record_set_id
description = "ID of the record set"
}
output "record_set_state" {
value = stackit_dns_record_set.this.state
description = "State of the record set"
}
output "record_set_error" {
value = stackit_dns_record_set.this.error
description = "Error during record creation (if any)"
output "record_sets" {
description = "A map of all created DNS record set objects."
value = stackit_dns_record_set.this
}

View file

@ -2,8 +2,7 @@ terraform {
required_providers {
stackit = {
source = "stackitcloud/stackit"
version = "~> 0.59.0"
version = "~> 0.61.0"
}
}
}

View file

@ -1,132 +1,48 @@
variable "stackit_project_id" {
description = "STACKIT project ID to which the dns zone is associated."
# variables.tf
variable "project_id" {
type = string
description = "STACKIT project ID to which the DNS resources are associated."
}
variable "zone_id" {
description = "ID of an existing DNS zone. If provided, no new zone will be created."
type = string
default = null
}
variable "zones" {
type = map(object({
# If zone_id is provided, the module will use the existing zone.
# Otherwise, a new zone will be created using the attributes below.
zone_id = optional(string)
# === DNS Zone variables ===
variable "dns_zone_name" {
description = "Descriptive name of the DNS zone."
type = string
}
# Required attributes for new zones
name = optional(string)
dns_name = optional(string)
variable "dns_zone_dns_name" {
description = "The actual zone name, e.g. example.com"
type = string
}
# Optional attributes for new zones
contact_email = optional(string)
description = optional(string)
acl = optional(string)
active = optional(bool)
default_ttl = optional(number)
expire_time = optional(number)
is_reverse_zone = optional(bool)
negative_cache = optional(number)
primaries = optional(list(string))
refresh_time = optional(number)
retry_time = optional(number)
type = optional(string, "primary")
variable "dns_zone_acl" {
description = "Access control list, e.g. 0.0.0.0/0,::/0"
type = string
default = null
}
# A map of DNS record sets to create within this zone.
# The key is a logical name for the record set (e.g., "www", "mx_record").
record_sets = optional(map(object({
# Required record set attributes
name = string
type = string
records = list(string)
variable "dns_zone_active" {
description = "Whether the zone is active. Defaults to true"
type = bool
default = true
}
variable "dns_zone_contact_email" {
description = "Contact email for the zone."
type = string
default = null
}
variable "dns_zone_default_ttl" {
description = "The default value of the TTL for new resource records in the DNS zone. Time in seconds. Defaults to 3600"
type = number
default = 3600
}
variable "dns_zone_description" {
description = "An additional descriptive free text for the DNS zone."
type = string
default = null
}
variable "dns_zone_expire_time" {
description = "If the secondary DNS server cannot perform a serial check in this interval, it must assume that its copy of the zone is obsolete and discard it. Time in seconds. Defaults to 1209600"
type = number
default = 1209600
}
variable "dns_zone_is_reverse_zone" {
description = "Indicates whether a zone is a reverse DNS zone or a forward zone. Both have a different set of allowed resource record types. Defaults to false"
type = bool
default = false
}
variable "dns_zone_negative_cache" {
description = "The response about the non-existence of a resource record is cached for this interval. Time in seconds. Defaults to 60"
type = number
default = 60
}
variable "dns_zone_primaries" {
description = "List of primary nameservers (for secondary zone)"
type = list(string)
default = []
}
variable "dns_zone_refresh_time" {
description = "This is the amount of time a secondary DNS server will wait before checking with the primary for a new serial if a zone refresh fails. Time in seconds. Defaults to 3600"
type = number
default = 3600
}
variable "dns_zone_retry_time" {
description = "This is the amount of time a secondary DNS server waits before retrying a zone update if the update fails. Time in seconds. Defaults to 600"
type = number
default = 600
}
variable "dns_zone_type" {
description = "Zone type (primary or secondary). Defaults to primary"
type = string
default = "primary"
}
# === DNS Record Set variables ===
variable "dns_record_set_name" {
description = "Name of the DNS record (e.g. 'www.example.com')"
type = string
}
variable "dns_record_set_records" {
description = "List of DNS record values"
type = list(string)
}
variable "dns_record_set_type" {
description = "Record type (e.g. 'A', 'CNAME', etc.)"
type = string
}
variable "dns_record_set_zone_id" {
description = "The zone ID for the DNS record"
type = string
}
variable "dns_record_set_ttl" {
description = "Time to live (TTL) for the record"
type = number
default = 3600
}
variable "dns_record_set_active" {
description = "Whether the record set is active"
type = bool
default = true
}
variable "dns_record_set_comment" {
description = "Optional comment for the record"
type = string
default = null
# Optional record set attributes
ttl = optional(number)
comment = optional(string)
active = optional(bool, true)
})), {})
}))
description = "A map of DNS zones to manage. The key is a logical name for the zone."
default = {}
}