Skip to content
Back to Blog
1 min read

Accelerating Content Delivery with Azure CDN

I wrote “2021-09-28-azure-cdn” to share practical, production-minded guidance on this topic.

CDN Offerings

ProviderFeaturesUse Case
Standard MicrosoftBasic caching, rules engineGeneral purpose
Standard VerizonAdvanced analyticsMedia streaming
Standard AkamaiLarge file optimizationSoftware distribution
Premium VerizonAdvanced rules, real-time analyticsEnterprise

Creating a CDN Profile and Endpoint

from azure.mgmt.cdn import CdnManagementClient
from azure.mgmt.cdn.models import (
    Profile,
    Sku,
    Endpoint,
    DeepCreatedOrigin,
    QueryStringCachingBehavior
)
from azure.identity import DefaultAzureCredential

class CDNManager:
    def __init__(self, subscription_id: str, resource_group: str):
        self.credential = DefaultAzureCredential()
        self.client = CdnManagementClient(self.credential, subscription_id)
        self.resource_group = resource_group

    def create_profile(self, profile_name: str, sku: str = "Standard_Microsoft") -> Profile:
        """Create a CDN profile."""

        profile = Profile(
            location="global",
            sku=Sku(name=sku)
        )

        return self.client.profiles.begin_create(
            self.resource_group,
            profile_name,
            profile
        ).result()

    def create_endpoint(self, profile_name: str, endpoint_name: str,
                       origin_hostname: str,
                       origin_path: str = None) -> Endpoint:
        """Create a CDN endpoint."""

        endpoint = Endpoint(
            location="global",
            origins=[
                DeepCreatedOrigin(
                    name="origin1",
                    host_name=origin_hostname,
                    http_port=80,
                    https_port=443,
                    origin_host_header=origin_hostname
                )
            ],
            origin_path=origin_path,
            is_http_allowed=True,
            is_https_allowed=True,
            query_string_caching_behavior=QueryStringCachingBehavior.IGNORE_QUERY_STRING,
            is_compression_enabled=True,
            content_types_to_compress=[
                "text/plain",
                "text/html",
                "text/css",
                "text/javascript",
                "application/javascript",
                "application/json",
                "application/xml"
            ]
        )

        return self.client.endpoints.begin_create(
            self.resource_group,
            profile_name,
            endpoint_name,
            endpoint
        ).result()

    def purge_content(self, profile_name: str, endpoint_name: str,
                     paths: list):
        """Purge cached content."""

        return self.client.endpoints.begin_purge_content(
            self.resource_group,
            profile_name,
            endpoint_name,
            {"content_paths": paths}
        ).result()

    def load_content(self, profile_name: str, endpoint_name: str,
                    paths: list):
        """Pre-load content into cache."""

        return self.client.endpoints.begin_load_content(
            self.resource_group,
            profile_name,
            endpoint_name,
            {"content_paths": paths}
        ).result()

# Usage
cdn_manager = CDNManager("subscription-id", "cdn-rg")

# Create profile
cdn_manager.create_profile("mycdn-profile")

# Create endpoint for blob storage
cdn_manager.create_endpoint(
    "mycdn-profile",
    "static-content",
    "mystorageaccount.blob.core.windows.net",
    "/public"
)

Configuring Custom Domains

from azure.mgmt.cdn.models import (
    CustomDomain,
    CustomDomainHttpsParameters,
    CdnManagedHttpsParameters,
    CertificateType,
    ProtocolType,
    MinimumTlsVersion
)

def add_custom_domain(cdn_manager: CDNManager, profile_name: str,
                     endpoint_name: str, custom_domain: str):
    """Add a custom domain to CDN endpoint."""

    # Custom domain name (without protocol)
    domain_name = custom_domain.replace(".", "-")

    custom_domain_obj = CustomDomain(
        host_name=custom_domain
    )

    # Create custom domain
    result = cdn_manager.client.custom_domains.begin_create(
        cdn_manager.resource_group,
        profile_name,
        endpoint_name,
        domain_name,
        custom_domain_obj
    ).result()

    return result

