Makefile Patterns: Task Running That Works Everywhere

Make has been around since 1976. It’s installed on virtually every Unix system. And while it was designed for compiling C programs, it’s become a universal task runner for any project. No npm, no pip, no cargo β€” just make. Why Make? Zero dependencies β€” Already on your system Declarative β€” Describe what you want, not how to get it Incremental β€” Only runs what’s needed Self-documenting β€” make help shows available targets Universal β€” Works the same on Linux, macOS, CI systems Basic Structure 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # Variables APP_NAME := myapp VERSION := 1.0.0 # Default target (runs when you just type 'make') .DEFAULT_GOAL := help # Phony targets don't create files .PHONY: build test clean help build: go build -o $(APP_NAME) ./cmd/$(APP_NAME) test: go test ./... clean: rm -f $(APP_NAME) help: @echo "Available targets:" @echo " build - Build the application" @echo " test - Run tests" @echo " clean - Remove build artifacts" Self-Documenting Makefiles The best pattern: automatic help generation from comments. ...

February 24, 2026 Β· 8 min Β· 1544 words Β· Rob Washington

Terraform State Management: Avoiding the Footguns

Terraform state is both essential and dangerous. It’s how Terraform knows what exists, what changed, and what to do. Mismanage it, and you’ll either destroy production or spend hours untangling drift. What State Actually Is State is Terraform’s record of reality. It maps your configuration to real resources: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { "resources": [ { "type": "aws_instance", "name": "web", "instances": [ { "attributes": { "id": "i-0abc123def456", "ami": "ami-12345678", "instance_type": "t3.medium" } } ] } ] } Without state, Terraform would: ...

February 24, 2026 Β· 7 min Β· 1386 words Β· Rob Washington

SSH Tips and Tricks: Beyond Basic Connections

SSH is the workhorse of remote access. But most people only scratch the surface. Here’s how to use it like a pro. SSH Config: Stop Typing So Much Instead of: 1 ssh -i ~/.ssh/prod-key.pem -p 2222 ubuntu@ec2-54-123-45-67.compute-1.amazonaws.com Create ~/.ssh/config: H H H o o o s s s t t t H U P I H U I F P U p o s o d s o s d o r s r s e r e t s e e r . o e o t r t n a t r n w i x r d N t g N t a n y a u 2 i i a d i r t J a m b 2 t n m e t d e u d e u 2 y g e p y A r m m n 2 F l F g n p i e t i s o i e a n c u l t y l n l b 2 e a e t a - g s 5 ~ i ~ y t 4 / n / e i - . g . s o 1 s . s n 2 s e s 3 h x h - / a / 4 p m s 5 r p t - o l a 6 d e g 7 - . i . k c n c e o g o y m - m . k p p e u e y t m . e p - e 1 m . a m a z o n a w s . c o m Now just: ...

February 24, 2026 Β· 8 min Β· 1594 words Β· Rob Washington

Ansible Playbook Patterns: Writing Maintainable Automation

