Tired of manually configuring cloud resources for your Hugo blog on AWS? Infrastructure as Code (IaC) offers a better way to manage, version, and deploy your infrastructure reliably. In this post, I’ll explain what IaC is, compare the major tools available, and share why I chose Pulumi for my setup — complete with solutions for tricky scenarios like using a .nl domain purchased outside Route 53.
Managing cloud infrastructure by hand is fun — right up until you forget which S3 bucket setting you changed, why CloudFront is suddenly caching the wrong thing, or which Lambda function you tweaked at 11 PM. Infrastructure as Code (IaC) solves all of that: your infrastructure becomes version-controlled, repeatable, and reviewable just like your application code.
In this post I’ll walk through what IaC is, which tools are available, why I landed on Pulumi for my Hugo blog on AWS, and how to handle one tricky real-world problem: using a .nl domain bought outside of Route 53.
What Is Infrastructure as Code?
Infrastructure as Code means describing your cloud resources (servers, buckets, DNS records, CDN distributions, functions, …) in files that a tool then applies to your cloud provider. Instead of clicking through the AWS console, you write a definition and let the tool figure out what needs to be created, changed, or deleted.
The benefits are concrete:
- Reproducibility — rebuild your entire stack in a new account or region with one command.
- Version control — every change is a commit; roll back is a git revert.
- Review & collaboration — your infra changes go through the same pull-request process as your code.
- Auditability — you always know the current desired state of your infrastructure.
- Drift detection — if someone changes something in the console, the tool tells you.
The Main IaC Tools
There are several serious options, each with a different philosophy.
Terraform / OpenTofu
Terraform by HashiCorp is the most widely adopted IaC tool. It uses its own declarative language called HCL (HashiCorp Configuration Language). You describe what you want, and Terraform computes a plan to get there.
Pros:
- Enormous community and module ecosystem.
- Mature, battle-tested, works with every cloud.
- Clear separation between plan and apply.
Cons:
- HCL is a domain-specific language — you need to learn it, and it has real limitations when you need loops, conditionals, or dynamic behaviour.
- Limited reuse patterns compared to general-purpose languages.
- HashiCorp’s license change led to the community fork OpenTofu, so you now need to decide which one to use.
AWS CDK (Cloud Development Kit)
AWS CDK lets you write infrastructure in TypeScript, Python, Java, or Go — but it ultimately synthesises to CloudFormation under the hood.
Pros:
- Full programming language support (Python included).
- High-level “constructs” that bundle multiple AWS resources together sensibly.
- First-class AWS support and deep integration.
Cons:
- AWS-only. If you ever move even partially off AWS, you start over.
- CloudFormation underneath means you inherit its quirks, long deploy times, and stack limits.
- Error messages can be hard to trace through the CDK-to-CFN synthesis layer.
Pulumi
Pulumi is the new-generation IaC tool built around the idea that infrastructure is software. You write your infra in Python, TypeScript, Go, or .NET — real general-purpose languages with real package managers, testing frameworks, and IDEs.
Pros:
- Python support — if you already know Python, you can start immediately.
- No DSL to learn; use pip packages, write functions, loops, classes.
- Multi-cloud from day one (AWS, Azure, GCP, Cloudflare, etc.).
- Strong state management (Pulumi Cloud, or self-hosted with S3).
- Excellent AWS support via
pulumi-aws.
Cons:
- Smaller community than Terraform (though growing fast).
- The Pulumi Cloud (free tier) stores state remotely — you need to decide where your state lives.
Ansible / CloudFormation / SAM
These exist but are generally not the right tool for the job described here. CloudFormation is verbose JSON/YAML with sharp edges. Ansible is great for configuration management but awkward for cloud resource provisioning. AWS SAM is purpose-built for serverless but limited in scope.
Why I Chose Pulumi
My stack is a Hugo static site on S3, served via CloudFront, with Lambda@Edge functions for custom page handling and Route 53 for DNS. That means I’m dealing with:
- An S3 bucket with specific policies and static website config
- A CloudFront distribution with custom behaviours, cache policies, and origin access
- One or more Lambda@Edge functions (which must live in
us-east-1) - Route 53 hosted zones and records
- SSL certificates via ACM (also
us-east-1for CloudFront)
Here is why Pulumi fits this exactly:
Python without compromise. I already write Python. With Pulumi I can import pulumi_aws, define a function that creates a CloudFront distribution, pass it a config object, and call it from a loop if I want multiple environments. No HCL, no YAML soup.
Real logic for real problems. Lambda@Edge functions must be deployed in us-east-1 regardless of where your main stack lives. In Pulumi this is trivial: create an aws.Provider pointing at us-east-1 and pass it to your Lambda resource. In Terraform it requires provider aliasing with extra wiring.
Multi-provider in one stack. My domain is registered outside AWS (more on this below). Pulumi has providers for Cloudflare, Namecheap-compatible DNS, and many others. I can manage my AWS infrastructure and update DNS records at my external registrar in the same pulumi up run.
Testability. Because it’s Python, I can unit test my infra logic with pytest. Pulumi even has a dedicated testing framework for mocking resource outputs.
The Domain Problem: .nl Domains and Route 53
Route 53 does not sell .nl domains. If you want a .nl domain, you need to buy it from a registrar that supports it — popular choices for Dutch domains are:
- Hostnet (nl-based, very reliable)
- TransIP (nl-based, developer-friendly API)
- Cloudflare Registrar (at-cost pricing, excellent API, no
.nlmarkup) - Namecheap
The good news is that Route 53 does not need to register your domain to manage DNS for it. The standard pattern is:
- Buy the domain at your preferred registrar (e.g., TransIP or Cloudflare).
- Create a Route 53 Hosted Zone for your domain. Route 53 gives you four nameservers.
- Point your domain’s nameservers at those four Route 53 nameservers, in your registrar’s control panel.
- Route 53 now handles all DNS for your domain, even though it was registered elsewhere.
This is a one-time manual step (updating nameservers at your registrar), but everything after that — A records, AAAA records, CNAME, aliases to CloudFront — is managed in Pulumi via aws.route53.Record.
Supporting Multiple Domains in Pulumi
Because Pulumi is Python, supporting multiple domains is clean. You can define a list of domains and iterate:
import pulumi
import pulumi_aws as aws
config = pulumi.Config()
domains = config.require_object("domains") # e.g. ["yourblog.nl", "yourblog.com"]
for domain in domains:
zone = aws.route53.Zone(f"zone-{domain}", name=domain)
aws.route53.Record(
f"alias-{domain}",
zone_id=zone.zone_id,
name=domain,
type="A",
aliases=[aws.route53.RecordAliasArgs(
name=cloudfront_distribution.domain_name,
zone_id=cloudfront_distribution.hosted_zone_id,
evaluate_target_health=False,
)],
)
You can store the domains list in Pulumi.<stack>.yaml or pass it as a secret, and Pulumi handles the rest. When you add a new domain, you add it to the list, run pulumi up, and get a new hosted zone with its nameserver records output — ready to paste into your external registrar.
SSL Certificates for Multiple Domains
CloudFront requires an ACM certificate in us-east-1. To cover multiple domains, you request a single certificate with Subject Alternative Names (SANs):
us_east_provider = aws.Provider("us-east-1", region="us-east-1")
cert = aws.acm.Certificate(
"blog-cert",
domain_name=domains[0],
subject_alternative_names=domains[1:],
validation_method="DNS",
opts=pulumi.ResourceOptions(provider=us_east_provider),
)
Pulumi will output the DNS validation records you need to add to each hosted zone — and you can automate that too, creating the validation records automatically for the Route 53-managed domains.
Putting It Together: The Stack at a Glance
Here is the high-level picture of what Pulumi will manage for this blog:
Pulumi stack
├── S3 Bucket (private, OAC-enabled)
│ └── Bucket Policy (allow CloudFront OAC only)
├── CloudFront Distribution
│ ├── Origin → S3 (via Origin Access Control)
│ ├── Cache Behaviours (Hugo pages, assets, API paths)
│ └── Lambda@Edge (us-east-1 provider)
│ └── viewer-request / origin-request functions
├── ACM Certificate (us-east-1 provider, SAN for all domains)
└── Route 53
├── Hosted Zone per domain
├── A Alias record → CloudFront
└── ACM validation records
Everything above lives in one Pulumi project, runs with pulumi up, and produces clear output showing which resources were created, changed, or deleted.
Next Steps
In the follow-up posts I will cover:
- Setting up the Pulumi project — project structure, state backend choice (Pulumi Cloud vs S3), and secrets management with
pulumi.Config. - The S3 + CloudFront module — bucket policies, Origin Access Control, and cache invalidation on deploy.
- Lambda@Edge in Python — writing, packaging, and deploying edge functions with the
us-east-1provider. - Multi-domain DNS — full example with Route 53 hosted zones, ACM validation, and CloudFront aliases.
- CI/CD integration — running
pulumi upfrom GitHub Actions on every push to main.
If you are evaluating IaC tools for a similar AWS-hosted static site, I hope this gives you a clear picture of the landscape. Pulumi’s Python-native approach removes the biggest friction point — learning a new language — while giving you the full power of a real programming language to handle the edge cases that every non-trivial infrastructure eventually throws at you.
Ready to automate your Hugo blog’s AWS infrastructure? Give Pulumi a try and share your setup in the comments!
