< Return Home

Building a Zero-Cost Serverless Blog with AWS CDK, S3, and Python

[SYS_DATE: 2026-08] | [CAT: ARCHITECTURE_LESSONS]

Deploying a personal blog or technical portfolio shouldn't mean managing virtual servers or paying monthly hosting subscriptions. By combining AWS CDK (Python), Amazon S3, and Amazon CloudFront, you can build a serverless static blog that costs fractions of a cent to host and offers near-infinite scalability.

1. The Architecture Overview

Instead of configuring an S3 bucket as a public website endpoint, the security best practice is to keep the S3 bucket completely private (BlockPublicAccess.BLOCK_ALL).

[ Reader ] ---> [ CloudFront CDN (HTTPS) ] ---> [ S3 Bucket (Private Content) ]
                                 ▲
                                 │ (Origin Access Control)

2. Infrastructure as Code: Python CDK Stack

This Python CDK stack provisions the private S3 bucket, configures CloudFront OAC, sets up an automated deployment pipeline for local web assets, and logs the public live URL.

import aws_cdk as cdk
from aws_cdk import (
    Stack,
    CfnOutput,
    RemovalPolicy,
    aws_s3 as s3,
    aws_s3_deployment as s3deploy,
    aws_cloudfront as cloudfront,
    aws_cloudfront_origins as origins,
)
from constructs import Construct

class BlogInfrastructureStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # 1. Private S3 Bucket for static site assets
        site_bucket = s3.Bucket(
            self, "BlogAssetBucket",
            block_public_access=s3.BlockPublicAccess.BLOCK_ALL,  # Max security
            removal_policy=RemovalPolicy.DESTROY,                 # Teardown on cdk destroy
            auto_delete_objects=True                               # Empty bucket prior to deletion
        )

        # 2. CloudFront CDN Distribution using Origin Access Control (OAC)
        distribution = cloudfront.Distribution(
            self, "BlogDistribution",
            default_behavior=cloudfront.BehaviorOptions(
                origin=origins.S3BucketOrigin.with_origin_access_control(site_bucket)
            ),
            default_root_object="index.html"
        )

        # 3. Automatic Deployment of local site assets to S3
        s3deploy.BucketDeployment(
            self, "DeployBlogAssets",
            sources=[s3deploy.Source.asset("./website")],
            destination_bucket=site_bucket,
            distribution=distribution  # Auto-invalidates CloudFront cache on deploy
        )

        # 4. Output the public CloudFront URL
        CfnOutput(
            self, "LiveBlogURL",
            value=f"https://{distribution.distribution_domain_name}",
            description="Live HTTPS endpoint for the blog"
        )

3. Automated Markdown Compilation Script

To avoid writing raw HTML manually for every article, use this lightweight Python build script. It parses markdown files (.md), wraps them in clean retro-styled HTML, and populates your public deployment folder.

import os
import glob
import markdown

HTML_TEMPLATE = """<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{title}</title>
    <link rel="stylesheet" href="./styles.css">
</head>
<body>
    <p><a href="./index.html">&lt;-- RETURN_TO_HOME</a></p>
    <article>
        {content}
    </article>
</body>
</html>
"""

def build_static_pages():
    os.makedirs("./website", exist_ok=True)
    os.makedirs("./posts", exist_ok=True)

    md_parser = markdown.Markdown(extensions=['fenced_code', 'tables'])

    for md_file in glob.glob("./posts/*.md"):
        filename = os.path.basename(md_file)
        slug = os.path.splitext(filename)[0]

        with open(md_file, "r", encoding="utf-8") as f:
            raw_markdown = f.read()

        html_body = md_parser.convert(raw_markdown)
        md_parser.reset()

        page_title = slug.replace("-", " ").title()
        formatted_html = HTML_TEMPLATE.format(title=page_title, content=html_body)

        output_file = f"./website/{slug}.html"
        with open(output_file, "w", encoding="utf-8") as f:
            f.write(formatted_html)

        print(f"Successfully compiled: {md_file} -> {output_file}")

if __name__ == "__main__":
    build_static_pages()

4. Cost Control & Safeguards

While S3 storage and CloudFront bandwidth fall within the AWS Free Tier for most small blogs (up to 1 TB of monthly outbound transfer), setting up an AWS Budget Alert prevents unexpected charges:

from aws_cdk import aws_budgets as budgets

budgets.CfnBudget(
    self, "MonthlyCostGuardrail",
    budget=budgets.CfnBudget.BudgetDataProperty(
        budget_name="BlogCostGuardrail",
        budget_type="COST",
        time_unit="MONTHLY",
        budget_limit=budgets.CfnBudget.SpendProperty(amount=1, unit="USD")
    ),
    notifications_with_subscribers=[
        budgets.CfnBudget.NotificationWithSubscribersProperty(
            notification=budgets.CfnBudget.NotificationProperty(
                comparison_operator="GREATER_THAN",
                notification_type="ACTUAL",
                threshold=100,
                threshold_type="PERCENTAGE"
            ),
            subscribers=[budgets.CfnBudget.SubscriberProperty(address="admin@example.com", subscription_type="EMAIL")]
        )
    ]
)

5. Deployment Workflow