Ansible playbooks start simple and grow complex. A quick server setup becomes infrastructure-as-code for dozens of machines. Here are patterns that keep playbooks maintainable as they scale. Project Structure a β”œ β”œ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”œ β”‚ β”‚ β”‚ β”œ β”‚ β”‚ β”‚ β”” n ─ ─ ─ ─ ─ s ─ ─ ─ ─ ─ i b a i β”œ β”‚ β”‚ β”‚ β”‚ β”” p β”œ β”œ β”” r β”œ β”œ β”” f l n n ─ ─ l ─ ─ ─ o ─ ─ ─ i e s v ─ ─ a ─ ─ ─ l ─ ─ ─ l / i e y e e b n p β”œ β”” s β”œ β”” b s w d s c n p s l t r ─ ─ t ─ ─ o i e a / o g o / e o o ─ ─ a ─ ─ o t b t m i s . r d g k e s a m n t c y u h g β”œ β”” i h g s . e b o x g f / c o r ─ ─ n o r y r a n r g t s o ─ ─ g s m v s e i t u / t u l e e s o s p a w s p r s / n . _ l e . _ s . y v l b y v . y m a . s m a y m l r y e l r m l s m r s l / l v / e r s . y m l ansible.cfg 1 2 3 4 5 6 7 8 9 [defaults] inventory = inventory/production roles_path = roles host_key_checking = False retry_files_enabled = False [ssh_connection] pipelining = True control_path = /tmp/ansible-%%r@%%h:%%p Inventory Patterns YAML Inventory (Preferred) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # inventory/production/hosts.yml all: children: webservers: hosts: web1.example.com: web2.example.com: vars: http_port: 80 databases: hosts: db1.example.com: postgres_version: 15 db2.example.com: postgres_version: 14 loadbalancers: hosts: lb1.example.com: Group Variables 1 2 3 4 5 6 7 8 9 10 11 12 13 # inventory/production/group_vars/all.yml --- ansible_user: deploy ansible_python_interpreter: /usr/bin/python3 ntp_servers: - 0.pool.ntp.org - 1.pool.ntp.org # inventory/production/group_vars/webservers.yml --- nginx_worker_processes: auto nginx_worker_connections: 1024 app_root: /var/www/app Role Structure r β”œ β”‚ β”œ β”‚ β”œ β”‚ β”œ β”‚ β”œ β”‚ β”œ β”‚ β”” o ─ ─ ─ ─ ─ ─ ─ l ─ ─ ─ ─ ─ ─ ─ e s d β”” v β”” t β”” h β”” t β”” f β”” m β”” / e ─ a ─ a ─ a ─ e ─ i ─ e ─ n f ─ r ─ s ─ n ─ m ─ l ─ t ─ g a s k d p e a i u m / m s m l m l n s s / m n l a a / a e a a g / s a x t i i i r i t i l i / s n n n s n e n - n / . . . / . s x p . y y y y / . a y m m m m c r m l l l l o a l n m f s . . j c 2 o n # # # # # f # D R T H J R e o a a i o f l s n n l a e k d j e u l a l v e e 2 m t a n r e r t s t t v i r e a a a y ( m d r b r p a i l p e l t a e o s a a b s i t t l n a e a e ( t r s n s h t d i ( g s d l h e e o r p w p v e e r i n s i c d t o e e r s n p i , c r t i i y e e o ) t s r c i . t ) y ) defaults/main.yml 1 2 3 4 5 6 --- # Overridable defaults nginx_worker_processes: auto nginx_worker_connections: 768 nginx_keepalive_timeout: 65 nginx_server_tokens: "off" tasks/main.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 --- - name: Install nginx ansible.builtin.apt: name: nginx state: present update_cache: true become: true - name: Configure nginx ansible.builtin.template: src: nginx.conf.j2 dest: /etc/nginx/nginx.conf owner: root group: root mode: '0644' become: true notify: Reload nginx - name: Enable and start nginx ansible.builtin.systemd: name: nginx enabled: true state: started become: true handlers/main.yml 1 2 3 4 5 6 7 8 9 10 11 12 --- - name: Reload nginx ansible.builtin.systemd: name: nginx state: reloaded become: true - name: Restart nginx ansible.builtin.systemd: name: nginx state: restarted become: true Task Patterns Idempotent Tasks 1 2 3 4 5 6 7 8 9 10 # Good - idempotent - name: Ensure user exists ansible.builtin.user: name: deploy state: present groups: [sudo, docker] # Avoid - not idempotent - name: Add user ansible.builtin.command: useradd deploy Conditional Execution 1 2 3 4 5 6 7 8 9 10 11 - name: Install package (Debian) ansible.builtin.apt: name: nginx state: present when: ansible_os_family == "Debian" - name: Install package (RedHat) ansible.builtin.dnf: name: nginx state: present when: ansible_os_family == "RedHat" Loops 1 2 3 4 5 6 7 8 9 10 - name: Create users ansible.builtin.user: name: "{{ item.name }}" groups: "{{ item.groups }}" state: present loop: - { name: alice, groups: [developers] } - { name: bob, groups: [developers, sudo] } loop_control: label: "{{ item.name }}" # Cleaner output Blocks for Error Handling 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 - name: Deploy application block: - name: Pull latest code ansible.builtin.git: repo: "{{ app_repo }}" dest: "{{ app_root }}" version: "{{ app_version }}" - name: Install dependencies ansible.builtin.pip: requirements: "{{ app_root }}/requirements.txt" virtualenv: "{{ app_root }}/venv" - name: Run migrations ansible.builtin.command: cmd: "{{ app_root }}/venv/bin/python manage.py migrate" chdir: "{{ app_root }}" rescue: - name: Notify on failure ansible.builtin.debug: msg: "Deployment failed, rolling back" - name: Rollback to previous version ansible.builtin.git: repo: "{{ app_repo }}" dest: "{{ app_root }}" version: "{{ previous_version }}" always: - name: Restart application ansible.builtin.systemd: name: myapp state: restarted Variable Precedence From lowest to highest priority: ...