def enable_https(cdn_manager: CDNManager, profile_name: str,
                endpoint_name: str, domain_name: str):
    """Enable HTTPS with CDN-managed certificate."""

    https_params = CdnManagedHttpsParameters(
        certificate_source="Cdn",
        protocol_type=ProtocolType.SERVER_NAME_INDICATION,
        minimum_tls_version=MinimumTlsVersion.TLS12,
        certificate_source_parameters={
            "certificate_type": CertificateType.DEDICATED
        }
    )

    return cdn_manager.client.custom_domains.begin_enable_custom_https(
        cdn_manager.resource_group,
        profile_name,
        endpoint_name,
        domain_name,
        https_params
    ).result()

# Add custom domain
add_custom_domain(cdn_manager, "mycdn-profile", "static-content", "cdn.example.com")

# Enable HTTPS
enable_https(cdn_manager, "mycdn-profile", "static-content", "cdn-example-com")

Rules Engine Configuration

from azure.mgmt.cdn.models import (
    DeliveryRule,
    DeliveryRuleCondition,
    UrlFileExtensionMatchConditionParameters,
    UrlPathMatchConditionParameters,
    DeliveryRuleCacheExpirationAction,
    CacheExpirationActionParameters,
    HeaderAction,
    HeaderActionParameters,
    UrlRedirectAction,
    UrlRedirectActionParameters
)

def configure_caching_rules(cdn_manager: CDNManager, profile_name: str,
                           endpoint_name: str):
    """Configure caching rules for different content types."""

    rules = []

    # Rule 1: Cache static assets for 30 days
    rules.append(DeliveryRule(
        name="CacheStaticAssets",
        order=1,
        conditions=[
            {
                "name": "UrlFileExtension",
                "parameters": UrlFileExtensionMatchConditionParameters(
                    operator="Equal",
                    match_values=["css", "js", "png", "jpg", "jpeg", "gif", "svg", "woff", "woff2"]
                )
            }
        ],
        actions=[
            DeliveryRuleCacheExpirationAction(
                parameters=CacheExpirationActionParameters(
                    cache_behavior="Override",
                    cache_type="All",
                    cache_duration="30.00:00:00"
                )
            )
        ]
    ))

    # Rule 2: Don't cache API responses
    rules.append(DeliveryRule(
        name="NoCacheAPI",
        order=2,
        conditions=[
            {
                "name": "UrlPath",
                "parameters": UrlPathMatchConditionParameters(
                    operator="BeginsWith",
                    match_values=["/api/"]
                )
            }
        ],
        actions=[
            DeliveryRuleCacheExpirationAction(
                parameters=CacheExpirationActionParameters(
                    cache_behavior="BypassCache"
                )
            )
        ]
    ))

    # Rule 3: Add security headers
    rules.append(DeliveryRule(
        name="SecurityHeaders",
        order=3,
        conditions=[],
        actions=[
            HeaderAction(
                parameters=HeaderActionParameters(
                    header_action="Append",
                    header_name="X-Content-Type-Options",
                    value="nosniff"
                )
            ),
            HeaderAction(
                parameters=HeaderActionParameters(
                    header_action="Append",
                    header_name="X-Frame-Options",
                    value="DENY"
                )
            ),
            HeaderAction(
                parameters=HeaderActionParameters(
                    header_action="Append",
                    header_name="Strict-Transport-Security",
                    value="max-age=31536000; includeSubDomains"
                )
            )
        ]
    ))

    # Rule 4: Redirect HTTP to HTTPS
    rules.append(DeliveryRule(
        name="HttpsRedirect",
        order=4,
        conditions=[
            {
                "name": "RequestScheme",
                "parameters": {"operator": "Equal", "match_values": ["HTTP"]}
            }
        ],
        actions=[
            UrlRedirectAction(
                parameters=UrlRedirectActionParameters(
                    redirect_type="Found",
                    destination_protocol="Https"
                )
            )
        ]
    ))

    # Update endpoint with rules
    endpoint = cdn_manager.client.endpoints.get(
        cdn_manager.resource_group,
        profile_name,
        endpoint_name
    )

    endpoint.delivery_policy = {"rules": rules}

    return cdn_manager.client.endpoints.begin_update(
        cdn_manager.resource_group,
        profile_name,
        endpoint_name,
        endpoint
    ).result()

CDN Integration with Storage

from azure.storage.blob import BlobServiceClient, generate_blob_sas, BlobSasPermissions
from datetime import datetime, timedelta

