Deploying this site with AWS CDK
A static export can be hosted in many places. This repository chooses a fairly
explicit path: AWS CDK defines a private S3 origin, a CloudFront distribution,
Route 53 records, an ACM certificate, and the deployment of the generated
out/ directory. That gives me one versioned description of the system from
build artifact to public hostname. It also turns a personal site into a small
cloud platform with several resources, two AWS regions, credentials, and a
deployment toolchain to maintain.
The source can establish that architecture, but it cannot establish the original personal reason for choosing it. I am therefore treating the current repository as the decision to evaluate, not reconstructing a provider bake-off or claiming that a particular operational problem happened. The useful question is narrower: what control does this design buy now, and what ceremony comes with it?
The deployment starts with a hard boundary
The web build produces a Next.js static export in out/. CDK does not build a
server or run Next.js at request time; it packages that directory as the source
for an S3 bucket deployment. The CDK entry point is deliberately small and is
declared in cdk.json:
{
"app": "node infra/app.js"
}
infra/app.js accepts only the prod environment, names the stack
GeorgeJeng-prod, and targets us-west-1. The production deployment script
first runs the web build with the public site URL, removes the previous
cdk.out, and invokes cdk deploy with the repository's AWS profile. The
manifest currently declares aws-cdk-lib as ^2.260.0; the infrastructure is
ordinary JavaScript using that library rather than a separate template checked
in by hand.
That production-only check matters. The code is repeatable for the supported
stack, but it is not a generic multi-environment module. Passing another
environment makes the application throw. Likewise, the cached Route 53 lookup
in cdk.context.json is tied to the configured domain, account, and stack
region. “Infrastructure as code” makes those constraints visible; it does not
automatically make the stack portable to every account or environment.
S3 is private and CloudFront is the public edge
The hosting module keeps the origin bucket out of the public request path.
Public access is blocked, SSL is enforced, and CloudFront reaches the bucket
through an origin access control. The distribution redirects viewers to HTTPS,
uses the optimized cache policy, and serves index.html as its default root
object. These are the exact resource declarations that connect the built site
to the distribution:
const frontendHostingBucket = new s3.Bucket(scope, 'FrontendHostingBucket', {
bucketName: frontendHostingBucketName,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
enforceSSL: true,
removalPolicy: statefulRemovalPolicy,
autoDeleteObjects: !isProductionEnvironment,
})
const originAccessControl = new cloudfront.S3OriginAccessControl(
scope,
'FrontendHostingOriginAccessControl',
{
originAccessControlName: `george-jeng-${envName}-frontend-hosting-oac`,
description: `OAC for George Jeng ${envName} frontend hosting bucket`,
},
)
const frontendDistribution = new cloudfront.Distribution(
scope,
'FrontendHostingDistribution',
{
comment: `George Jeng ${envName} frontend hosting distribution`,
certificate,
defaultRootObject: 'index.html',
domainNames,
defaultBehavior: {
origin: origins.S3BucketOrigin.withOriginAccessControl(
frontendHostingBucket,
{
originAccessControl,
},
),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
functionAssociations: [
{
function: routeRewriteFunction,
eventType: cloudfront.FunctionEventType.VIEWER_REQUEST,
},
],
},
},
)
new s3deploy.BucketDeployment(scope, 'FrontendHostingDeployment', {
destinationBucket: frontendHostingBucket,
distribution: frontendDistribution,
distributionPaths: ['/*'],
waitForDistributionInvalidation: false,
memoryLimit: 1024,
ephemeralStorageSize: Size.mebibytes(1024),
prune: true,
retainOnDelete: isProductionEnvironment,
sources: [s3deploy.Source.asset(frontendBuildPath)],
})
The deployment prunes objects that are no longer in the build, supplies /*
as the distribution invalidation path, and is configured not to wait for that
invalidation. Production state is retained on deletion; the bucket is not
configured to auto-delete its objects. Those are consequential lifecycle
choices, and keeping them beside the bucket and deployment makes them
reviewable together.
CloudFront also runs a viewer-request function. It redirects the www host to
the apex domain and translates directory-shaped static-export URLs to their
index.html objects. A request for /articles/example/, for instance, needs
an origin path ending in /index.html; a request for a filename containing a
dot is left alone. This function is part of the hosting contract. Moving only
the files to another object store without reproducing that routing behavior
would not reproduce the site.
The domain is another chain of resources
The foundation stack creates the custom-domain resources first, passes their
certificate and domain names into the hosting module, then points DNS at the
resulting distribution. Route 53 looks up the existing hosted zone for the
apex domain. ACM creates a DNS-validated certificate for both the apex and
www names, explicitly in us-east-1, while the foundation stack itself is
in us-west-1.
After CloudFront exists, the stack creates Route 53 alias records for it: A and
AAAA records for both the apex and www names. The same distribution accepts
both hosts, and the viewer-request function performs the canonical-host
redirect. Certificate validation, distribution aliases, DNS records, and
redirect behavior therefore form one connected deployment rather than four
unrelated console settings.
That connection is where AWS CDK earns much of its complexity. Resource references carry the certificate and distribution through the stack, and the order follows from those references. A code review can see a DNS change beside the distribution it targets. A synthesis materializes a reviewable CloudFormation template from the source before deployment. If the stack must be recreated in its supported environment, its important settings are in source instead of depending on someone remembering a sequence of console clicks.
Repeatability still needs a qualifier. CDK can repeatedly synthesize the same
declared system, but the deployment also depends on an AWS account, the named
SSO profile, regional context, permissions, the existing hosted zone, package
versions, and the contents of out/. The code controls more of the process; it
does not remove every external prerequisite.
Control has an operational price
This repository contains no billing history, so I cannot attach a dollar amount or claim that this arrangement is cheaper than a managed host. I can identify the cost surface it creates. Storage and object requests belong to S3; edge requests and transfer belong to CloudFront; the hosted zone and DNS queries belong to Route 53. Those dimensions can be useful when fine-grained control matters, but they also mean that the total is assembled across services instead of presented as one static-site product.
Complexity is similarly distributed. A deployer needs the web toolchain, the CDK application, AWS credentials, the configured profile, the correct region, and a valid synthesized stack. The certificate lives in a different region from the stack. The CloudFront function owns URL semantics that a managed host might supply through a routing configuration. The bucket, distribution, certificate, hosted zone, records, deployment resource, and their lifecycle policies all have to keep agreeing.
Maintenance follows those boundaries. The CDK library and CLI need compatible updates. The SSO and IAM path must continue to authorize synthesis and deployment. The route-rewrite function has to evolve with the exported URL shape. DNS and certificate configuration must remain aligned with the distribution, and deployment behavior such as pruning and invalidation needs review when the asset strategy changes. These are responsibilities implied by the current source, not a claim that any of them has already caused an outage or a measured delay.
Portability splits cleanly at out/. The generated HTML, JavaScript, CSS, and
assets are static files and could move to many hosts. The CDK layer is much
less portable: its bucket policy, origin access control, distribution,
certificate, DNS aliases, and viewer-request function are AWS-specific. A
migration would preserve the artifact but translate the infrastructure and
routing semantics. Infrastructure as code makes the existing system easier to
reproduce inside AWS; it does not make the system cloud-neutral.
A managed static host is a serious alternative
The direct alternative is a managed static host that takes a repository or prebuilt directory and supplies deployment, CDN delivery, TLS, custom-domain wiring, and route rules as one product. I am not claiming that a named provider was historically evaluated or rejected here; the repository does not contain that history. This is a present-day comparison with the responsibilities in the current stack.
That managed path would reduce the number of cloud resources a maintainer has to understand. Git-connected previews, certificate renewal, cache behavior, and deployment status may be exposed through a narrower workflow. The trade is less control over the precise resources and policies, reliance on the host's configuration model and limits, and a different kind of portability risk: the files remain portable, while provider-specific build and routing features may not.
I would choose the managed path for a personal static site whose primary goal is publishing with minimal infrastructure work, especially when it does not need AWS-specific integrations or independently configured DNS, storage, and edge behavior. It would also be the better default for a team that wants preview deployments and domain management but does not want AWS credentials and CDK knowledge to be part of the publishing workflow.
The current approach is more defensible when the infrastructure itself is part of the requirement: a private origin, explicit lifecycle policies, a custom edge rewrite, controlled DNS aliases, and one code review surface for the resource chain. Even then, the choice is not free. I get a precise, repeatable AWS deployment and accept that I am maintaining a small AWS system rather than only uploading a static directory.
That is the real boundary of this architecture. AWS CDK turns S3, CloudFront, Route 53, and ACM into a coherent program, and the static artifact remains simple. The program gives me control over how the site reaches the internet. The ceremony, cost surface, maintenance, and cloud-specific code are the price of keeping that control in this repository.