February 24, 2026 Β· 8 min Β· 1621 words Β· Rob Washington

DNS Debugging: Finding Why Your Domain Isn't Working

β€œIt’s always DNS” is a meme because it’s usually true. When something network-related breaks, DNS is the first suspect. Here’s how to investigate. The Essential Tools dig - The DNS Swiss Army Knife 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # Basic lookup dig example.com # Specific record type dig example.com MX dig example.com TXT dig example.com CNAME # Short output (just the answer) dig +short example.com # Query a specific nameserver dig @8.8.8.8 example.com # Trace the full resolution path dig +trace example.com nslookup - Quick and Simple 1 2 3 4 5 6 7 8 9 10 # Basic lookup nslookup example.com # Query specific server nslookup example.com 8.8.8.8 # Interactive mode nslookup > set type=MX > example.com host - Even Simpler 1 2 3 4 5 # Basic lookup host example.com # Specific record type host -t MX example.com Common DNS Problems Problem: Domain Not Resolving Symptoms: NXDOMAIN or SERVFAIL errors ...

February 24, 2026 Β· 6 min Β· 1132 words Β· Rob Washington

Background Job Patterns: Processing Work Outside the Request Cycle

Some work doesn’t belong in a web request. Sending emails, processing uploads, generating reports, syncing with external APIs β€” these tasks are too slow, too unreliable, or too resource-intensive to run while a user waits. Background jobs solve this by moving work out of the request cycle and into a separate processing system. The Basic Architecture β”Œ β”‚ β”” ─ ─ ─ W ─ ─ e ─ ─ b ─ ─ ─ β”‚ β”‚ β”” ─ A ─ ─ ─ p ─ ─ ─ p ─ ─ ─ ─ ─ ─ ─ ─ ┐ β”‚ β”˜ ─ ─ ─ ─ ─ ─ β–Ά ─ β”Œ β”‚ β”” β–Ά ─ ─ β”Œ β”‚ β”” ─ R ─ ─ ─ ─ e ─ ─ Q ─ ─ s ─ ─ u ─ ─ u ─ ─ e ─ ─ l ─ ─ u ─ ─ t ─ ─ e ─ ─ s ─ ─ ─ ─ ─ ┐ β”‚ β”˜ ┐ β”‚ β”˜ ─ β—€ ─ ─ ─ ─ ─ ─ β–Ά ─ β”Œ β”‚ β”” ─ ─ ─ ─ ─ W ─ ─ ─ o ─ ─ ─ r ─ β”‚ β”˜ ─ k ─ β”‚ ─ e ─ ─ r ─ ─ s ─ ─ ─ ─ ─ ┐ β”‚ β”˜ Producer: Web app enqueues jobs Queue: Stores jobs until workers are ready Workers: Process jobs independently Results: Optional storage for job outcomes Choosing a Queue Backend Redis (with Sidekiq, Bull, Celery) 1 2 3 4 5 6 7 8 9 # Celery with Redis from celery import Celery app = Celery('tasks', broker='redis://localhost:6379/0') @app.task def send_email(user_id, template): user = get_user(user_id) email_service.send(user.email, template) Pros: Fast, simple, good ecosystem Cons: Not durable by default (can lose jobs on crash) ...

February 24, 2026 Β· 7 min Β· 1300 words Β· Rob Washington

LLM API Integration Patterns: Building Reliable AI Features

