Back to blog

Technical notes · AWS operations

A Domain-Driven Approach to AWS Infrastructure

Jovan Hernandez ·

I have several side projects on AWS. The hard part is remembering which repo owns a resource, where a domain points, and what else might break when I change a server.

I want to be able to open a project, understand its infrastructure, and make a change without first reconstructing the whole account. That has shaped how I separate account setup, application infrastructure, compute and data.

Start with domain ownership

I am borrowing from domain-driven design to make infrastructure ownership explicit. Each product has its own data, deployment permissions and lifecycle, even when its services share hardware. Those boundaries guide what belongs in a project’s Terraform stack and what belongs to shared hosting.

A shared EC2 host is an operational grouping, not automatically a bounded context. CardTrends can put its web service and worker on the same machine while retaining a separate identity from other products. DNS names describe traffic routing; they do not define the business domain.

Account setup and project work have different jobs

I use one AWS account, with the active application infrastructure in us-east-1. Shared account configuration lives in a local AWS/ directory: IAM bootstrapping, CLI profiles, DNS zones and state-bucket setup. Each application has its own repository and infrastructure directory.

That separation is mostly about ownership. Creating a deployment role or a hosted zone is account work. Deploying a new version of an application is project work. I want the second task to use a scoped project identity instead of administrator credentials.

Newer projects use assumed deployment roles. Some older projects still have a different access model, including an IAM user and a monitoring stack that uses the account administration profile. Those are migration items. The convention is ahead of parts of the implementation.

Keep infrastructure alongside the project

Most application stacks use Terraform. The repo contains the instance, storage, networking and application-specific DNS configuration, so a change to the service can be reviewed alongside the infrastructure it needs.

Newer projects share an S3 backend bucket with a separate state key for each project, such as summon/infra/aws.tfstate. Older stacks have their own state buckets. State locking uses use_lockfile = true, supported by the Terraform S3 backend. The deployment role still needs permissions scoped to the right state objects; different keys alone are not an access boundary.

I also use project prefixes and tags so resource names, runtime roles and parameter paths are recognizable. Terraform state and secret files stay out of Git. My personal site is an exception to the Terraform pattern: its S3 and CloudFront setup currently uses checked-in AWS configuration JSON. Calling the whole account fully managed by Terraform would be inaccurate.

Place workloads by how they run

A recent capacity check showed that most of my EC2 instances used only about 1–5% CPU on average over a week. Memory told a different story. One 1 GB web host briefly reached about 97% memory usage. Looking at CPU alone would have made it seem like a good place to add another app.

The reorganization I am planning has three main groups:

This is a target layout, not a migration I have already completed. The trading system stays on its existing host during that work. Its operating requirements are different from an early web application.

Consolidation will not produce a large bill reduction. The instances are already inexpensive, and some of the apparent spare capacity is CPU rather than RAM. The benefit is having fewer hosts to operate and a clear place for the next small app. Docker Compose is enough for this stage; I do not need a Kubernetes cluster to host a few low-traffic services.

Separate the data from the idea of a server

Static sites and shared objects belong in S3. My personal site uses S3 behind CloudFront, so serving these pages does not require an EC2 instance. Application hosts use encrypted gp3 EBS volumes for their operating systems and local runtime data, including database and container volumes.

A persistent volume is useful, but it is not a backup. EBS survival on instance termination depends on the volume's deletion settings. Moving an app means accounting for its database and uploaded files as well as its container image.

I verified recent application backups in S3 during the audit. That establishes that backup objects exist; it does not establish that every application can be restored. A tested restore and a rollback path belong ahead of retiring the old machine.

A domain is a route, not a machine

I keep a private map of hostname, DNS provider, traffic destination, application owner and intended host group. It distinguishes a static S3/CloudFront site from a domain pointing at a running application, and from a domain used only for email.

This matters because a hostname is not always named after the machine serving it. Several domains can share a host, and a domain can keep the same public address while its application moves behind a proxy or CDN. Before retiring an instance, I need to account for every hostname pointing at it.

DNS ownership and application hosting can change independently. Mail routing is another separate concern. Moving a website is not a reason to replace its MX records or disturb its mail provider.

Make the limits visible

I use CloudWatch for EC2 metrics and a self-hosted Prometheus and Grafana stack for host and service telemetry. Memory and filesystem information need host-level collection; the CloudWatch agent can collect those additional metrics.

For a shared host, I care about available memory, swap activity, disk growth, job duration, request latency and CPU credits. An average CPU percentage does not tell me whether a scheduled job makes the web app slow or whether a build will exhaust memory.

The next steps are fairly practical: verify restores, fill the telemetry gaps, set resource limits, move one workload at a time and check the routes after each move. I am comfortable with a shared host at this stage, provided I understand what it contains and have a way to recover it.