class CDNStorageIntegration:
    def __init__(self, storage_connection: str, cdn_hostname: str):
        self.blob_service = BlobServiceClient.from_connection_string(storage_connection)
        self.cdn_hostname = cdn_hostname

    def get_cdn_url(self, container: str, blob_name: str) -> str:
        """Get CDN URL for a blob."""
        return f"https://{self.cdn_hostname}/{container}/{blob_name}"

    def upload_with_cache_headers(self, container: str, blob_name: str,
                                 data: bytes, content_type: str,
                                 cache_control: str = "public, max-age=86400"):
        """Upload blob with cache-control headers."""

        container_client = self.blob_service.get_container_client(container)
        blob_client = container_client.get_blob_client(blob_name)

        blob_client.upload_blob(
            data,
            content_settings={
                "content_type": content_type,
                "cache_control": cache_control
            },
            overwrite=True
        )

        return self.get_cdn_url(container, blob_name)

    def upload_versioned(self, container: str, blob_name: str,
                        data: bytes, version: str) -> str:
        """Upload with version in filename for cache busting."""

        # Add version to filename
        name_parts = blob_name.rsplit('.', 1)
        if len(name_parts) == 2:
            versioned_name = f"{name_parts[0]}.{version}.{name_parts[1]}"
        else:
            versioned_name = f"{blob_name}.{version}"

        return self.upload_with_cache_headers(
            container,
            versioned_name,
            data,
            "application/octet-stream",
            "public, max-age=31536000, immutable"  # 1 year, immutable
        )

# Usage
cdn_storage = CDNStorageIntegration(
    "your-storage-connection",
    "mycdn.azureedge.net"
)

# Upload static assets
with open("styles.css", "rb") as f:
    url = cdn_storage.upload_versioned(
        "static",
        "styles.css",
        f.read(),
        "v1.2.3"
    )
print(f"CDN URL: {url}")

Monitoring and Analytics

from azure.monitor.query import MetricsQueryClient, LogsQueryClient
from datetime import datetime, timedelta

def get_cdn_metrics(metrics_client: MetricsQueryClient,
                   endpoint_resource_id: str,
                   hours: int = 24) -> dict:
    """Get CDN endpoint metrics."""

    end_time = datetime.utcnow()
    start_time = end_time - timedelta(hours=hours)

    metrics = metrics_client.query_resource(
        endpoint_resource_id,
        metric_names=[
            "ByteHitRatio",
            "RequestCount",
            "ResponseSize",
            "OriginLatency"
        ],
        timespan=(start_time, end_time),
        granularity=timedelta(hours=1)
    )

    results = {}
    for metric in metrics.metrics:
        values = []
        for series in metric.timeseries:
            for data in series.data:
                values.append({
                    "timestamp": data.timestamp,
                    "average": data.average,
                    "total": data.total
                })
        results[metric.name] = values

    return results

def generate_cdn_report(metrics: dict) -> str:
    """Generate CDN performance report."""

    report = []
    report.append("=== CDN Performance Report ===\n")

    if "ByteHitRatio" in metrics:
        avg_hit_ratio = sum(v["average"] or 0 for v in metrics["ByteHitRatio"]) / max(len(metrics["ByteHitRatio"]), 1)
        report.append(f"Cache Hit Ratio: {avg_hit_ratio:.1f}%")

    if "RequestCount" in metrics:
        total_requests = sum(v["total"] or 0 for v in metrics["RequestCount"])
        report.append(f"Total Requests: {total_requests:,}")

    if "ResponseSize" in metrics:
        total_bytes = sum(v["total"] or 0 for v in metrics["ResponseSize"])
        report.append(f"Data Transferred: {total_bytes / (1024**3):.2f} GB")

    if "OriginLatency" in metrics:
        avg_latency = sum(v["average"] or 0 for v in metrics["OriginLatency"]) / max(len(metrics["OriginLatency"]), 1)
        report.append(f"Average Origin Latency: {avg_latency:.0f} ms")

    return "\n".join(report)

Best Practices

  1. Enable Compression: Compress text-based content
  2. Set Cache Headers: Use appropriate cache-control headers
  3. Version Assets: Include version in filenames for cache busting
  4. Use HTTPS: Always enable HTTPS with modern TLS
  5. Monitor Hit Ratio: Aim for >90% cache hit ratio
  6. Purge Strategically: Purge only what’s necessary

Azure CDN significantly improves application performance by reducing latency and offloading traffic from origin servers.\n\n## Takeaways\n\nAdd a concise, personal takeaway and recommended next steps here.\n

Michael John Pena

Michael John Pena

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.