LLM APIs are deceptively simple: send a prompt, get text back. But building reliable AI features requires handling rate limits, managing costs, structuring outputs, and gracefully degrading when things go wrong. Here are the patterns that work in production. The Basic Client Start with a wrapper that handles common concerns: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import os import time from typing import Optional import anthropic from tenacity import retry, stop_after_attempt, wait_exponential class LLMClient: def __init__(self): self.client = anthropic.Anthropic() self.default_model = "claude-sonnet-4-20250514" self.max_tokens = 4096 @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=60) ) def complete( self, prompt: str, system: Optional[str] = None, model: Optional[str] = None, max_tokens: Optional[int] = None ) -> str: messages = [{"role": "user", "content": prompt}] response = self.client.messages.create( model=model or self.default_model, max_tokens=max_tokens or self.max_tokens, system=system or "", messages=messages ) return response.content[0].text The tenacity library handles retries with exponential backoff β€” essential for rate limits and transient errors. ...

February 24, 2026 Β· 6 min Β· 1104 words Β· Rob Washington

API Versioning Strategies: Breaking Changes Without Breaking Clients

Your API will change. Features will be added, mistakes will be corrected, and sometimes you’ll need to break things. The question isn’t whether to version β€” it’s how. Why Version? Breaking changes are changes that make existing clients fail: Removing a field from a response Changing a field’s type (string β†’ integer) Requiring a new parameter Changing the meaning of a value Removing an endpoint Without versioning, every change risks breaking someone’s integration. With versioning, you can evolve the API while giving clients time to adapt. ...

February 24, 2026 Β· 7 min Β· 1384 words Β· Rob Washington

Log Aggregation Pipelines: From Scattered Files to Searchable Insights

When you have one server, you SSH in and grep the logs. When you have fifty servers, that stops working. Log aggregation is how you make β€œwhat happened?” answerable at scale. The Pipeline Architecture Every log aggregation system follows the same basic pattern: β”Œ β”‚ β”” ─ ─ ─ S ─ ─ o ─ ─ u ─ ─ r ─ β”‚ β”‚ β”” ─ c ─ ─ ─ e ─ ─ ─ s ─ ─ ─ ─ ─ ┐ β”‚ β”˜ ─ ─ ─ ─ ─ ─ ─ β–Ά β–Ά β”Œ β”‚ β”” β”Œ β”‚ β”” ─ ─ ─ ─ ─ C ─ ─ ─ ─ o ─ ─ Q ─ ─ l ─ ─ u ─ ─ l ─ ─ e ─ ─ e ─ ─ r ─ ─ c ─ ─ y ─ ─ t ─ ─ ─ ─ ─ ─ ─ ┐ β”‚ β”˜ ┐ β”‚ β”˜ ─ β—€ ─ ─ ─ ─ β–Ά ─ β”Œ β”‚ β”” ─ ─ ─ ─ ─ P ─ ─ ─ r ─ ─ ─ o ─ ─ ─ c ─ ─ ─ e ─ ─ ─ s ─ ─ ─ s ─ ─ ─ ─ ─ ┐ β”‚ β”˜ ─ ─ ─ ─ ─ ─ ─ β–Ά ─ β”Œ β”‚ β”” ─ ─ ─ ─ ─ ─ ─ ─ S ─ ─ ─ t ─ ─ ─ o ─ β”‚ β”˜ ─ r ─ β”‚ ─ e ─ ─ ─ ─ ─ ┐ β”‚ β”˜ Each stage has choices. Let’s walk through them. ...

February 24, 2026 Β· 10 min Β· 1999 words Β· Rob Washington

Configuration Management Principles: Making Deployments Predictable

Most production incidents I’ve debugged came down to configuration. A missing environment variable. A wrong database URL. A feature flag stuck in the wrong state. Code was fine; configuration was the problem. Configuration management is the unsexy work that prevents those 3 AM pages. The Core Principles 1. Separate Configuration from Code Configuration should never be baked into your application binary or container image. Wrong: 1 2 # Hardcoded in code DATABASE_URL = "postgres://prod:password@db.example.com/myapp" Also wrong: ...

February 24, 2026 Β· 7 min Β· 1321 words Β· Rob Washington