Repository: stackitcloud/terraform-provider-stackit
Pull request #801: Feature: CDN custom domain resource and data source
Author: login: malteehrlen
Created: 2025-05-02T09:55:06Z | Merged: 2025-05-05T11:10:43Z

## Description

<!-- **Please link some issue here describing what you are trying to achieve.**

In case there is no issue present for your PR, please consider creating one.
At least please give us some description what you are trying to achieve and why your change is needed. -->

Adds a resource and data source for the cdn custom domain resource

## Checklist

- [x] Issue was linked above
- [x] Code format was applied: `make fmt`
- [x] Examples were added / adjusted (see `examples/` directory)
- [x] Docs are up-to-date: `make generate-docs` (will be checked by CI)
- [x] Unit tests got implemented or updated
- [x] Acceptance tests got implemented or updated (see e.g. [here](https://github.com/stackitcloud/terraform-provider-stackit/blob/f5f99d170996b208672ae684b6da53420e369563/stackit/internal/services/dns/dns_acc_test.go))
- [x] Unit tests are passing: `make test` (will be checked by CI)
- [x] No linter issues: `make lint` (will be checked by CI)  



Diff:
diff --git a/docs/data-sources/cdn_custom_domain.md b/docs/data-sources/cdn_custom_domain.md
new file mode 100644
index 000000000..38c2da3ea
--- /dev/null
+++ b/docs/data-sources/cdn_custom_domain.md
@@ -0,0 +1,39 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "stackit_cdn_custom_domain Data Source - stackit"
+subcategory: ""
+description: |-
+  CDN distribution data source schema.
+  ~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources.
+---
+
+# stackit_cdn_custom_domain (Data Source)
+
+CDN distribution data source schema.
+
+~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources.
+
+## Example Usage
+
+```terraform
+data "stackit_cdn_custom_domain" "example" {
+  project_id      = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+  distribution_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+  name            = "https://xxx.xxx"
+}
+```
+
+<!-- schema generated by tfplugindocs -->
+## Schema
+
+### Required
+
+- `distribution_id` (String) CDN distribution ID
+- `name` (String)
+- `project_id` (String) STACKIT project ID associated with the distribution
+
+### Read-Only
+
+- `errors` (List of String) List of distribution errors
+- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`distribution_id`".
+- `status` (String) Status of the distribution
diff --git a/docs/guides/stackit_cdn_with_custom_domain.md b/docs/guides/stackit_cdn_with_custom_domain.md
new file mode 100644
index 000000000..78d9cb06e
--- /dev/null
+++ b/docs/guides/stackit_cdn_with_custom_domain.md
@@ -0,0 +1,60 @@
+---
+page_title: "Using STACKIT CDN with your own domain"
+---
+# Using STACKIT CDN with your own domain
+
+## Overview
+
+This guide outlines the process of creating a STACKIT CDN distribution and configuring it to make use of an existing domain using STACKIT DNS.
+
+## Steps
+
+1. **Create a STACKIT CDN and DNS Zone**
+    
+    Create the CDN distribution and the DNS zone.
+    
+    ```terraform
+    resource "stackit_cdn_distribution" "example_distribution" {
+      project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+      config = {
+        backend = {
+          type       = "http"
+          origin_url = "mybackend.onstackit.cloud"
+        }
+        regions = ["EU", "US", "ASIA", "AF", "SA"]
+      }
+    }
+    
+    resource "stackit_dns_zone" "example_zone" {
+      project_id    = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+      name          = "My DNS zone"
+      dns_name      = "myapp.runs.onstackit.cloud"
+      contact_email = "aa@bb.ccc"
+      type          = "primary"
+    }
+    ```
+    
+2. **Add CNAME record to your DNS zone**
+
+    If you want to redirect your entire domain to the CDN, you can instead use an A record.
+    ```terraform
+    resource "stackit_dns_record_set" "example" {
+      project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+      zone_id    = stackit_dns_zone.example_zone.zone_id
+      name       = "cdn"
+      type       = "CNAME"
+      records    = ["${stackit_cdn_distribution.domains[0].name}."]
+    }
+    ```
+    
+3. **Create a STACKIT CDN Custom Domain**
+    ```terraform
+    # Create a CDN custom domain
+    resource "stackit_cdn_custom_domain" "example" {
+      project_id      = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+      distribution_id = stackit_cdn_distribution.example_distribution.distribution_id
+      name            = "${stackit_dns_record_set.example.name}.${stackit_dns_zone.dns_name}"
+    }
+    ```
+    
+    Now, you can access your content on the url `cdn.myapp.runs.onstackit.cloud`.
diff --git a/docs/resources/cdn_custom_domain.md b/docs/resources/cdn_custom_domain.md
new file mode 100644
index 000000000..fbceba302
--- /dev/null
+++ b/docs/resources/cdn_custom_domain.md
@@ -0,0 +1,40 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "stackit_cdn_custom_domain Resource - stackit"
+subcategory: ""
+description: |-
+  CDN distribution data source schema.
+  ~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources.
+---
+
+# stackit_cdn_custom_domain (Resource)
+
+CDN distribution data source schema.
+
+~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources.
+
+## Example Usage
+
+```terraform
+# Create a CDN custom domain
+resource "stackit_cdn_custom_domain" "example" {
+  project_id      = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+  distribution_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+  name            = "https://xxx.xxx"
+}
+```
+
+<!-- schema generated by tfplugindocs -->
+## Schema
+
+### Required
+
+- `distribution_id` (String) CDN distribution ID
+- `name` (String)
+- `project_id` (String) STACKIT project ID associated with the distribution
+
+### Read-Only
+
+- `errors` (List of String) List of distribution errors
+- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`distribution_id`".
+- `status` (String) Status of the distribution
diff --git a/docs/resources/cdn_distribution.md b/docs/resources/cdn_distribution.md
index 2704a1e68..aa6bed529 100644
--- a/docs/resources/cdn_distribution.md
+++ b/docs/resources/cdn_distribution.md
@@ -40,7 +40,7 @@ resource "stackit_cdn_distribution" "example_distribution" {
 ### Read-Only
 
 - `created_at` (String) Time when the distribution was created
-- `distribution_id` (String) STACKIT project ID associated with the distribution
+- `distribution_id` (String) CDN distribution ID
 - `domains` (Attributes List) List of configured domains for the distribution (see [below for nested schema](#nestedatt--domains))
 - `errors` (List of String) List of distribution errors
 - `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`distribution_id`".
diff --git a/examples/data-sources/stackit_cdn_custom_domain/data-source.tf b/examples/data-sources/stackit_cdn_custom_domain/data-source.tf
new file mode 100644
index 000000000..23504bb6b
--- /dev/null
+++ b/examples/data-sources/stackit_cdn_custom_domain/data-source.tf
@@ -0,0 +1,6 @@
+data "stackit_cdn_custom_domain" "example" {
+  project_id      = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+  distribution_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+  name            = "https://xxx.xxx"
+}
+
diff --git a/examples/resources/stackit_cdn_custom_domain/resource.tf b/examples/resources/stackit_cdn_custom_domain/resource.tf
new file mode 100644
index 000000000..3ba9af21b
--- /dev/null
+++ b/examples/resources/stackit_cdn_custom_domain/resource.tf
@@ -0,0 +1,6 @@
+# Create a CDN custom domain
+resource "stackit_cdn_custom_domain" "example" {
+  project_id      = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+  distribution_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+  name            = "https://xxx.xxx"
+}
diff --git a/stackit/internal/services/cdn/cdn_acc_test.go b/stackit/internal/services/cdn/cdn_acc_test.go
index c7a6541a9..6d3b021e1 100644
--- a/stackit/internal/services/cdn/cdn_acc_test.go
+++ b/stackit/internal/services/cdn/cdn_acc_test.go
@@ -3,9 +3,12 @@ package cdn_test
 import (
 	"context"
 	"fmt"
+	"net"
 	"strings"
 	"testing"
+	"time"
 
+	"github.com/google/uuid"
 	"github.com/hashicorp/terraform-plugin-testing/helper/resource"
 	"github.com/hashicorp/terraform-plugin-testing/terraform"
 	"github.com/stackitcloud/stackit-sdk-go/core/config"
@@ -21,6 +24,7 @@ var instanceResource = map[string]string{
 	"config_backend_origin_url": "https://test-backend-1.cdn-dev.runs.onstackit.cloud",
 	"config_regions":            "\"EU\", \"US\"",
 	"config_regions_updated":    "\"EU\", \"US\", \"ASIA\"",
+	"custom_domain_prefix":      uuid.NewString(), // we use a different domain prefix each test run due to inconsistent upstream release of domains, which might impair consecutive test runs
 }
 
 func configResources(regions string) string {
@@ -37,7 +41,35 @@ func configResources(regions string) string {
 						regions = [%s]
 					}
 				}
-		`, testutil.CdnProviderConfig(), testutil.ProjectId, instanceResource["config_backend_origin_url"], regions)
+
+				resource "stackit_dns_zone" "dns_zone" {
+					project_id    = "%s"
+					name          = "cdn_acc_test_zone"
+					dns_name      = "cdntestzone.stackit.gg"
+					contact_email = "aa@bb.cc"
+					type          = "primary"
+					default_ttl   = 3600
+				}
+				resource "stackit_dns_record_set" "dns_record" {
+					project_id = "%s"
+					zone_id    = stackit_dns_zone.dns_zone.zone_id
+					name       = "%s"
+					type       = "CNAME"
+					records    = ["${stackit_cdn_distribution.distribution.domains[0].name}."]
+				}
+		`, testutil.CdnProviderConfig(), testutil.ProjectId, instanceResource["config_backend_origin_url"], regions, testutil.ProjectId, testutil.ProjectId, instanceResource["custom_domain_prefix"])
+}
+
+func configCustomDomainResources(regions string) string {
+	return fmt.Sprintf(`
+				%s
+
+		        resource "stackit_cdn_custom_domain" "custom_domain" {
+					project_id = stackit_cdn_distribution.distribution.project_id
+					distribution_id = stackit_cdn_distribution.distribution.distribution_id
+		            name = "${stackit_dns_record_set.dns_record.name}.cdntestzone.stackit.gg"
+				}
+`, configResources(regions))
 }
 
 func configDatasources(regions string) string {
@@ -48,7 +80,13 @@ func configDatasources(regions string) string {
 					project_id = stackit_cdn_distribution.distribution.project_id
 					distribution_id = stackit_cdn_distribution.distribution.distribution_id
 				}
-		`, configResources(regions))
+				
+				data "stackit_cdn_custom_domain" "custom_domain" {
+					project_id = stackit_cdn_custom_domain.custom_domain.project_id
+					distribution_id = stackit_cdn_custom_domain.custom_domain.distribution_id
+					name = stackit_cdn_custom_domain.custom_domain.name
+				}
+		`, configCustomDomainResources(regions))
 }
 
 func TestAccCDNDistributionResource(t *testing.T) {
@@ -56,7 +94,7 @@ func TestAccCDNDistributionResource(t *testing.T) {
 		ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
 		CheckDestroy:             testAccCheckCDNDistributionDestroy,
 		Steps: []resource.TestStep{
-			// Create
+			// Distribution Create
 			{
 				Config: configResources(instanceResource["config_regions"]),
 				Check: resource.ComposeAggregateTestCheckFunc(
@@ -65,6 +103,7 @@ func TestAccCDNDistributionResource(t *testing.T) {
 					resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "updated_at"),
 					resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.#", "1"),
 					resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "domains.0.name"),
+					resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.0.type", "managed"),
 					resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.0.status", "ACTIVE"),
 					resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.#", "2"),
 					resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.0", "EU"),
@@ -73,6 +112,24 @@ func TestAccCDNDistributionResource(t *testing.T) {
 					resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "status", "ACTIVE"),
 				),
 			},
+			// Wait step, that confirms the CNAME record has "propagated"
+			{
+				Config: configResources(instanceResource["config_regions"]),
+				Check: func(_ *terraform.State) error {
+					_, err := blockUntilDomainResolves(instanceResource["custom_domain_prefix"] + ".cdntestzone.stackit.gg")
+					return err
+				},
+			},
+			// Custom Domain Create
+			{
+				Config: configCustomDomainResources(instanceResource["config_regions"]),
+				Check: resource.ComposeAggregateTestCheckFunc(
+					resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "status", "ACTIVE"),
+					resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "name", instanceResource["custom_domain_prefix"]+".cdntestzone.stackit.gg"),
+					resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "distribution_id", "stackit_cdn_custom_domain.custom_domain", "distribution_id"),
+					resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "project_id", "stackit_cdn_custom_domain.custom_domain", "project_id"),
+				),
+			},
 			// Import
 			{
 				ResourceName: "stackit_cdn_distribution.distribution",
@@ -88,6 +145,28 @@ func TestAccCDNDistributionResource(t *testing.T) {
 
 					return fmt.Sprintf("%s,%s", testutil.ProjectId, distributionId), nil
 				},
+				ImportState:             true,
+				ImportStateVerify:       true,
+				ImportStateVerifyIgnore: []string{"domains"}, // we added a domain in the meantime...
+			},
+			{
+				ResourceName: "stackit_cdn_custom_domain.custom_domain",
+				ImportStateIdFunc: func(s *terraform.State) (string, error) {
+					r, ok := s.RootModule().Resources["stackit_cdn_custom_domain.custom_domain"]
+					if !ok {
+						return "", fmt.Errorf("couldn't find resource stackit_cdn_custom_domain.custom_domain")
+					}
+					distributionId, ok := r.Primary.Attributes["distribution_id"]
+					if !ok {
+						return "", fmt.Errorf("couldn't find attribute distribution_id")
+					}
+					name, ok := r.Primary.Attributes["name"]
+					if !ok {
+						return "", fmt.Errorf("couldn't find attribute name")
+					}
+
+					return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, distributionId, name), nil
+				},
 				ImportState:       true,
 				ImportStateVerify: true,
 			},
@@ -95,35 +174,50 @@ func TestAccCDNDistributionResource(t *testing.T) {
 			{
 				Config: configDatasources(instanceResource["config_regions"]),
 				Check: resource.ComposeAggregateTestCheckFunc(
-					resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "distribution_id"),
-					resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "created_at"),
-					resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "updated_at"),
-					resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.#", "1"),
-					resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "domains.0.name"),
-					resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.0.status", "ACTIVE"),
-					resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.#", "2"),
-					resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.0", "EU"),
-					resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.1", "US"),
-					resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId),
-					resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "status", "ACTIVE"),
+					resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.distribution", "distribution_id"),
+					resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.distribution", "created_at"),
+					resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.distribution", "updated_at"),
+					resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "domains.#", "2"),
+					resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.distribution", "domains.0.name"),
+					resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "domains.1.name", instanceResource["custom_domain_prefix"]+".cdntestzone.stackit.gg"),
+					resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "domains.0.status", "ACTIVE"),
+					resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "domains.1.status", "ACTIVE"),
+					resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "domains.0.type", "managed"),
+					resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "domains.1.type", "custom"),
+					resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.regions.#", "2"),
+					resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.regions.0", "EU"),
+					resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.regions.1", "US"),
+					resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId),
+					resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "status", "ACTIVE"),
+					resource.TestCheckResourceAttr("data.stackit_cdn_custom_domain.custom_domain", "status", "ACTIVE"),
+					resource.TestCheckResourceAttr("data.stackit_cdn_custom_domain.custom_domain", "name", instanceResource["custom_domain_prefix"]+".cdntestzone.stackit.gg"),
+					resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "distribution_id", "stackit_cdn_custom_domain.custom_domain", "distribution_id"),
 				),
 			},
 			// Update
 			{
-				Config: configResources(instanceResource["config_regions_updated"]),
+				Config: configCustomDomainResources(instanceResource["config_regions_updated"]),
 				Check: resource.ComposeAggregateTestCheckFunc(
 					resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "distribution_id"),
 					resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "created_at"),
 					resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "updated_at"),
-					resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.#", "1"),
+					resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.#", "2"),
 					resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "domains.0.name"),
+					resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.1.name", instanceResource["custom_domain_prefix"]+".cdntestzone.stackit.gg"),
 					resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.0.status", "ACTIVE"),
+					resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.1.status", "ACTIVE"),
+					resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.0.type", "managed"),
+					resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.1.type", "custom"),
 					resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.#", "3"),
 					resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.0", "EU"),
 					resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.1", "US"),
 					resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.2", "ASIA"),
 					resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId),
 					resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "status", "ACTIVE"),
+					resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "status", "ACTIVE"),
+					resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "name", instanceResource["custom_domain_prefix"]+".cdntestzone.stackit.gg"),
+					resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "distribution_id", "stackit_cdn_custom_domain.custom_domain", "distribution_id"),
+					resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "project_id", "stackit_cdn_custom_domain.custom_domain", "project_id"),
 				),
 			},
 		},
@@ -165,3 +259,39 @@ func testAccCheckCDNDistributionDestroy(s *terraform.State) error {
 	}
 	return nil
 }
+
+const (
+	recordCheckInterval time.Duration = 3 * time.Second
+	recordCheckAttempts               = 100 // wait up to 5 minutes for record to be come available (normally takes less than 2 minutes)
+)
+
+func blockUntilDomainResolves(domain string) (net.IP, error) {
+	// wait until it becomes ready
+	isReady := func() (net.IP, error) {
+		ips, err := net.LookupIP(domain)
+		if err != nil {
+			return nil, fmt.Errorf("error looking up IP for domain %s: %w", domain, err)
+		}
+		for _, ip := range ips {
+			if ip.String() != "<nil>" {
+				return ip, nil
+			}
+		}
+		return nil, fmt.Errorf("no IP for domain: %v", domain)
+	}
+	return retry(recordCheckAttempts, recordCheckInterval, isReady)
+}
+
+func retry[T any](attempts int, sleep time.Duration, f func() (T, error)) (T, error) {
+	var zero T
+	var errOuter error
+	for i := 0; i < attempts; i++ {
+		dist, err := f()
+		if err == nil {
+			return dist, nil
+		}
+		errOuter = err
+		time.Sleep(sleep)
+	}
+	return zero, fmt.Errorf("retry timed out, last error: %w", errOuter)
+}
diff --git a/stackit/internal/services/cdn/customdomain/datasource.go b/stackit/internal/services/cdn/customdomain/datasource.go
new file mode 100644
index 000000000..7f08fa58a
--- /dev/null
+++ b/stackit/internal/services/cdn/customdomain/datasource.go
@@ -0,0 +1,150 @@
+package cdn
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/hashicorp/terraform-plugin-framework/datasource"
+	"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+	"github.com/hashicorp/terraform-plugin-log/tflog"
+	"github.com/stackitcloud/stackit-sdk-go/core/config"
+	"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
+	"github.com/stackitcloud/stackit-sdk-go/services/cdn"
+	"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
+	"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
+	"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+	_ datasource.DataSource              = &customDomainDataSource{}
+	_ datasource.DataSourceWithConfigure = &customDomainDataSource{}
+)
+
+type customDomainDataSource struct {
+	client *cdn.APIClient
+}
+
+func NewCustomDomainDataSource() datasource.DataSource {
+	return &customDomainDataSource{}
+}
+
+func (d *customDomainDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+	if req.ProviderData == nil {
+		return
+	}
+	providerData, ok := req.ProviderData.(core.ProviderData)
+	if !ok {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
+		return
+	}
+	features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_cdn_custom_domain", "datasource")
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	var apiClient *cdn.APIClient
+	var err error
+	if providerData.CdnCustomEndpoint != "" {
+		ctx = tflog.SetField(ctx, "cdn_custom_endpoint", providerData.CdnCustomEndpoint)
+		apiClient, err = cdn.NewAPIClient(
+			config.WithCustomAuth(providerData.RoundTripper),
+			config.WithEndpoint(providerData.CdnCustomEndpoint),
+		)
+	} else {
+		apiClient, err = cdn.NewAPIClient(
+			config.WithCustomAuth(providerData.RoundTripper),
+		)
+	}
+	if err != nil {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err))
+		return
+	}
+	d.client = apiClient
+	tflog.Info(ctx, "CDN client configured")
+}
+
+func (r *customDomainDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+	resp.TypeName = req.ProviderTypeName + "_cdn_custom_domain"
+}
+
+func (r *customDomainDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		MarkdownDescription: features.AddBetaDescription("CDN distribution data source schema."),
+		Description:         "CDN distribution data source schema.",
+		Attributes: map[string]schema.Attribute{
+			"id": schema.StringAttribute{
+				Description: customDomainSchemaDescriptions["id"],
+				Computed:    true,
+			},
+			"name": schema.StringAttribute{
+				Description: customDomainSchemaDescriptions["name"],
+				Required:    true,
+			},
+			"distribution_id": schema.StringAttribute{
+				Description: customDomainSchemaDescriptions["distribution_id"],
+				Required:    true,
+				Validators:  []validator.String{validate.UUID()},
+			},
+			"project_id": schema.StringAttribute{
+				Description: customDomainSchemaDescriptions["project_id"],
+				Required:    true,
+			},
+			"status": schema.StringAttribute{
+				Computed:    true,
+				Description: customDomainSchemaDescriptions["status"],
+			},
+			"errors": schema.ListAttribute{
+				ElementType: types.StringType,
+				Computed:    true,
+				Description: customDomainSchemaDescriptions["errors"],
+			},
+		},
+	}
+}
+
+func (r *customDomainDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
+	var model CustomDomainModel
+	diags := req.Config.Get(ctx, &model)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	projectId := model.ProjectId.ValueString()
+	ctx = tflog.SetField(ctx, "project_id", projectId)
+	distributionId := model.DistributionId.ValueString()
+	ctx = tflog.SetField(ctx, "distribution_id", distributionId)
+	name := model.Name.ValueString()
+	ctx = tflog.SetField(ctx, "name", name)
+
+	customDomainResp, err := r.client.GetCustomDomain(ctx, projectId, distributionId, name).Execute()
+	if err != nil {
+		var oapiErr *oapierror.GenericOpenAPIError
+		// n.b. err is caught here if of type *oapierror.GenericOpenAPIError, which the stackit SDK client returns
+		if errors.As(err, &oapiErr) {
+			if oapiErr.StatusCode == http.StatusNotFound {
+				resp.State.RemoveResource(ctx)
+				return
+			}
+		}
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN custom domain", fmt.Sprintf("Calling API: %v", err))
+		return
+	}
+	err = mapCustomDomainFields(customDomainResp.CustomDomain, &model)
+	if err != nil {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN custom domain", fmt.Sprintf("Processing API payload: %v", err))
+		return
+	}
+	// Set refreshed state
+	diags = resp.State.Set(ctx, model)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+	tflog.Info(ctx, "CDN custom domain read")
+}
diff --git a/stackit/internal/services/cdn/customdomain/resource.go b/stackit/internal/services/cdn/customdomain/resource.go
new file mode 100644
index 000000000..402b5f778
--- /dev/null
+++ b/stackit/internal/services/cdn/customdomain/resource.go
@@ -0,0 +1,310 @@
+package cdn
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/google/uuid"
+	"github.com/hashicorp/terraform-plugin-framework/attr"
+	"github.com/hashicorp/terraform-plugin-framework/path"
+	"github.com/hashicorp/terraform-plugin-framework/resource"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+	"github.com/hashicorp/terraform-plugin-log/tflog"
+	"github.com/stackitcloud/stackit-sdk-go/core/config"
+	"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
+	"github.com/stackitcloud/stackit-sdk-go/services/cdn"
+	"github.com/stackitcloud/stackit-sdk-go/services/cdn/wait"
+	"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
+	"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
+	"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+	_ resource.Resource                = &customDomainResource{}
+	_ resource.ResourceWithConfigure   = &customDomainResource{}
+	_ resource.ResourceWithImportState = &customDomainResource{}
+)
+
+var customDomainSchemaDescriptions = map[string]string{
+	"id":              "Terraform's internal resource identifier. It is structured as \"`project_id`,`distribution_id`\".",
+	"distribution_id": "CDN distribution ID",
+	"project_id":      "STACKIT project ID associated with the distribution",
+	"status":          "Status of the distribution",
+	"errors":          "List of distribution errors",
+}
+
+type CustomDomainModel struct {
+	ID             types.String `tfsdk:"id"`              // Required by Terraform
+	DistributionId types.String `tfsdk:"distribution_id"` // DistributionID associated with the cdn distribution
+	ProjectId      types.String `tfsdk:"project_id"`      // ProjectId associated with the cdn distribution
+	Name           types.String `tfsdk:"name"`            // The custom domain
+	Status         types.String `tfsdk:"status"`          // The status of the cdn distribution
+	Errors         types.List   `tfsdk:"errors"`          // Any errors that the distribution has
+}
+
+type customDomainResource struct {
+	client       *cdn.APIClient
+	providerData core.ProviderData
+}
+
+func NewCustomDomainResource() resource.Resource {
+	return &customDomainResource{}
+}
+
+func (r *customDomainResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+	if req.ProviderData == nil {
+		return
+	}
+	var ok bool
+	if r.providerData, ok = req.ProviderData.(core.ProviderData); !ok {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
+		return
+	}
+	features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_cdn_custom_domain", "resource")
+
+	var apiClient *cdn.APIClient
+	var err error
+	if r.providerData.CdnCustomEndpoint != "" {
+		ctx = tflog.SetField(ctx, "cdn_custom_endpoint", r.providerData.CdnCustomEndpoint)
+		apiClient, err = cdn.NewAPIClient(
+			config.WithCustomAuth(r.providerData.RoundTripper),
+			config.WithEndpoint(r.providerData.CdnCustomEndpoint),
+		)
+	} else {
+		apiClient, err = cdn.NewAPIClient(
+			config.WithCustomAuth(r.providerData.RoundTripper),
+		)
+	}
+	if err != nil {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err))
+		return
+	}
+	r.client = apiClient
+	tflog.Info(ctx, "CDN client configured")
+}
+
+func (r *customDomainResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+	resp.TypeName = req.ProviderTypeName + "_cdn_custom_domain"
+}
+
+func (r *customDomainResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		MarkdownDescription: features.AddBetaDescription("CDN distribution data source schema."),
+		Description:         "CDN distribution data source schema.",
+		Attributes: map[string]schema.Attribute{
+			"id": schema.StringAttribute{
+				Description: customDomainSchemaDescriptions["id"],
+				Computed:    true,
+			},
+			"name": schema.StringAttribute{
+				Description: customDomainSchemaDescriptions["name"],
+				Required:    true,
+				Optional:    false,
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.UseStateForUnknown(),
+				},
+			},
+			"distribution_id": schema.StringAttribute{
+				Description: customDomainSchemaDescriptions["distribution_id"],
+				Required:    true,
+				Optional:    false,
+				Validators:  []validator.String{validate.UUID()},
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.RequiresReplace(),
+				},
+			},
+			"project_id": schema.StringAttribute{
+				Description: customDomainSchemaDescriptions["project_id"],
+				Required:    true,
+				Optional:    false,
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.RequiresReplace(),
+				},
+			},
+			"status": schema.StringAttribute{
+				Computed:    true,
+				Description: customDomainSchemaDescriptions["status"],
+			},
+			"errors": schema.ListAttribute{
+				ElementType: types.StringType,
+				Computed:    true,
+				Description: customDomainSchemaDescriptions["errors"],
+			},
+		},
+	}
+}
+
+func (r *customDomainResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
+	var model CustomDomainModel
+	diags := req.Plan.Get(ctx, &model)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+	projectId := model.ProjectId.ValueString()
+	ctx = tflog.SetField(ctx, "project_id", projectId)
+	distributionId := model.DistributionId.ValueString()
+	ctx = tflog.SetField(ctx, "distribution_id", distributionId)
+	name := model.Name.ValueString()
+	ctx = tflog.SetField(ctx, "name", name)
+
+	payload := cdn.PutCustomDomainPayload{IntentId: cdn.PtrString(uuid.NewString())}
+
+	_, err := r.client.PutCustomDomain(ctx, projectId, distributionId, name).PutCustomDomainPayload(payload).Execute()
+	if err != nil {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN custom domain", fmt.Sprintf("Calling API: %v", err))
+		return
+	}
+	waitResp, err := wait.CreateCDNCustomDomainWaitHandler(ctx, r.client, projectId, distributionId, name).SetTimeout(5 * time.Minute).WaitWithContext(ctx)
+	if err != nil {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN custom domain", fmt.Sprintf("Waiting for create: %v", err))
+		return
+	}
+
+	err = mapCustomDomainFields(waitResp, &model)
+	if err != nil {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN custom domain", fmt.Sprintf("Processing API payload: %v", err))
+		return
+	}
+
+	diags = resp.State.Set(ctx, model)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+	tflog.Info(ctx, "CDN custom domain created")
+}
+
+func (r *customDomainResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
+	var model CustomDomainModel
+	diags := req.State.Get(ctx, &model)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	projectId := model.ProjectId.ValueString()
+	ctx = tflog.SetField(ctx, "project_id", projectId)
+	distributionId := model.DistributionId.ValueString()
+	ctx = tflog.SetField(ctx, "distribution_id", distributionId)
+	name := model.Name.ValueString()
+	ctx = tflog.SetField(ctx, "name", name)
+
+	customDomainResp, err := r.client.GetCustomDomain(ctx, projectId, distributionId, name).Execute()
+	if err != nil {
+		var oapiErr *oapierror.GenericOpenAPIError
+		// n.b. err is caught here if of type *oapierror.GenericOpenAPIError, which the stackit SDK client returns
+		if errors.As(err, &oapiErr) {
+			if oapiErr.StatusCode == http.StatusNotFound {
+				resp.State.RemoveResource(ctx)
+				return
+			}
+		}
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN custom domain", fmt.Sprintf("Calling API: %v", err))
+		return
+	}
+	err = mapCustomDomainFields(customDomainResp.CustomDomain, &model)
+	if err != nil {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN custom domain", fmt.Sprintf("Processing API payload: %v", err))
+		return
+	}
+	// Set refreshed state
+	diags = resp.State.Set(ctx, model)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+	tflog.Info(ctx, "CDN custom domain read")
+}
+
+func (r *customDomainResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
+	// Update shouldn't be called; custom domains have only computed fields and fields that require replacement when changed
+	core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating CDN custom domain", "Custom domain cannot be updated")
+}
+
+func (r *customDomainResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform
+	var model CustomDomainModel
+	diags := req.State.Get(ctx, &model)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	projectId := model.ProjectId.ValueString()
+	ctx = tflog.SetField(ctx, "project_id", projectId)
+	distributionId := model.DistributionId.ValueString()
+	ctx = tflog.SetField(ctx, "distribution_id", distributionId)
+	name := model.Name.ValueString()
+	ctx = tflog.SetField(ctx, "name", name)
+
+	_, err := r.client.DeleteCustomDomain(ctx, projectId, distributionId, name).Execute()
+	if err != nil {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Delete CDN custom domain", fmt.Sprintf("Delete custom domain: %v", err))
+	}
+	_, err = wait.DeleteCDNCustomDomainWaitHandler(ctx, r.client, projectId, distributionId, name).WaitWithContext(ctx)
+	if err != nil {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Delete CDN custom domain", fmt.Sprintf("Waiting for deletion: %v", err))
+		return
+	}
+	tflog.Info(ctx, "CDN custom domain deleted")
+}
+
+func (r *customDomainResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+	idParts := strings.Split(req.ID, core.Separator)
+
+	if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing CDN custom domain", fmt.Sprintf("Expected import identifier on the format: [project_id]%q[distribution_id]%q[custom_domain_name], got %q", core.Separator, core.Separator, req.ID))
+	}
+	resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
+	resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("distribution_id"), idParts[1])...)
+	resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[2])...)
+	tflog.Info(ctx, "CDN custom domain state imported")
+}
+
+func mapCustomDomainFields(customDomain *cdn.CustomDomain, model *CustomDomainModel) error {
+	if customDomain == nil {
+		return fmt.Errorf("response input is nil")
+	}
+	if model == nil {
+		return fmt.Errorf("model input is nil")
+	}
+
+	if customDomain.Name == nil {
+		return fmt.Errorf("Name is missing in response")
+	}
+
+	if customDomain.Status == nil {
+		return fmt.Errorf("Status missing in response")
+	}
+
+	id := model.ProjectId.ValueString() + core.Separator + model.DistributionId.ValueString() + core.Separator + *customDomain.Name
+
+	model.ID = types.StringValue(id)
+	model.Status = types.StringValue(string(*customDomain.Status))
+
+	customDomainErrors := []attr.Value{}
+	if customDomain.Errors != nil {
+		for _, e := range *customDomain.Errors {
+			if e.En == nil {
+				return fmt.Errorf("Error description missing")
+			}
+			customDomainErrors = append(customDomainErrors, types.StringValue(*e.En))
+		}
+	}
+	modelErrors, diags := types.ListValue(types.StringType, customDomainErrors)
+	if diags.HasError() {
+		return core.DiagsToError(diags)
+	}
+	model.Errors = modelErrors
+
+	return nil
+}
diff --git a/stackit/internal/services/cdn/customdomain/resource_test.go b/stackit/internal/services/cdn/customdomain/resource_test.go
new file mode 100644
index 000000000..1d72ea119
--- /dev/null
+++ b/stackit/internal/services/cdn/customdomain/resource_test.go
@@ -0,0 +1,90 @@
+package cdn
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/hashicorp/terraform-plugin-framework/attr"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+	"github.com/stackitcloud/stackit-sdk-go/services/cdn"
+)
+
+func TestMapFields(t *testing.T) {
+	emtpyErrorsList := types.ListValueMust(types.StringType, []attr.Value{})
+	expectedModel := func(mods ...func(*CustomDomainModel)) *CustomDomainModel {
+		model := &CustomDomainModel{
+			ID:             types.StringValue("test-project-id,test-distribution-id,https://testdomain.com"),
+			DistributionId: types.StringValue("test-distribution-id"),
+			ProjectId:      types.StringValue("test-project-id"),
+			Status:         types.StringValue("ACTIVE"),
+			Errors:         emtpyErrorsList,
+		}
+		for _, mod := range mods {
+			mod(model)
+		}
+		return model
+	}
+	customDomainFixture := func(mods ...func(*cdn.CustomDomain)) *cdn.CustomDomain {
+		distribution := &cdn.CustomDomain{
+			Errors: &[]cdn.StatusError{},
+			Name:   cdn.PtrString("https://testdomain.com"),
+			Status: cdn.DOMAINSTATUS_ACTIVE.Ptr(),
+		}
+		for _, mod := range mods {
+			mod(distribution)
+		}
+		return distribution
+	}
+	tests := map[string]struct {
+		Input    *cdn.CustomDomain
+		Expected *CustomDomainModel
+		IsValid  bool
+	}{
+		"happy_path": {
+			Expected: expectedModel(),
+			Input:    customDomainFixture(),
+			IsValid:  true,
+		},
+		"happy_path_status_error": {
+			Expected: expectedModel(func(m *CustomDomainModel) {
+				m.Status = types.StringValue("ERROR")
+			}),
+			Input: customDomainFixture(func(d *cdn.CustomDomain) {
+				d.Status = cdn.DOMAINSTATUS_ERROR.Ptr()
+			}),
+			IsValid: true,
+		},
+		"sad_path_custom_domain_nil": {
+			Expected: expectedModel(),
+			Input:    nil,
+			IsValid:  false,
+		},
+		"sad_path_name_missing": {
+			Expected: expectedModel(),
+			Input: customDomainFixture(func(d *cdn.CustomDomain) {
+				d.Name = nil
+			}),
+			IsValid: false,
+		},
+	}
+	for tn, tc := range tests {
+		t.Run(tn, func(t *testing.T) {
+			model := &CustomDomainModel{}
+			model.DistributionId = tc.Expected.DistributionId
+			model.ProjectId = tc.Expected.ProjectId
+			err := mapCustomDomainFields(tc.Input, model)
+			if err != nil && tc.IsValid {
+				t.Fatalf("Error mapping fields: %v", err)
+			}
+			if err == nil && !tc.IsValid {
+				t.Fatalf("Should have failed")
+			}
+			if tc.IsValid {
+				diff := cmp.Diff(model, tc.Expected)
+				if diff != "" {
+					t.Fatalf("Create Payload not as expected: %s", diff)
+				}
+			}
+		})
+	}
+}
diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go
index b4125c1ab..e7332f63c 100644
--- a/stackit/internal/services/cdn/distribution/resource.go
+++ b/stackit/internal/services/cdn/distribution/resource.go
@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"net/http"
 	"strings"
+	"time"
 
 	"github.com/google/uuid"
 	"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
@@ -154,7 +155,7 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques
 				Computed:    true,
 			},
 			"distribution_id": schema.StringAttribute{
-				Description: schemaDescriptions["project_id"],
+				Description: schemaDescriptions["distribution_id"],
 				Computed:    true,
 				Validators:  []validator.String{validate.UUID()},
 				PlanModifiers: []planmodifier.String{
@@ -266,7 +267,7 @@ func (r *distributionResource) Create(ctx context.Context, req resource.CreateRe
 		core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN distribution", fmt.Sprintf("Calling API: %v", err))
 		return
 	}
-	waitResp, err := wait.CreateDistributionPoolWaitHandler(ctx, r.client, projectId, *createResp.Distribution.Id).WaitWithContext(ctx)
+	waitResp, err := wait.CreateDistributionPoolWaitHandler(ctx, r.client, projectId, *createResp.Distribution.Id).SetTimeout(5 * time.Minute).WaitWithContext(ctx)
 	if err != nil {
 		core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN distribution", fmt.Sprintf("Waiting for create: %v", err))
 		return
diff --git a/stackit/provider.go b/stackit/provider.go
index 4502199d0..e39586f33 100644
--- a/stackit/provider.go
+++ b/stackit/provider.go
@@ -14,6 +14,7 @@ import (
 	"github.com/hashicorp/terraform-plugin-framework/types"
 	"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
 	roleAssignements "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/authorization/roleassignments"
+	cdnCustomDomain "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/customdomain"
 	cdn "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/distribution"
 	dnsRecordSet "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/recordset"
 	dnsZone "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/zone"
@@ -476,6 +477,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource
 	return []func() datasource.DataSource{
 		alertGroup.NewAlertGroupDataSource,
 		cdn.NewDistributionDataSource,
+		cdnCustomDomain.NewCustomDomainDataSource,
 		dnsZone.NewZoneDataSource,
 		dnsRecordSet.NewRecordSetDataSource,
 		iaasAffinityGroup.NewAffinityGroupDatasource,
@@ -532,6 +534,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource {
 	resources := []func() resource.Resource{
 		alertGroup.NewAlertGroupResource,
 		cdn.NewDistributionResource,
+		cdnCustomDomain.NewCustomDomainResource,
 		dnsZone.NewZoneResource,
 		dnsRecordSet.NewRecordSetResource,
 		iaasAffinityGroup.NewAffinityGroupResource,
diff --git a/templates/guides/stackit_cdn_with_custom_domain.md.tmpl b/templates/guides/stackit_cdn_with_custom_domain.md.tmpl
new file mode 100644
index 000000000..78d9cb06e
--- /dev/null
+++ b/templates/guides/stackit_cdn_with_custom_domain.md.tmpl
@@ -0,0 +1,60 @@
+---
+page_title: "Using STACKIT CDN with your own domain"
+---
+# Using STACKIT CDN with your own domain
+
+## Overview
+
+This guide outlines the process of creating a STACKIT CDN distribution and configuring it to make use of an existing domain using STACKIT DNS.
+
+## Steps
+
+1. **Create a STACKIT CDN and DNS Zone**
+    
+    Create the CDN distribution and the DNS zone.
+    
+    ```terraform
+    resource "stackit_cdn_distribution" "example_distribution" {
+      project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+      config = {
+        backend = {
+          type       = "http"
+          origin_url = "mybackend.onstackit.cloud"
+        }
+        regions = ["EU", "US", "ASIA", "AF", "SA"]
+      }
+    }
+    
+    resource "stackit_dns_zone" "example_zone" {
+      project_id    = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+      name          = "My DNS zone"
+      dns_name      = "myapp.runs.onstackit.cloud"
+      contact_email = "aa@bb.ccc"
+      type          = "primary"
+    }
+    ```
+    
+2. **Add CNAME record to your DNS zone**
+
+    If you want to redirect your entire domain to the CDN, you can instead use an A record.
+    ```terraform
+    resource "stackit_dns_record_set" "example" {
+      project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+      zone_id    = stackit_dns_zone.example_zone.zone_id
+      name       = "cdn"
+      type       = "CNAME"
+      records    = ["${stackit_cdn_distribution.domains[0].name}."]
+    }
+    ```
+    
+3. **Create a STACKIT CDN Custom Domain**
+    ```terraform
+    # Create a CDN custom domain
+    resource "stackit_cdn_custom_domain" "example" {
+      project_id      = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+      distribution_id = stackit_cdn_distribution.example_distribution.distribution_id
+      name            = "${stackit_dns_record_set.example.name}.${stackit_dns_zone.dns_name}"
+    }
+    ```
+    
+    Now, you can access your content on the url `cdn.myapp.runs.onstackit.cloud`.


Reviews:
user: login: bahkauv70
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T09:42:05Z
user: login: bahkauv70
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T09:45:07Z
user: login: bahkauv70
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T09:46:55Z
user: login: malteehrlen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T09:53:20Z
user: login: malteehrlen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T09:54:06Z
user: login: malteehrlen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T09:54:21Z
user: login: bahkauv70
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T11:10:36Z

Review comments:
diff_hunk: @@ -0,0 +1,307 @@
+package cdn
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/google/uuid"
+	"github.com/hashicorp/terraform-plugin-framework/attr"
+	"github.com/hashicorp/terraform-plugin-framework/path"
+	"github.com/hashicorp/terraform-plugin-framework/resource"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+	"github.com/hashicorp/terraform-plugin-log/tflog"
+	"github.com/stackitcloud/stackit-sdk-go/core/config"
+	"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
+	"github.com/stackitcloud/stackit-sdk-go/services/cdn"
+	"github.com/stackitcloud/stackit-sdk-go/services/cdn/wait"
+	"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
+	"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
+	"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+	_ resource.Resource                = &customDomainResource{}
+	_ resource.ResourceWithConfigure   = &customDomainResource{}
+	_ resource.ResourceWithImportState = &customDomainResource{}
+)
+
+var customDomainSchemaDescriptions = map[string]string{
+	"id":              "Terraform's internal resource identifier. It is structured as \"`project_id`,`distribution_id`\".",
+	"distribution_id": "CDN distribution ID",
+	"project_id":      "STACKIT project ID associated with the distribution",
+	"status":          "Status of the distribution",
+	"errors":          "List of distribution errors",
+}
+
+type CustomDomainModel struct {
+	ID             types.String `tfsdk:"id"`              // Required by Terraform
+	DistributionId types.String `tfsdk:"distribution_id"` // DistributionID associated with the cdn distribution
+	ProjectId      types.String `tfsdk:"project_id"`      // ProjectId associated with the cdn distribution
+	Name           types.String `tfsdk:"name"`            // The custom domain
+	Status         types.String `tfsdk:"status"`          // The status of the cdn distribution
+	Errors         types.List   `tfsdk:"errors"`          // Any errors that the distribution has
+}
+
+type customDomainResource struct {
+	client       *cdn.APIClient
+	providerData core.ProviderData
+}
+
+func NewCustomDomainResource() resource.Resource {
+	return &customDomainResource{}
+}
+
+func (r *customDomainResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+	if req.ProviderData == nil {
+		return
+	}
+	var ok bool
+	if r.providerData, ok = req.ProviderData.(core.ProviderData); !ok {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
+		return
+	}
+	features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_cdn_custom_domain", "resource")
+
+	var apiClient *cdn.APIClient
+	var err error
+	if r.providerData.CdnCustomEndpoint != "" {
+		ctx = tflog.SetField(ctx, "cdn_custom_endpoint", r.providerData.CdnCustomEndpoint)
+		apiClient, err = cdn.NewAPIClient(
+			config.WithCustomAuth(r.providerData.RoundTripper),
+			config.WithEndpoint(r.providerData.CdnCustomEndpoint),
+		)
+	} else {
+		apiClient, err = cdn.NewAPIClient(
+			config.WithCustomAuth(r.providerData.RoundTripper),
+		)
+	}
+	if err != nil {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err))
+		return
+	}
+	r.client = apiClient
+	tflog.Info(ctx, "CDN client configured")
+}
+
+func (r *customDomainResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+	resp.TypeName = req.ProviderTypeName + "_cdn_custom_domain"
+}
+
+func (r *customDomainResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		MarkdownDescription: features.AddBetaDescription("CDN distribution data source schema."),
+		Description:         "CDN distribution data source schema.",
+		Attributes: map[string]schema.Attribute{
+			"id": schema.StringAttribute{
+				Description: customDomainSchemaDescriptions["id"],
+				Computed:    true,
+			},
+			"name": schema.StringAttribute{
+				Description: customDomainSchemaDescriptions["name"],
+				Required:    true,
+				Optional:    false,
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.UseStateForUnknown(),
+				},
+			},
+			"distribution_id": schema.StringAttribute{
+				Description: customDomainSchemaDescriptions["distribution_id"],
+				Required:    true,
+				Optional:    false,
+				Validators:  []validator.String{validate.UUID()},
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.RequiresReplace(),
+				},
+			},
+			"project_id": schema.StringAttribute{
+				Description: customDomainSchemaDescriptions["project_id"],
+				Required:    true,
+				Optional:    false,
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.RequiresReplace(),
+				},
+			},
+			"status": schema.StringAttribute{
+				Computed:    true,
+				Description: customDomainSchemaDescriptions["status"],
+			},
+			"errors": schema.ListAttribute{
+				ElementType: types.StringType,
+				Computed:    true,
+				Description: customDomainSchemaDescriptions["errors"],
+			},
+		},
+	}
+}
+
+func (r *customDomainResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
+	var model CustomDomainModel
+	diags := req.Plan.Get(ctx, &model)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+	projectId := model.ProjectId.ValueString()
+	ctx = tflog.SetField(ctx, "project_id", projectId)
+	distributionId := model.DistributionId.ValueString()
+	ctx = tflog.SetField(ctx, "distribution_id", distributionId)
+	name := model.Name.ValueString()
+	ctx = tflog.SetField(ctx, "name", name)
+
+	payload := &cdn.PutCustomDomainPayload{IntentId: cdn.PtrString(uuid.NewString())}
+
+	_, err := r.client.PutCustomDomain(ctx, projectId, distributionId, name).PutCustomDomainPayload(*payload).Execute()
+	if err != nil {
path: stackit/internal/services/cdn/customdomain/resource.go
user: login: bahkauv70
body: The payload can be declared as a value

```suggestion
	payload := cdn.PutCustomDomainPayload{IntentId: cdn.PtrString(uuid.NewString())}

	_, err := r.client.PutCustomDomain(ctx, projectId, distributionId, name).PutCustomDomainPayload(payload).Execute()
	if err != nil {
```
created_at: 2025-05-05T09:42:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,307 @@
+package cdn
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/google/uuid"
+	"github.com/hashicorp/terraform-plugin-framework/attr"
+	"github.com/hashicorp/terraform-plugin-framework/path"
+	"github.com/hashicorp/terraform-plugin-framework/resource"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+	"github.com/hashicorp/terraform-plugin-log/tflog"
+	"github.com/stackitcloud/stackit-sdk-go/core/config"
+	"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
+	"github.com/stackitcloud/stackit-sdk-go/services/cdn"
+	"github.com/stackitcloud/stackit-sdk-go/services/cdn/wait"
+	"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
+	"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
+	"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+	_ resource.Resource                = &customDomainResource{}
+	_ resource.ResourceWithConfigure   = &customDomainResource{}
+	_ resource.ResourceWithImportState = &customDomainResource{}
+)
+
+var customDomainSchemaDescriptions = map[string]string{
+	"id":              "Terraform's internal resource identifier. It is structured as \"`project_id`,`distribution_id`\".",
+	"distribution_id": "CDN distribution ID",
+	"project_id":      "STACKIT project ID associated with the distribution",
+	"status":          "Status of the distribution",
+	"errors":          "List of distribution errors",
+}
+
+type CustomDomainModel struct {
+	ID             types.String `tfsdk:"id"`              // Required by Terraform
+	DistributionId types.String `tfsdk:"distribution_id"` // DistributionID associated with the cdn distribution
+	ProjectId      types.String `tfsdk:"project_id"`      // ProjectId associated with the cdn distribution
+	Name           types.String `tfsdk:"name"`            // The custom domain
+	Status         types.String `tfsdk:"status"`          // The status of the cdn distribution
+	Errors         types.List   `tfsdk:"errors"`          // Any errors that the distribution has
+}
+
+type customDomainResource struct {
+	client       *cdn.APIClient
+	providerData core.ProviderData
+}
+
+func NewCustomDomainResource() resource.Resource {
+	return &customDomainResource{}
+}
+
+func (r *customDomainResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+	if req.ProviderData == nil {
+		return
+	}
+	var ok bool
+	if r.providerData, ok = req.ProviderData.(core.ProviderData); !ok {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
+		return
+	}
+	features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_cdn_custom_domain", "resource")
+
+	var apiClient *cdn.APIClient
+	var err error
+	if r.providerData.CdnCustomEndpoint != "" {
+		ctx = tflog.SetField(ctx, "cdn_custom_endpoint", r.providerData.CdnCustomEndpoint)
+		apiClient, err = cdn.NewAPIClient(
+			config.WithCustomAuth(r.providerData.RoundTripper),
+			config.WithEndpoint(r.providerData.CdnCustomEndpoint),
+		)
+	} else {
+		apiClient, err = cdn.NewAPIClient(
+			config.WithCustomAuth(r.providerData.RoundTripper),
+		)
+	}
+	if err != nil {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err))
+		return
+	}
+	r.client = apiClient
+	tflog.Info(ctx, "CDN client configured")
+}
+
+func (r *customDomainResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+	resp.TypeName = req.ProviderTypeName + "_cdn_custom_domain"
+}
+
+func (r *customDomainResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		MarkdownDescription: features.AddBetaDescription("CDN distribution data source schema."),
+		Description:         "CDN distribution data source schema.",
+		Attributes: map[string]schema.Attribute{
+			"id": schema.StringAttribute{
+				Description: customDomainSchemaDescriptions["id"],
+				Computed:    true,
+			},
+			"name": schema.StringAttribute{
+				Description: customDomainSchemaDescriptions["name"],
+				Required:    true,
+				Optional:    false,
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.UseStateForUnknown(),
+				},
+			},
+			"distribution_id": schema.StringAttribute{
+				Description: customDomainSchemaDescriptions["distribution_id"],
+				Required:    true,
+				Optional:    false,
+				Validators:  []validator.String{validate.UUID()},
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.RequiresReplace(),
+				},
+			},
+			"project_id": schema.StringAttribute{
+				Description: customDomainSchemaDescriptions["project_id"],
+				Required:    true,
+				Optional:    false,
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.RequiresReplace(),
+				},
+			},
+			"status": schema.StringAttribute{
+				Computed:    true,
+				Description: customDomainSchemaDescriptions["status"],
+			},
+			"errors": schema.ListAttribute{
+				ElementType: types.StringType,
+				Computed:    true,
+				Description: customDomainSchemaDescriptions["errors"],
+			},
+		},
+	}
+}
+
+func (r *customDomainResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
+	var model CustomDomainModel
+	diags := req.Plan.Get(ctx, &model)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+	projectId := model.ProjectId.ValueString()
+	ctx = tflog.SetField(ctx, "project_id", projectId)
+	distributionId := model.DistributionId.ValueString()
+	ctx = tflog.SetField(ctx, "distribution_id", distributionId)
+	name := model.Name.ValueString()
+	ctx = tflog.SetField(ctx, "name", name)
+
+	payload := &cdn.PutCustomDomainPayload{IntentId: cdn.PtrString(uuid.NewString())}
+
+	_, err := r.client.PutCustomDomain(ctx, projectId, distributionId, name).PutCustomDomainPayload(*payload).Execute()
+	if err != nil {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN custom domain", fmt.Sprintf("Calling API: %v", err))
+		return
+	}
+	waitResp, err := wait.CreateCDNCustomDomainWaitHandler(ctx, r.client, projectId, distributionId, name).SetTimeout(5 * time.Minute).WaitWithContext(ctx)
+	if err != nil {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN custom domain", fmt.Sprintf("Waiting for create: %v", err))
+		return
+	}
+
+	err = mapCustomDomainFields(waitResp, &model)
+	if err != nil {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN custom domain", fmt.Sprintf("Processing API payload: %v", err))
+		return
+	}
+
+	diags = resp.State.Set(ctx, model)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+	tflog.Info(ctx, "CDN custom domain created")
+}
+
+func (r *customDomainResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
+	var model CustomDomainModel
+	diags := req.State.Get(ctx, &model)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	projectId := model.ProjectId.ValueString()
+	ctx = tflog.SetField(ctx, "project_id", projectId)
+	distributionId := model.DistributionId.ValueString()
+	ctx = tflog.SetField(ctx, "distribution_id", distributionId)
+	name := model.Name.ValueString()
+	ctx = tflog.SetField(ctx, "name", name)
+
+	customDomainResp, err := r.client.GetCustomDomain(ctx, projectId, distributionId, name).Execute()
+	if err != nil {
+		var oapiErr *oapierror.GenericOpenAPIError
+		// n.b. err is caught here if of type *oapierror.GenericOpenAPIError, which the stackit SDK client returns
+		if errors.As(err, &oapiErr) {
+			if oapiErr.StatusCode == http.StatusNotFound {
+				resp.State.RemoveResource(ctx)
+				return
+			}
+		}
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN custom domain", fmt.Sprintf("Calling API: %v", err))
+		return
+	}
+	err = mapCustomDomainFields(customDomainResp.CustomDomain, &model)
+	if err != nil {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN custom domain", fmt.Sprintf("Processing API payload: %v", err))
+		return
+	}
+	// Set refreshed state
+	diags = resp.State.Set(ctx, model)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+	tflog.Info(ctx, "CDN custom domain read")
+}
+
+func (r *customDomainResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
+	// Update shouldn't be called; custom domains have only computed fields and fields that require replacement when changed
+	core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating CDN custom domain", "Custom domain cannot be updated")
+}
+
+func (r *customDomainResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform
+	var model CustomDomainModel
+	diags := req.State.Get(ctx, &model)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	projectId := model.ProjectId.ValueString()
+	ctx = tflog.SetField(ctx, "project_id", projectId)
+	distributionId := model.DistributionId.ValueString()
+	ctx = tflog.SetField(ctx, "distribution_id", distributionId)
+	name := model.Name.ValueString()
+	ctx = tflog.SetField(ctx, "name", name)
+
+	_, err := r.client.DeleteCustomDomain(ctx, projectId, distributionId, name).Execute()
+	if err != nil {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Delete CDN custom domain", fmt.Sprintf("Delete custom domain: %v", err))
+	}
+	_, err = wait.DeleteCDNCustomDomainWaitHandler(ctx, r.client, projectId, distributionId, name).WaitWithContext(ctx)
+	if err != nil {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Delete CDN custom domain", fmt.Sprintf("Waiting for deletion: %v", err))
+		return
+	}
+	tflog.Info(ctx, "CDN custom domain deleted")
+}
+
+func (r *customDomainResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+	idParts := strings.Split(req.ID, core.Separator)
+
+	if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing CDN custom domain", fmt.Sprintf("Expected import identifier on the format: [project_id]%q[distribution_id]%q[custom_domain_name], got %q", core.Separator, core.Separator, req.ID))
+	}
+	resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
+	resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("distribution_id"), idParts[1])...)
+	resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[2])...)
+	tflog.Info(ctx, "CDN custom domain state imported")
+}
+
+func mapCustomDomainFields(customDomain *cdn.CustomDomain, model *CustomDomainModel) error {
+	if customDomain == nil {
+		return fmt.Errorf("response input is nil")
+	}
+	if model == nil {
+		return fmt.Errorf("model input is nil")
+	}
+
+	if customDomain.Name == nil {
+		return fmt.Errorf("Name is missing in response")
+	}
+
+	if customDomain.Status == nil {
+		return fmt.Errorf("Status missing in response")
+	}
+
+	id := model.ProjectId.ValueString() + core.Separator + model.DistributionId.ValueString() + core.Separator + *customDomain.Name
+
+	model.ID = types.StringValue(id)
+	model.Status = types.StringValue(string(*customDomain.Status))
+
+	customDomainErrors := []attr.Value{}
+	if customDomain.Errors != nil {
+		for _, e := range *customDomain.Errors {
+			customDomainErrors = append(customDomainErrors, types.StringValue(*e.En))
path: stackit/internal/services/cdn/customdomain/resource.go
user: login: bahkauv70
body: Avoid a potential nil-dereference

```suggestion
			customDomainErrors = append(customDomainErrors, types.StringPointerValue(e.En))
```
created_at: 2025-05-05T09:45:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,150 @@
+package cdn
path: stackit/internal/services/cdn/customdomain/datasource.go
user: login: bahkauv70
body: The datasource should have a testcase as well
created_at: 2025-05-05T09:46:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,150 @@
+package cdn
path: stackit/internal/services/cdn/customdomain/datasource.go
user: login: malteehrlen
body: The data source has a test case. The tf setup is on line 84, and the validations start on line 192
created_at: 2025-05-05T09:53:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,307 @@
+package cdn
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/google/uuid"
+	"github.com/hashicorp/terraform-plugin-framework/attr"
+	"github.com/hashicorp/terraform-plugin-framework/path"
+	"github.com/hashicorp/terraform-plugin-framework/resource"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+	"github.com/hashicorp/terraform-plugin-log/tflog"
+	"github.com/stackitcloud/stackit-sdk-go/core/config"
+	"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
+	"github.com/stackitcloud/stackit-sdk-go/services/cdn"
+	"github.com/stackitcloud/stackit-sdk-go/services/cdn/wait"
+	"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
+	"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
+	"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+	_ resource.Resource                = &customDomainResource{}
+	_ resource.ResourceWithConfigure   = &customDomainResource{}
+	_ resource.ResourceWithImportState = &customDomainResource{}
+)
+
+var customDomainSchemaDescriptions = map[string]string{
+	"id":              "Terraform's internal resource identifier. It is structured as \"`project_id`,`distribution_id`\".",
+	"distribution_id": "CDN distribution ID",
+	"project_id":      "STACKIT project ID associated with the distribution",
+	"status":          "Status of the distribution",
+	"errors":          "List of distribution errors",
+}
+
+type CustomDomainModel struct {
+	ID             types.String `tfsdk:"id"`              // Required by Terraform
+	DistributionId types.String `tfsdk:"distribution_id"` // DistributionID associated with the cdn distribution
+	ProjectId      types.String `tfsdk:"project_id"`      // ProjectId associated with the cdn distribution
+	Name           types.String `tfsdk:"name"`            // The custom domain
+	Status         types.String `tfsdk:"status"`          // The status of the cdn distribution
+	Errors         types.List   `tfsdk:"errors"`          // Any errors that the distribution has
+}
+
+type customDomainResource struct {
+	client       *cdn.APIClient
+	providerData core.ProviderData
+}
+
+func NewCustomDomainResource() resource.Resource {
+	return &customDomainResource{}
+}
+
+func (r *customDomainResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+	if req.ProviderData == nil {
+		return
+	}
+	var ok bool
+	if r.providerData, ok = req.ProviderData.(core.ProviderData); !ok {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
+		return
+	}
+	features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_cdn_custom_domain", "resource")
+
+	var apiClient *cdn.APIClient
+	var err error
+	if r.providerData.CdnCustomEndpoint != "" {
+		ctx = tflog.SetField(ctx, "cdn_custom_endpoint", r.providerData.CdnCustomEndpoint)
+		apiClient, err = cdn.NewAPIClient(
+			config.WithCustomAuth(r.providerData.RoundTripper),
+			config.WithEndpoint(r.providerData.CdnCustomEndpoint),
+		)
+	} else {
+		apiClient, err = cdn.NewAPIClient(
+			config.WithCustomAuth(r.providerData.RoundTripper),
+		)
+	}
+	if err != nil {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err))
+		return
+	}
+	r.client = apiClient
+	tflog.Info(ctx, "CDN client configured")
+}
+
+func (r *customDomainResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+	resp.TypeName = req.ProviderTypeName + "_cdn_custom_domain"
+}
+
+func (r *customDomainResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		MarkdownDescription: features.AddBetaDescription("CDN distribution data source schema."),
+		Description:         "CDN distribution data source schema.",
+		Attributes: map[string]schema.Attribute{
+			"id": schema.StringAttribute{
+				Description: customDomainSchemaDescriptions["id"],
+				Computed:    true,
+			},
+			"name": schema.StringAttribute{
+				Description: customDomainSchemaDescriptions["name"],
+				Required:    true,
+				Optional:    false,
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.UseStateForUnknown(),
+				},
+			},
+			"distribution_id": schema.StringAttribute{
+				Description: customDomainSchemaDescriptions["distribution_id"],
+				Required:    true,
+				Optional:    false,
+				Validators:  []validator.String{validate.UUID()},
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.RequiresReplace(),
+				},
+			},
+			"project_id": schema.StringAttribute{
+				Description: customDomainSchemaDescriptions["project_id"],
+				Required:    true,
+				Optional:    false,
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.RequiresReplace(),
+				},
+			},
+			"status": schema.StringAttribute{
+				Computed:    true,
+				Description: customDomainSchemaDescriptions["status"],
+			},
+			"errors": schema.ListAttribute{
+				ElementType: types.StringType,
+				Computed:    true,
+				Description: customDomainSchemaDescriptions["errors"],
+			},
+		},
+	}
+}
+
+func (r *customDomainResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
+	var model CustomDomainModel
+	diags := req.Plan.Get(ctx, &model)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+	projectId := model.ProjectId.ValueString()
+	ctx = tflog.SetField(ctx, "project_id", projectId)
+	distributionId := model.DistributionId.ValueString()
+	ctx = tflog.SetField(ctx, "distribution_id", distributionId)
+	name := model.Name.ValueString()
+	ctx = tflog.SetField(ctx, "name", name)
+
+	payload := &cdn.PutCustomDomainPayload{IntentId: cdn.PtrString(uuid.NewString())}
+
+	_, err := r.client.PutCustomDomain(ctx, projectId, distributionId, name).PutCustomDomainPayload(*payload).Execute()
+	if err != nil {
path: stackit/internal/services/cdn/customdomain/resource.go
user: login: malteehrlen
body: OK
created_at: 2025-05-05T09:54:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,307 @@
+package cdn
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/google/uuid"
+	"github.com/hashicorp/terraform-plugin-framework/attr"
+	"github.com/hashicorp/terraform-plugin-framework/path"
+	"github.com/hashicorp/terraform-plugin-framework/resource"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+	"github.com/hashicorp/terraform-plugin-log/tflog"
+	"github.com/stackitcloud/stackit-sdk-go/core/config"
+	"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
+	"github.com/stackitcloud/stackit-sdk-go/services/cdn"
+	"github.com/stackitcloud/stackit-sdk-go/services/cdn/wait"
+	"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
+	"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
+	"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+	_ resource.Resource                = &customDomainResource{}
+	_ resource.ResourceWithConfigure   = &customDomainResource{}
+	_ resource.ResourceWithImportState = &customDomainResource{}
+)
+
+var customDomainSchemaDescriptions = map[string]string{
+	"id":              "Terraform's internal resource identifier. It is structured as \"`project_id`,`distribution_id`\".",
+	"distribution_id": "CDN distribution ID",
+	"project_id":      "STACKIT project ID associated with the distribution",
+	"status":          "Status of the distribution",
+	"errors":          "List of distribution errors",
+}
+
+type CustomDomainModel struct {
+	ID             types.String `tfsdk:"id"`              // Required by Terraform
+	DistributionId types.String `tfsdk:"distribution_id"` // DistributionID associated with the cdn distribution
+	ProjectId      types.String `tfsdk:"project_id"`      // ProjectId associated with the cdn distribution
+	Name           types.String `tfsdk:"name"`            // The custom domain
+	Status         types.String `tfsdk:"status"`          // The status of the cdn distribution
+	Errors         types.List   `tfsdk:"errors"`          // Any errors that the distribution has
+}
+
+type customDomainResource struct {
+	client       *cdn.APIClient
+	providerData core.ProviderData
+}
+
+func NewCustomDomainResource() resource.Resource {
+	return &customDomainResource{}
+}
+
+func (r *customDomainResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+	if req.ProviderData == nil {
+		return
+	}
+	var ok bool
+	if r.providerData, ok = req.ProviderData.(core.ProviderData); !ok {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
+		return
+	}
+	features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_cdn_custom_domain", "resource")
+
+	var apiClient *cdn.APIClient
+	var err error
+	if r.providerData.CdnCustomEndpoint != "" {
+		ctx = tflog.SetField(ctx, "cdn_custom_endpoint", r.providerData.CdnCustomEndpoint)
+		apiClient, err = cdn.NewAPIClient(
+			config.WithCustomAuth(r.providerData.RoundTripper),
+			config.WithEndpoint(r.providerData.CdnCustomEndpoint),
+		)
+	} else {
+		apiClient, err = cdn.NewAPIClient(
+			config.WithCustomAuth(r.providerData.RoundTripper),
+		)
+	}
+	if err != nil {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err))
+		return
+	}
+	r.client = apiClient
+	tflog.Info(ctx, "CDN client configured")
+}
+
+func (r *customDomainResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+	resp.TypeName = req.ProviderTypeName + "_cdn_custom_domain"
+}
+
+func (r *customDomainResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		MarkdownDescription: features.AddBetaDescription("CDN distribution data source schema."),
+		Description:         "CDN distribution data source schema.",
+		Attributes: map[string]schema.Attribute{
+			"id": schema.StringAttribute{
+				Description: customDomainSchemaDescriptions["id"],
+				Computed:    true,
+			},
+			"name": schema.StringAttribute{
+				Description: customDomainSchemaDescriptions["name"],
+				Required:    true,
+				Optional:    false,
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.UseStateForUnknown(),
+				},
+			},
+			"distribution_id": schema.StringAttribute{
+				Description: customDomainSchemaDescriptions["distribution_id"],
+				Required:    true,
+				Optional:    false,
+				Validators:  []validator.String{validate.UUID()},
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.RequiresReplace(),
+				},
+			},
+			"project_id": schema.StringAttribute{
+				Description: customDomainSchemaDescriptions["project_id"],
+				Required:    true,
+				Optional:    false,
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.RequiresReplace(),
+				},
+			},
+			"status": schema.StringAttribute{
+				Computed:    true,
+				Description: customDomainSchemaDescriptions["status"],
+			},
+			"errors": schema.ListAttribute{
+				ElementType: types.StringType,
+				Computed:    true,
+				Description: customDomainSchemaDescriptions["errors"],
+			},
+		},
+	}
+}
+
+func (r *customDomainResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
+	var model CustomDomainModel
+	diags := req.Plan.Get(ctx, &model)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+	projectId := model.ProjectId.ValueString()
+	ctx = tflog.SetField(ctx, "project_id", projectId)
+	distributionId := model.DistributionId.ValueString()
+	ctx = tflog.SetField(ctx, "distribution_id", distributionId)
+	name := model.Name.ValueString()
+	ctx = tflog.SetField(ctx, "name", name)
+
+	payload := &cdn.PutCustomDomainPayload{IntentId: cdn.PtrString(uuid.NewString())}
+
+	_, err := r.client.PutCustomDomain(ctx, projectId, distributionId, name).PutCustomDomainPayload(*payload).Execute()
+	if err != nil {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN custom domain", fmt.Sprintf("Calling API: %v", err))
+		return
+	}
+	waitResp, err := wait.CreateCDNCustomDomainWaitHandler(ctx, r.client, projectId, distributionId, name).SetTimeout(5 * time.Minute).WaitWithContext(ctx)
+	if err != nil {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN custom domain", fmt.Sprintf("Waiting for create: %v", err))
+		return
+	}
+
+	err = mapCustomDomainFields(waitResp, &model)
+	if err != nil {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN custom domain", fmt.Sprintf("Processing API payload: %v", err))
+		return
+	}
+
+	diags = resp.State.Set(ctx, model)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+	tflog.Info(ctx, "CDN custom domain created")
+}
+
+func (r *customDomainResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
+	var model CustomDomainModel
+	diags := req.State.Get(ctx, &model)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	projectId := model.ProjectId.ValueString()
+	ctx = tflog.SetField(ctx, "project_id", projectId)
+	distributionId := model.DistributionId.ValueString()
+	ctx = tflog.SetField(ctx, "distribution_id", distributionId)
+	name := model.Name.ValueString()
+	ctx = tflog.SetField(ctx, "name", name)
+
+	customDomainResp, err := r.client.GetCustomDomain(ctx, projectId, distributionId, name).Execute()
+	if err != nil {
+		var oapiErr *oapierror.GenericOpenAPIError
+		// n.b. err is caught here if of type *oapierror.GenericOpenAPIError, which the stackit SDK client returns
+		if errors.As(err, &oapiErr) {
+			if oapiErr.StatusCode == http.StatusNotFound {
+				resp.State.RemoveResource(ctx)
+				return
+			}
+		}
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN custom domain", fmt.Sprintf("Calling API: %v", err))
+		return
+	}
+	err = mapCustomDomainFields(customDomainResp.CustomDomain, &model)
+	if err != nil {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN custom domain", fmt.Sprintf("Processing API payload: %v", err))
+		return
+	}
+	// Set refreshed state
+	diags = resp.State.Set(ctx, model)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+	tflog.Info(ctx, "CDN custom domain read")
+}
+
+func (r *customDomainResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
+	// Update shouldn't be called; custom domains have only computed fields and fields that require replacement when changed
+	core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating CDN custom domain", "Custom domain cannot be updated")
+}
+
+func (r *customDomainResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform
+	var model CustomDomainModel
+	diags := req.State.Get(ctx, &model)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	projectId := model.ProjectId.ValueString()
+	ctx = tflog.SetField(ctx, "project_id", projectId)
+	distributionId := model.DistributionId.ValueString()
+	ctx = tflog.SetField(ctx, "distribution_id", distributionId)
+	name := model.Name.ValueString()
+	ctx = tflog.SetField(ctx, "name", name)
+
+	_, err := r.client.DeleteCustomDomain(ctx, projectId, distributionId, name).Execute()
+	if err != nil {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Delete CDN custom domain", fmt.Sprintf("Delete custom domain: %v", err))
+	}
+	_, err = wait.DeleteCDNCustomDomainWaitHandler(ctx, r.client, projectId, distributionId, name).WaitWithContext(ctx)
+	if err != nil {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Delete CDN custom domain", fmt.Sprintf("Waiting for deletion: %v", err))
+		return
+	}
+	tflog.Info(ctx, "CDN custom domain deleted")
+}
+
+func (r *customDomainResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+	idParts := strings.Split(req.ID, core.Separator)
+
+	if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
+		core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing CDN custom domain", fmt.Sprintf("Expected import identifier on the format: [project_id]%q[distribution_id]%q[custom_domain_name], got %q", core.Separator, core.Separator, req.ID))
+	}
+	resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
+	resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("distribution_id"), idParts[1])...)
+	resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[2])...)
+	tflog.Info(ctx, "CDN custom domain state imported")
+}
+
+func mapCustomDomainFields(customDomain *cdn.CustomDomain, model *CustomDomainModel) error {
+	if customDomain == nil {
+		return fmt.Errorf("response input is nil")
+	}
+	if model == nil {
+		return fmt.Errorf("model input is nil")
+	}
+
+	if customDomain.Name == nil {
+		return fmt.Errorf("Name is missing in response")
+	}
+
+	if customDomain.Status == nil {
+		return fmt.Errorf("Status missing in response")
+	}
+
+	id := model.ProjectId.ValueString() + core.Separator + model.DistributionId.ValueString() + core.Separator + *customDomain.Name
+
+	model.ID = types.StringValue(id)
+	model.Status = types.StringValue(string(*customDomain.Status))
+
+	customDomainErrors := []attr.Value{}
+	if customDomain.Errors != nil {
+		for _, e := range *customDomain.Errors {
+			customDomainErrors = append(customDomainErrors, types.StringValue(*e.En))
path: stackit/internal/services/cdn/customdomain/resource.go
user: login: malteehrlen
body: added a nil check
created_at: 2025-05-05T09:54:21Z
author_association: CONTRIBUTOR

PR comments:
user: login: malteehrlen
created_at: 2025-05-02T10:24:41Z
body: One problem I ran into a couple of times when executing the acceptance test is that the dns record is not always available even though the resource was created successfully. That leads to an error in the custom domain creation. Still figuring out how to solve this
author_association: CONTRIBUTOR
user: login: malteehrlen
created_at: 2025-05-05T07:47:15Z
body: The test flakiness has been resolved, I added a CNAME waiter to the test steps
author_association: CONTRIBUTOR