<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.3.3">Jekyll</generator><link href="https://blog.thomaswimprine.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://blog.thomaswimprine.com/" rel="alternate" type="text/html" hreflang="en" /><updated>2025-12-10T20:03:41+00:00</updated><id>https://blog.thomaswimprine.com/feed.xml</id><title type="html">Cloudy with a Chance of Tech</title><subtitle>&quot;Tom&apos;s Guide to Building Smart Homes with Cloud Computing and Home Assistant.&quot;
</subtitle><entry><title type="html">Initial Lessons Learned with Agentic Programming</title><link href="https://blog.thomaswimprine.com/blog/2025-10-09-Initial-Lessons-Learned-with-Agentic-Programming/" rel="alternate" type="text/html" title="Initial Lessons Learned with Agentic Programming" /><published>2025-10-09T00:00:00+00:00</published><updated>2025-12-10T20:02:35+00:00</updated><id>https://blog.thomaswimprine.com/blog/Initial-Lessons-Learned-with-Agentic-Programming</id><content type="html" xml:base="https://blog.thomaswimprine.com/blog/2025-10-09-Initial-Lessons-Learned-with-Agentic-Programming/"><![CDATA[<p>It’s honestly not a secret—we are finally getting technology to where I imagined back in the early 90s when I was in high school. I remember having discussions with my friends about the potentials and problems we would face when we got here. It’s been an interesting journey to watch, but now we’re here and what do we do with this technology I had dreamed about for thirty years? We do cool things!</p>

<p>When ChatGPT announced their LLM to the world, I immediately started using it to write code. I’m huge into automation and have a list of projects and ideas I would like to get executed. Problem is I’m one measly weak human that requires sleep, food and such… I would open the console, ask a question, get a response or a code snippet, test, iterate, test, iterate, test, rinse and repeat. I would get useful code sometimes, but other times I would ask for something resembling a simple sandwich and was served with a 12-course meal and all the fixings but not a sandwich in sight. Exceptionally exciting if you want to see what it can do, but not very useful when you have a detailed task list and a deadline.</p>

<p>So began my journey to figure out what actually works.</p>

<h2 id="phase-1---prompt-engineering">Phase 1 - Prompt Engineering</h2>

<p>Initially we believed that the key to making this work was to improve the prompt. It’s a computer after all and it’s supposed to follow instructions, right? If it’s making a mistake it’s probably because I wasn’t clear enough or left a detail out, something…</p>

<p>So we ended up writing long detailed prompts with examples, gotchas, lists of do/don’t actions, etc. everything we could think of to make sure the system knew what we wanted.</p>

<p>This only got us so far. It worked a bit but definitely didn’t give the results we needed and certainly wouldn’t ship production-ready code. We also noticed that the agents would have a tendency to overcomplicate simple solutions—the wheelbarrow becomes a tricycle big rig. It works but definitely not what you need, way too complicated and a city-sized mountain of technical debt right out the gate.</p>

<p><strong>Not the solution.</strong></p>

<h2 id="phase-2---gatesguardrails">Phase 2 - Gates/Guardrails</h2>

<p>As a systems admin I want to trust the process and remove the human element as much as possible. I also believe that people, if given enough opportunity, will find ways around constraints. So we took a different approach: we don’t let the agent do the thing—we build a process that ensures it does the thing correctly, so the process has permission but the agent doesn’t.</p>

<p>That didn’t work out. The agent would work and realize there is a guardrail and spend the time and tokens trying to circumvent the git-hooks rather than fixing the linting problems. It would spend more time trying to get around the guardrail than actually doing the work.</p>

<p><strong>Not the solution.</strong></p>

<h2 id="phase-3---structure">Phase 3 - Structure</h2>

<p>A little digression first:</p>

<p>Jocko Willink has a great podcast and one of the things he talks about is “Discipline Equals Freedom.” The principles are outlined in his book <a href="https://amzn.to/4o7prhr">Discipline Equals Freedom: Field Manual Mk1-MOD1</a>. The more structure you have, the more freedom you have to operate. This is counter-intuitive to most people as they think structure is confining and limiting. It’s not, it’s liberating.</p>

<p>We started to figure out that if we would give the agent smaller focused tasks <em>within a clear process</em> and definitions of success, it would stay on track and deliver a bit better results.</p>

<p>Not perfect but better! Definitely making progress.</p>

<h2 id="phase-4---process">Phase 4 - Process</h2>

<p>The key to the “Structure” component was the process. We needed to define for the agent a clear step-by-step process to follow. This doesn’t just include actions but also intent. If the agent is aware of the intent and final result, it’s better able to make decisions about the required steps to keep moving towards the goal and not take a hard left into hallucination land.</p>

<p>This is the part that made all the difference. We decided to develop a simple framework built around three fundamental questions:</p>

<ul>
  <li><strong>What’s important to you and your team?</strong></li>
  <li><strong>What does success look like?</strong></li>
  <li><strong>What is the definition of done?</strong></li>
</ul>

<p>We found that these three simple questions help the agent understand the context of the task and what is required to complete it. Everything else is supporting information, but these questions are essential for making the right decisions.</p>

<p>This was about the time Claude Code was released and we were actually able to leverage the agents directly and not have to use ChatGPT as a middleman with a lot of copy/paste. (I was behind a bit.) This also allowed for the use of slash-commands. Leveraging this functionality, we were able to develop a practical workflow.</p>

<h2 id="the-evolution-from-failure-to-success">The Evolution: From Failure to Success</h2>

<p>Here’s a visual representation of the journey through these four phases:</p>

<pre><code class="language-mermaid">%%{init: {'theme':'base', 'themeVariables': { 'fontSize':'14px'}}}%%
graph TB
    Start([Start: Agentic Programming Journey]) --&gt; Phase1

    %% Phase 1: Prompt Engineering
    Phase1[🔴 Phase 1: Prompt Engineering&lt;br/&gt;Long detailed prompts with examples,&lt;br/&gt;gotchas, do/don't lists]
    Phase1 --&gt; Result1{Result}
    Result1 --&gt;|❌ FAILED| Problem1[Problem: Overcomplicated solutions&lt;br/&gt;Wheelbarrow → Tricycle Big Rig&lt;br/&gt;Too much technical debt]
    Problem1 --&gt; Phase2

    %% Phase 2: Gates/Guardrails
    Phase2[🔴 Phase 2: Gates/Guardrails&lt;br/&gt;Build process with guardrails&lt;br/&gt;to ensure correct execution]
    Phase2 --&gt; Result2{Result}
    Result2 --&gt;|❌ FAILED| Problem2[Problem: Agent circumvented guardrails&lt;br/&gt;Spent time fighting git-hooks&lt;br/&gt;instead of doing work]
    Problem2 --&gt; Phase3

    %% Phase 3: Structure
    Phase3[🟡 Phase 3: Structure&lt;br/&gt;Smaller focused tasks within&lt;br/&gt;clear process &amp; success definitions]
    Phase3 --&gt; Result3{Result}
    Result3 --&gt;|⚠️ BETTER| Problem3[Better but not perfect&lt;br/&gt;Making progress!]
    Problem3 --&gt; Phase4

    %% Phase 4: Process - The Solution
    Phase4[🟢 Phase 4: Process&lt;br/&gt;Three Key Questions]
    Phase4 --&gt; Questions{What's important?&lt;br/&gt;What does success look like?&lt;br/&gt;What is definition of done?}

    Questions --&gt; Workflow[Three-Step Workflow]

    %% Step 1: Draft PRP
    Workflow --&gt; Step1[Step 1: /draft-prp command]
    Step1 --&gt; Draft[Create detailed draft document&lt;br/&gt;20-150k tokens]
    Draft --&gt; Sections[Sections:&lt;br/&gt;• Constraints&lt;br/&gt;• Requirements&lt;br/&gt;• Definitions&lt;br/&gt;• Success Criteria&lt;br/&gt;• Definition of Done&lt;br/&gt;• Stories/Tasks&lt;br/&gt;• Acceptance Criteria&lt;br/&gt;• Test Cases]
    Sections --&gt; Review{Review &amp;&lt;br/&gt;Validate Draft}
    Review --&gt;|Needs Changes| Step1
    Review --&gt;|✅ Approved| Step2

    %% Step 2: Generate PR
    Step2[Step 2: /generate-prp command]
    Step2 --&gt; Breakdown[Break large document into&lt;br/&gt;smaller discrete PRs]
    Breakdown --&gt; Step3[Step 3: /execute-prp command]
    Step3 --&gt; Execute[Execute individual PRs&lt;br/&gt;with quality gates]
    Execute --&gt; Success[✅ SUCCESS: Production-Ready Code]

    %% Styling
    classDef failedPhase fill:#ffcccc,stroke:#cc0000,stroke-width:3px
    classDef betterPhase fill:#ffffcc,stroke:#ccaa00,stroke-width:3px
    classDef successPhase fill:#ccffcc,stroke:#00cc00,stroke-width:3px
    classDef processBox fill:#e6f3ff,stroke:#0066cc,stroke-width:2px
    classDef doneBox fill:#d4edda,stroke:#28a745,stroke-width:3px

    class Phase1,Phase2,Result1,Result2,Problem1,Problem2 failedPhase
    class Phase3,Result3,Problem3 betterPhase
    class Phase4,Questions,Workflow successPhase
    class Step1,Draft,Sections,Review,Step2,Breakdown,Step3,Execute processBox
    class Success doneBox
</code></pre>

<h2 id="the-three-step-workflow-that-actually-works">The Three-Step Workflow That Actually Works</h2>

<p>Once we understood that process was the key, we built a workflow that embodies those three fundamental questions. Here’s how it works in practice:</p>

<h3 id="step-1-create-the-draft-prp">Step 1: Create the Draft PRP</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/draft-prp Create a webpage with a blue background and white text that says <span class="s2">"Hello World"</span>
</code></pre></div></div>

<p>Our <code class="language-plaintext highlighter-rouge">/draft-prp</code> command is a custom prompt that includes the requirements for the task. It lays out the process, definitions, and context needed. This creates a draft Product Requirements Proposal (PRP) that is placed in the <code class="language-plaintext highlighter-rouge">./prp/drafts</code> directory for review.</p>

<p>This document will contain all the sections and requirements you’ve defined such as:</p>
<ul>
  <li>Constraints</li>
  <li>Requirements</li>
  <li>Definitions</li>
  <li>Success Criteria</li>
  <li>Definition of Done</li>
  <li>Stories/Tasks</li>
  <li>Acceptance Criteria</li>
  <li>Test Cases</li>
</ul>

<p>Yes, all of that! The more structure the better. This will create a large document but it’s worth it (20-150k tokens). Review the document and make sure it meets your needs. If not, update the doc or work with the agents to adjust. This is your starting point so make sure it’s complete and correct.</p>

<h3 id="step-2-generate-individual-prs">Step 2: Generate Individual PRs</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/generate-prp &lt;draft-filename&gt;
</code></pre></div></div>

<p>This command takes that detailed draft and breaks it down into smaller, more manageable pieces. If you noticed, the document that was created is very detailed, includes a lot of information and is a bit overwhelming to implement as one monolithic task. This step creates individual PRs for each discrete unit of work.</p>

<p>Out of that 100k token file we will break it into individual PRs that are easier to manage, review and implement. Remember, the point is to accomplish the work and keep the agent focused, so smaller discrete tasks are better than large complex ones where the agent <em>will</em> get lost.</p>

<p>I’ve configured my system to organize them using this pattern:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>###-[a-z]-###-task-title.md
[PRP #]-[Major Task ID]-[Sequence Number]-[Task Title].md
</code></pre></div></div>

<p>So now we have a directory of PRs that are ready to be worked on:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./prp/active/333-a-001-create-index-html.md
./prp/active/333-a-002-create-styles-css.md
./prp/active/333-a-003-create-tests.md
etc...
</code></pre></div></div>

<p>You need to figure out your system and process that works for you, but this is a good starting point.</p>

<h3 id="step-3-execute-the-pr">Step 3: Execute the PR</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/execute-prp &lt;pr-filename&gt;
</code></pre></div></div>

<p>This is where the magic happens. The agent will read the PR, understand the requirements and make the changes to your codebase. It will also run any tests, linters or actions you have configured to make sure the code is correct.</p>

<p>If the PR is completed successfully it will move the PR to the <code class="language-plaintext highlighter-rouge">./prp/completed</code> directory and commit and push the change. If there are any issues it will move the PR to the <code class="language-plaintext highlighter-rouge">./prp/failed</code> directory and leave the PR open for review.</p>

<p>Now let’s dive into what’s actually happening behind the scenes during execution.</p>

<h2 id="behind-the-scenes-what-actually-happens-during-execute-prp">Behind the Scenes: What Actually Happens During <code class="language-plaintext highlighter-rouge">/execute-prp</code></h2>

<p>When you run <code class="language-plaintext highlighter-rouge">/execute-prp</code>, you’re not just executing a script—you’re launching a sophisticated multi-agent orchestration system that coordinates 12+ specialized AI agents to deliver production-ready code. Here’s what happens behind the scenes.</p>

<h3 id="the-12-phase-pipeline">The 12-Phase Pipeline</h3>

<p>The execution follows a carefully choreographed sequence where each agent specializes in one domain and hands off to the next:</p>

<h4 id="phase-1-4-planning--design">Phase 1-4: Planning &amp; Design</h4>
<ul>
  <li><strong>Project Manager</strong> coordinates the entire pipeline and tracks progress</li>
  <li><strong>Architect Reviewer</strong> designs the system architecture and validates design patterns</li>
  <li><strong>Security Reviewer</strong> performs threat modeling before any code is written</li>
  <li><strong>Compliance Officer</strong> ensures regulatory and standards compliance (NIST, WCAG, etc.)</li>
</ul>

<h4 id="phase-5-9-implementation--validation">Phase 5-9: Implementation &amp; Validation</h4>
<ul>
  <li><strong>Developer Agents</strong> (language-specific) implement features using strict TDD</li>
  <li><strong>Test Automation</strong> validates test quality and coverage</li>
  <li><strong>Performance Profiler</strong> benchmarks and optimizes critical paths</li>
  <li><strong>API Designer</strong> validates API contracts and endpoints</li>
  <li><strong>Database Administrator</strong> handles all data layer concerns</li>
</ul>

<h4 id="phase-10-12-documentation--deployment">Phase 10-12: Documentation &amp; Deployment</h4>
<ul>
  <li><strong>Documentation Writer</strong> creates README, API docs, ADRs, and deployment guides</li>
  <li><strong>Deployment Manager</strong> configures infrastructure and deployment pipelines</li>
  <li><strong>Code Management</strong> creates the pull request with comprehensive description</li>
</ul>

<p>Each agent is a specialist. The Python developer doesn’t touch security. The security reviewer doesn’t write database schemas. This separation ensures expertise at every step.</p>

<h3 id="the-tdd-engine-red-green-refactor">The TDD Engine: RED-GREEN-REFACTOR</h3>

<p>Here’s where things get interesting. Every developer agent is <strong>enforced</strong> to follow Test-Driven Development:</p>

<ol>
  <li><strong>RED</strong>: Write a failing test first (proves the feature doesn’t exist yet)</li>
  <li><strong>GREEN</strong>: Write minimal code to make the test pass</li>
  <li><strong>REFACTOR</strong>: Clean up the code while keeping tests green</li>
</ol>

<p>The system <strong>blocks</strong> any commit that doesn’t follow this pattern. You can’t skip straight to implementation. This ensures:</p>
<ul>
  <li>Every feature has tests from day one</li>
  <li>Tests actually validate behavior (not just passing by accident)</li>
  <li>Code remains maintainable through refactoring</li>
</ul>

<p>This is real TDD, not “tests eventually” or “we’ll add tests later” development.</p>

<h3 id="quality-gates-that-actually-enforce-quality">Quality Gates That Actually Enforce Quality</h3>

<p>Between each phase, automated gates validate the work. These aren’t suggestions—they’re enforced requirements:</p>

<h4 id="test-coverage-gate">Test Coverage Gate</h4>
<ul>
  <li>Requires 100% coverage (lines, branches, functions, statements)</li>
  <li>No “we’ll add tests later”—the code won’t merge without them</li>
  <li>Enforced by automated tooling, not trust</li>
</ul>

<h4 id="mutation-testing-gate">Mutation Testing Gate</h4>
<ul>
  <li>Validates that tests actually catch bugs (not just execute code)</li>
  <li>Requires ≥95% mutation score</li>
  <li>Prevents weak tests that just achieve coverage numbers without actually validating behavior</li>
</ul>

<p>For those unfamiliar with mutation testing: the system deliberately introduces bugs into your code (changing <code class="language-plaintext highlighter-rouge">+</code> to <code class="language-plaintext highlighter-rouge">-</code>, inverting boolean conditions, etc.) and ensures your tests catch them. If your tests still pass when the code is broken, they’re not really testing anything useful. Mutation testing catches this.</p>

<h4 id="production-ready-gate">Production-Ready Gate</h4>
<ul>
  <li><strong>Zero stubs allowed</strong>: No <code class="language-plaintext highlighter-rouge">pass</code>, no <code class="language-plaintext highlighter-rouge">NotImplementedError</code>, no “TODO: implement this”</li>
  <li><strong>Complete error handling</strong>: Every external call wrapped in try/catch with meaningful errors</li>
  <li><strong>Comprehensive logging</strong>: Entry/exit logging, error logging, state changes tracked</li>
  <li><strong>No unresolved TODOs</strong>: Either implement it or link to a future issue</li>
</ul>

<h4 id="security-gate">Security Gate</h4>
<ul>
  <li>No hardcoded secrets or credentials</li>
  <li>Encryption requirements validated</li>
  <li>Auth/authorization patterns verified</li>
  <li>Vulnerability scan passes</li>
</ul>

<p>These gates aren’t optional. They’re enforced by the system and you can’t merge without passing them all.</p>

<h3 id="what-you-get-at-the-end">What You Get at the End</h3>

<p>After all 12 phases complete, you receive:</p>

<p><strong>1. Production-Ready Code</strong></p>
<ul>
  <li>100% test coverage (not aspirational—enforced)</li>
  <li>Zero stub implementations</li>
  <li>Complete error handling and logging</li>
  <li>≥95% mutation score</li>
  <li>Security-validated</li>
  <li>Performance-benchmarked</li>
</ul>

<p><strong>2. Comprehensive Documentation</strong></p>
<ul>
  <li>README with installation, usage, and examples</li>
  <li>API documentation (if applicable)</li>
  <li>Architecture Decision Records (ADRs)</li>
  <li>Deployment guides with infrastructure setup</li>
  <li>Security documentation</li>
</ul>

<p><strong>3. Quality Reports</strong></p>
<ul>
  <li>Coverage report (<code class="language-plaintext highlighter-rouge">coverage/index.html</code>)</li>
  <li>Mutation testing results (<code class="language-plaintext highlighter-rouge">mutation/results.html</code>)</li>
  <li>Performance benchmarks</li>
  <li>Security scan results</li>
  <li>Accessibility audit (if UI-related)</li>
</ul>

<p><strong>4. Pull Request</strong></p>
<ul>
  <li>Comprehensive PR description with context</li>
  <li>All changes explained with rationale</li>
  <li>Test results embedded</li>
  <li>Ready for human review</li>
</ul>

<p><strong>5. Complete Audit Trail</strong></p>
<ul>
  <li>Every agent action logged</li>
  <li>Every decision documented</li>
  <li>Every test result recorded</li>
  <li>Full traceability from requirement to implementation</li>
</ul>

<h3 id="the-human-still-owns-the-decision">The Human Still Owns the Decision</h3>

<p>Here’s the crucial part: <code class="language-plaintext highlighter-rouge">/execute-prp</code> creates a <strong>pull request</strong>, not a direct commit. The system delivers production-ready code, but <strong>you still review and merge</strong>. This keeps humans in control while automating the grunt work.</p>

<p>The PR description includes:</p>
<ul>
  <li>What was built and why</li>
  <li>Design decisions made</li>
  <li>Test coverage summary</li>
  <li>Performance characteristics</li>
  <li>Security considerations</li>
  <li>Breaking changes (if any)</li>
</ul>

<p>You review it like any other PR, except it’s already passed 6+ quality gates and has 100% test coverage.</p>

<h3 id="why-this-matters">Why This Matters</h3>

<p><strong>Traditional development:</strong> Developer writes code → manually writes tests → hopes they caught everything → ships and prays.</p>

<p><strong>Agentic development:</strong> PRP defines requirements → 12 agents orchestrate delivery → every quality gate passes → human reviews and merges → ship with confidence.</p>

<p>The difference? <strong>You can’t skip quality</strong>. The gates enforce it. You can’t ship incomplete code. The production-ready validator blocks it. You can’t merge without tests. The coverage gate prevents it.</p>

<p>It’s not about AI replacing developers—it’s about AI handling the tedious parts (tests, docs, quality checks) so developers can focus on architecture, design, and strategic decisions.</p>

<p>Next time you run <code class="language-plaintext highlighter-rouge">/execute-prp</code>, you’ll know there are 12 agents working in parallel, following strict TDD, enforcing quality gates, and delivering code that’s actually production-ready—not “hope it works” ready.</p>

<h2 id="lessons-learned-from-four-phases-to-one-framework">Lessons Learned: From Four Phases to One Framework</h2>

<p>Looking back at this journey, the pattern becomes clear:</p>

<p><strong>Phase 1 (Prompt Engineering)</strong> taught us that more words don’t equal better results. The agent doesn’t need a novel—it needs structure.</p>

<p><strong>Phase 2 (Gates/Guardrails)</strong> showed us that fighting the agent is counterproductive. Guardrails without context create adversarial relationships where the agent spends energy circumventing controls instead of doing work.</p>

<p><strong>Phase 3 (Structure)</strong> was the breakthrough that smaller, focused tasks within clear processes work better. But we were still missing something.</p>

<p><strong>Phase 4 (Process)</strong> brought it all together. The three questions—What’s important? What does success look like? What is the definition of done?—combined with the three-step workflow (draft, generate, execute) created a framework that actually delivers.</p>

<p>The key insight? <strong>Agents need the same things humans need to do good work</strong>: clear objectives, defined success criteria, and a process to follow. The difference is that with agents, we can enforce quality gates that humans might be tempted to skip under pressure.</p>

<p>This process has worked well for us and we are able to get a lot of work done effectively. The agents stay focused and deliver quality code that meets our requirements because the process keeps them on track.</p>

<p>We’re still learning and iterating, but this framework has proven solid. If you’re struggling with agentic programming, consider whether you’re giving your agents structure and process, or just hoping better prompts will fix everything.</p>

<p>The technology I dreamed about in the 90s is finally here. Now we know how to use it effectively.</p>

<!--
EDITOR'S NOTES - MAJOR CHANGES MADE:

1. **CRITICAL FIX**: Changed Step 2 command from `/draft-prp <draft-filename>` to `/generate-prp <draft-filename>` - this was a factual error that would confuse readers trying to follow the workflow.

2. **Improved Flow**:
   - Added transitional phrases between sections ("So began my journey...", "Once we understood that process was the key...", "Now let's dive into what's actually happening...")
   - Created smooth bridge from high-level process to technical deep-dive
   - Added subheadings within the technical section for better readability

3. **Tone Consistency**:
   - Maintained conversational but professional tone throughout
   - Removed overly casual "we do cool stuff!!!" in favor of "we do cool things!"
   - Kept personal voice while ensuring technical credibility

4. **Fixed Mermaid Diagram**:
   - Changed "Two-Step Workflow" to "Three-Step Workflow" to match the actual process
   - Added Step 3 (/execute-prp) to the diagram flow
   - Maintained visual consistency

5. **Enhanced Technical Accuracy**:
   - Clarified mutation testing with concrete explanation
   - Emphasized TDD enforcement vs suggestion
   - Made quality gates more concrete with specific thresholds
   - Fact-checked against Exa research on TDD and mutation testing

6. **Strengthened Conclusion**:
   - Tied back to all four phases explicitly
   - Summarized key lessons from each phase
   - Ended with callback to the opening (90s dreams)
   - Reinforced the core insight about structure and process

7. **Improved Structure**:
   - Added descriptive subheadings ("The 12-Phase Pipeline", "The TDD Engine", etc.)
   - Created clearer section breaks
   - Made scanning easier for readers

8. **Maintained Author's Voice**:
   - Kept personal anecdotes (high school dreams, systems admin perspective)
   - Preserved enthusiasm and practical tone
   - Maintained Jocko Willink reference and Discipline Equals Freedom example

9. **Fixed Minor Issues**:
   - Consistent quote style (straight quotes in body text)
   - Improved code block formatting
   - Better emphasis on key terms (bold/italic usage)

The article now flows coherently from personal journey through technical implementation to practical lessons learned, while maintaining accuracy and the author's distinctive voice.
-->]]></content><author><name>Thomas Wimprine</name><email>thomas@thomaswimprine.com</email></author><category term="ai" /><category term="development" /><summary type="html"><![CDATA[My journey through four phases of learning to work effectively with AI agents—from failed attempts at prompt engineering to discovering a process that actually delivers production-ready code.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.thomaswimprine.com/assets/img/blog/2025-10-09-GnomesLearningAgenticProgramming_blog.png" /><media:content medium="image" url="https://blog.thomaswimprine.com/assets/img/blog/2025-10-09-GnomesLearningAgenticProgramming_blog.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Why Build a MUD in 2025?</title><link href="https://blog.thomaswimprine.com/blog/2025-10-06-Building-a-MUD-in-2025/" rel="alternate" type="text/html" title="Why Build a MUD in 2025?" /><published>2025-10-06T00:00:00+00:00</published><updated>2025-12-10T20:02:35+00:00</updated><id>https://blog.thomaswimprine.com/blog/Building-a-MUD-in-2025</id><content type="html" xml:base="https://blog.thomaswimprine.com/blog/2025-10-06-Building-a-MUD-in-2025/"><![CDATA[<p>Multi-User Dungeons (MUDs) are text-first, real-time multiplayer worlds that predate modern MMOs. In 2025, building a MUD might seem contrarian, but it’s exactly the kind of project that rewards strong engineering, clean contracts, and a relentless focus on player experience. I’m building one as both a learning lab and a playable product.</p>

<p>This post lays out the “why”: the technical leverage, the design space, and the practical upside of shipping a modern MUD today.</p>

<h2 id="tldr">TL;DR</h2>
<ul>
  <li>Low cost to build, host, and scale—perfect for iterative development.</li>
  <li>Forces great systems design: protocols, state, persistence, and concurrency.</li>
  <li>Ideal sandbox for applied AI: agents, content tooling, and testing.</li>
  <li>Accessible UX: works everywhere, inclusive for screen readers, low bandwidth.</li>
  <li>Recaptures the fun of tight feedback loops that many modern games have lost.</li>
</ul>

<h2 id="what-im-building">What I’m Building</h2>
<p>I’m working on a networked MUD in Go with a modular architecture: dedicated services for auth, world simulation, NPCs/agents, economy, and observability. It’s engineered for clarity and testability, not just nostalgia.</p>

<ul>
  <li>Repo (private for now): ~/Repositories/mud</li>
  <li>Language: Go</li>
  <li>Shape: service-oriented core with focused modules (auth, energy, NPCs, network, advertising)</li>
  <li>Tests: high coverage with per-module coverage reports and profiling artifacts</li>
  <li>Deployment: Kubernetes; local dev + CI; S3 + CloudFront for web docs</li>
</ul>

<p>This series will cover the design and evolution of the system—starting with the “why,” then the network and world simulation, then tools and content.</p>

<h2 id="why-a-mud-still-makes-sense">Why a MUD Still Makes Sense</h2>

<h3 id="1-systems-thinking-with-instant-feedback">1) Systems Thinking with Instant Feedback</h3>
<p>MUDs require you to design clear interfaces between networking, world state, persistence, and <a href="/glossary/#llm-large-language-model">AI</a>. There’s nowhere to hide: strong contracts and repeatable tests win. Small mistakes are visible instantly—and fixable just as fast.</p>

<p>What you learn transfers cleanly to backend engineering, multiplayer infrastructure, and agent systems.</p>

<h3 id="2-agentic-ai-is-better-in-text-first-worlds">2) Agentic AI Is Better in Text-First Worlds</h3>
<p>Text-first worlds are a natural habitat for <a href="/glossary/#llm-large-language-model">AI</a> agents:</p>
<ul>
  <li>Observations are structured text; actions are tokens; no rendering pipeline required.</li>
  <li>You can mix <a href="/glossary/#goap-goal-oriented-action-planning">GOAP</a>/<a href="/glossary/#behavior-tree-bt">BT</a> planners with <a href="/glossary/#llm-large-language-model">LLM</a> tooling for dynamic behaviors.</li>
  <li>Content generation is safer and cheaper: descriptions, quests, items, chatter.</li>
  <li>Simulation speed is limited by CPU and your update loop, not frame rate.</li>
</ul>

<p>The result: richer NPCs, faster iteration, and better telemetry for evaluation.</p>

<h3 id="3-accessibility-and-reach">3) Accessibility and Reach</h3>
<p>MUDs run anywhere—terminal, web, mobile SSH client (I’m not enabling telnet for security)—and are friendly to screen readers. Latency tolerance is high, bandwidth use is tiny, and players can drop in for 5 minutes without a 10 GB patch. That’s a superpower.</p>

<h3 id="4-operates-well-on-a-small-budget">4) Operates Well on a Small Budget</h3>
<p>The stack is lean and predictable. A single modest server can host thousands of concurrent connections when the code is efficient. It’s the right canvas for one person (or a small team) to ship something real.<br />
I’m known for overengineering things, so this is running on a Kubernetes cluster with network policies, lots of observability, and workflows—but it could easily be pared down to a single VM or even a serverless function.</p>

<h3 id="5-teaches-the-hard-partssafely">5) Teaches the Hard Parts—Safely</h3>
<p>You’ll touch concurrency, protocols, persistence, migrations, observability, and liveops. You’ll also build tooling: map editors, item pipelines, NPC behavior fixtures. All of this pays dividends on any backend system.</p>

<h2 id="architecture-notes">Architecture Notes</h2>
<p>I’m leveraging agents and LLMs for development, but I believe the future is in learning to build with these systems using a systematic methodology. This allows for stretch goals and learning that would have been otherwise unachievable. This project is also about what makes architecture and design so important, interesting, reliable, and predictable.</p>

<p><strong>Key principles I’m following:</strong></p>
<ul>
  <li><a href="/glossary/#contract-first">Contract-first</a>: define message formats and service boundaries up front.</li>
  <li><a href="/glossary/#deterministic-core">Deterministic core</a>: world simulation tick with clear ordering and isolation.</li>
  <li><a href="/glossary/#observability-of-secrets-workflow">Observability</a>: logs, metrics, and traces around network, AI, and economy.</li>
  <li><a href="/glossary/#test-harness">Test harnesses</a>: soak tests for performance; fuzz tests for protocol edges.</li>
  <li><a href="/glossary/#zero-trust-mindset">Zero-trust mindset</a>: auth, rate limiting, and resource isolation per subsystem.</li>
</ul>

<p><strong>Planned posts will include:</strong></p>
<ol>
  <li><a href="/glossary/#network-protocol">Network protocol</a> and <a href="/glossary/#session-lifecycle">session lifecycle</a></li>
  <li><a href="/glossary/#world-tick">World tick</a>, <a href="/glossary/#ecs-entity-component-system">ECS</a>-like entities, and <a href="/glossary/#state-persistence">state persistence</a></li>
  <li><a href="/glossary/#npc-non-player-character">NPCs</a> and agent behaviors (rule-based + <a href="/glossary/#llm-large-language-model">LLM</a>-assisted)</li>
  <li><a href="/glossary/#economy-design">Economy design</a> and <a href="/glossary/#anti-exploit-checks">anti-exploit checks</a></li>
  <li>Tooling, <a href="/glossary/#profiling">profiling</a>, and <a href="/glossary/#deploys">deploys</a></li>
</ol>

<h2 id="what-success-looks-like">What “Success” Looks Like</h2>
<ul>
  <li>A stable, well-documented server with clean contracts.</li>
  <li>A small, active player base enjoying tight loop gameplay.</li>
  <li>A flexible AI layer that’s fun to extend and evaluate.</li>
  <li>Reusable components applicable to other multiplayer/back-end work.</li>
  <li>A series of posts sharing what I learned along the way.</li>
  <li>API access for bots and extensions. (ReadOnly)</li>
</ul>

<h2 id="call-for-collaborators---maybe">Call for collaborators - Maybe?!?</h2>
<p>Interested in systems, networking, or AI behaviors? I’m sure I would love discussing ideas and approaches. I don’t plan on opening the repo as it is a personal project, but if you want to help, look around or kick the tires when it’s ready, ping me or just stay tuned!</p>

<p>– Cheers</p>]]></content><author><name>Thomas Wimprine</name><email>thomas@thomaswimprine.com</email></author><category term="development" /><category term="game-dev" /><category term="networking" /><category term="ai" /><category term="go" /><summary type="html"><![CDATA[Multi-User Dungeons (MUDs) are text-first, real-time multiplayer worlds that predate modern MMOs. In 2025, building a MUD might seem contrarian, but it’s exactly the kind of project that rewards strong engineering, clean contracts, and a relentless focus on player experience. I’m building one as both a learning lab and a playable product.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.thomaswimprine.com/assets/img/blog/2025-10-06-GnomesStartingToPlayArthermoor_blog.png" /><media:content medium="image" url="https://blog.thomaswimprine.com/assets/img/blog/2025-10-06-GnomesStartingToPlayArthermoor_blog.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Lab Update</title><link href="https://blog.thomaswimprine.com/blog/2025-07-07-Lab-Update/" rel="alternate" type="text/html" title="Lab Update" /><published>2025-07-07T00:00:00+00:00</published><updated>2025-12-10T20:02:35+00:00</updated><id>https://blog.thomaswimprine.com/blog/Lab-Update</id><content type="html" xml:base="https://blog.thomaswimprine.com/blog/2025-07-07-Lab-Update/"><![CDATA[<p>This is a short update on the lab status and what I’ve been working on. Life’s been busy recently
and posting hasn’t been a priority. I’m hoping that will change soon as I’m pursuing opportunities 
that will allow more exposure to the communities I’ve loved and missed over the years.</p>

<h1 id="hardware">Hardware</h1>
<p>Really same as before - I haven’t had the need to upgrade until recently when I’ve started actual 
development on projects.</p>
<ul>
  <li><a href="https://store.ui.com/us/en/category/cloud-gateways-large-scale/products/udm-pro">Ubiquiti Router</a></li>
  <li>Cisco Switches</li>
  <li>pfSense (BGP)</li>
  <li><a href="https://tinyurl.com/lenovotiny">Lenovo Tiny Desktops</a>
    <ul>
      <li>Dedicated to Kubernetes Master Nodes, freeing me from relying on my Pi Clusters.</li>
      <li>One system runs the automation systems via HomeAssistant.</li>
    </ul>
  </li>
  <li><a href="https://amzn.to/3IvIWkY">Raspberry Pi CM4 Cluster</a> (6 nodes, 8GB RAM each, NVMe storage)</li>
  <li><a href="https://amzn.to/432Tlvh">DeskPi Super6C Raspberry Pi CM4 Cluster Board</a></li>
  <li>Raspberry Pi 4 - Solely handling DNS duties.</li>
  <li>Synology NAS</li>
</ul>

<h2 id="differences">Differences</h2>
<ul>
  <li><em>Networking</em>
    <ul>
      <li>BGP handles the Kubernetes routing</li>
      <li>pfSense is providing the BGP services</li>
      <li>Default gateway routes the designated networks to the pfSense gateway address. These load balancer IPs are dynamically provisioned by the pfSense.</li>
    </ul>
  </li>
  <li>Storage
    <ul>
      <li>The Raspberry Pi are all sporting NVMe for stateful-set storage</li>
      <li>Half the Raspberry Pi have internal eMMC storage</li>
      <li>Half are using SD Cards (Don’t do this)</li>
    </ul>
  </li>
</ul>

<h1 id="software">Software</h1>
<ul>
  <li>OS: <em>Talos Linux</em></li>
  <li>Networking: <em>Cilium</em></li>
  <li>Storage: <em>local-path-provider</em></li>
  <li>Network Monitoring: <em>Hubble-UI</em></li>
  <li>Monitoring: <em>Prometheus</em></li>
  <li>Reporting: <em>Grafana</em></li>
  <li>Secrets: <em>Vault</em></li>
</ul>

<p>A few YouTube videos, bit of reading about Talos and I knew I wanted to learn more. It literally had 
 everything I wanted:</p>
<ul>
  <li>API access</li>
  <li>Required config management (if you’re doing it right, please don’t just apply random patches, use the pipeline)</li>
  <li>Security by default</li>
</ul>

<p>If I was going to learn how to leverage kubernetes I may as well learn it right!</p>

<p>So I wiped the functional Debian kub cluster and started from scratch… It was more difficult than I thought
 and I brought a lot of the pain upon myself. A few issues getting the Pi configured properly, a desire to PXE boot 
 to realize my setup wouldn’t work as configured. Hardware failures, changing jobs, moving homes, get everything setup again,
 buy new SD card (128GB), they are all bad… Flash what I have and start building the cluster.</p>

<h1 id="mistakes">Mistakes</h1>
<ul>
  <li>SD Cards too small
    <ul>
      <li>I needed to use what I had and the Talos OS is pretty small. I’m using NVMe for the persistent storage so I’m good! <em>WRONG!</em> I forgot about ephemeral storage! The pods that don’t have stateful-sets still need storage to operate and this goes in the ephemeral storage. Since I needed to dedicate the entire drive to the local-path-provisioner it only had what was left on the SD card for ephemeral storage. This meant my pods are beating the hell out of the SD cards on half the worker nodes. This is less than ideal and led to an idea for expansion…</li>
    </ul>
  </li>
  <li>Didn’t plan out the networking
    <ul>
      <li>I was trying to get everything working and assigned an entire /24 subnet to the loadbalancer and it worked! CHEERS! I went to bed. Later I came back and realized that wasn’t what I wanted. I had to go back and plan out the environments like I wanted them and touch it a second time. Now they are configured and everything is working as planned. More of a personal ‘damn it’ than a mistake but I’m learning and should have put some thought into it beforehand.</li>
      <li>Why do I want to separate my networking environments? It’s a lab that I’m trying to simulate an enterprise environment with while having minimal hardware. Network separation is simple and effective if you have proper network controls. I would like to eventually migrate this to leverage VLANs but I’ve got mixed results when testing with the Ubiquiti and MY Cisco gear. It’s on the list but not a priority until the expansion plans.</li>
    </ul>
  </li>
  <li>Hardware
    <ul>
      <li>The Pi CM4 are handling my current load without an issue - I’m actually quite impressed! My issue is the networking. It’s a hardware limitation that each of the CM4 connect to a 1GB switch which has one 1GB external port. My control nodes are x64 systems and connected to 1GB ports each. While everything can communicate the network is… less than reliable right now.</li>
      <li>Control Nodes are older systems and it certainly shows. They need to be replaced and I would like to expand the cluster with additional Pi nodes to do that with. They are efficient and do the job.</li>
    </ul>
  </li>
  <li>Configuration - Jeez, here are the big ones…
    <ul>
      <li>I’m learning as I go so first thing I did is change the cluster name. While this can be overcome and may be a normal practice, it’s not something you should do when learning.</li>
      <li>I tried to get MetalLB working on the talos nodes. It worked on the Debian cluster and I really didn’t get the security setup of Talos. MetalLB requires more access than I was comfortable trying to configure, but it started me down the BGP path. So, it all worked out.</li>
      <li>Try and initialize, unlock, join-nodes and configure auto-unlock in a single step. Why?!? Well like I said I wanted this to be a production grade, security first cluster so I don’t want Vault to be locked if the cluster or pod goes down. I certainly want it in Production mode, using Raft and clustered. Like I said, secure and production grade. I currently have everything working from bootstrap to unlocked and ready for use, but I haven’t revisited the auto-unlock yet.</li>
    </ul>
  </li>
</ul>

<h1 id="things-to-finish">Things to Finish</h1>
<p>They are actually running just not configured. They are also going to be collecting the stats from the HomeAssistant installation.</p>
<ul>
  <li>Prometheus</li>
  <li>Grafana</li>
  <li>HomeAssistant Integration - I want to trigger events or start pods/apps based on external inputs. These plans are still being developed and have not yet been researched.</li>
  <li>BDR - Not if, When! I have this setup and now it needs to protect it, and the workloads. I want to do this two ways:
    <ul>
      <li><em>Local</em>: Backup to NFS, both the Kubernetes components (etcd, configs, secrets, etc.) and the applications. While snapshots are great in general they aren’t in this environment, so I’m going to require other solutions.</li>
      <li><em>Remote</em>: I’m going to leverage StorJ for the remote option for backup. Why? Well it’s a lot less expensive than S3, client side encrypted, interesting project and I don’t need compliance in my lab.</li>
    </ul>
  </li>
  <li>DHCP
    <ul>
      <li>I’m currently using DHCP off my Ubiquiti router and it’s not very… feature rich currently when it comes to these features. I’m already using BIND for DNS and would like to migrate to Kea for DHCP. This will allow me to start trying to leverage PXE which will bring a new new game to the lab!</li>
    </ul>
  </li>
  <li>PXE
    <ul>
      <li>Currently the systems are booting from an SD card located underneath the cluster board. If it’s mounted they can’t be accessed. If the SD card accidentally gets wiped from say forgetting to specify the partition. You need to bring the entire cluster down, disassemble, remove and flash the SD, reverse. Less than ideal for a lab, when this is configured it removes that dependency and now the SD card can just be ephemeral storage.</li>
    </ul>
  </li>
</ul>

<h1 id="things-i-would-do-differently">Things I would do differently</h1>
<p>This is a loaded idea because there’s not much I would really keep the same.</p>
<ul>
  <li>I would do a lot more planning now that I know what to look for in a general sense. That would reduce the number of mistakes, changes, confusing configurations and general cruft of the system. I am going to cut myself a little slack since this is the first cluster leveraging Talos and learning the general workings. There’s a lot more but I learn by doing so I’m definitely going to break something.</li>
  <li>Different Hardware: I would <em>NOT</em> use the cluster board for what I’m trying to do. While it’s completely functional for learning and small projects, for my use case it’s not appropriate. I would replace the cluster board with individual PoE Pi boards or blades. Boot from PXE (preferred) or SD card, eMMC for ephemeral storage and NVMe for stateful-sets.</li>
</ul>

<h1 id="current-uses">Current Uses</h1>
<p>I’m writing cloud first apps leveraging Kubernetes and just like the cluster they are a continuous work in progress but a great learning platform. How to leverage different components, proper secret management, pipeline development and checks, etc.</p>

<h2 id="the-stack">The Stack</h2>
<p><em>Database</em>: MongoDB stateful-set</p>

<p><em>Caching</em>: Redis</p>

<p><em>Frontend</em>: Streamlit and FastAPI/Kong</p>

<p><em>Monitoring</em>: Prometheus</p>

<p><em>Reporting</em>: Grafana</p>

<p><em>Security</em>: Vault</p>

<p><em>Pipeline</em>: GitHub Actions</p>

<p><em>Runners</em>: Local</p>

<h2 id="current-development">Current Development</h2>
<p><em>Health &amp; Safety App</em></p>

<p><em>Agent Analysis App</em></p>

<p><em>Agent Development Company</em> (Ambitious but it’s fun and am learning lots)</p>

<h1 id="immediate-plans">Immediate Plans</h1>
<p>I’m going to need to expand this soon and this is one of the things I think is great about Talos. I’m leveraging KubeSpan and am going to configure autoscaling into GCP (price - it’s always price). This creates a WireGuard VPN across the nodes of the cluster encrypting all traffic in-flight. This allows me to extend the cluster into multiple cloud providers as needed leveraging the least expensive resource possible, dynamically. This also allows me to seamlessly protect against a major single cloud provider failure. Leveraging Terraform I’ll be able to maintain standards and change control across multiple providers. This is an awesome win!</p>

<h1 id="less-immediate-plans">Less Immediate Plans</h1>
<p>Significant upgrades to the entire <em>“datacenter”</em>, as financing permits:</p>
<ul>
  <li>10G redundant networking (Storage and Master Nodes)</li>
  <li>ProxMox Nodes for Ceph storage, Talos Master Node VMs &amp; x64 workloads/pods</li>
  <li>Raspberry Pi Blades or other compact units with POE and storage options (Primary worker nodes)</li>
  <li>PXE Boot all nodes (ARM &amp; X64)</li>
  <li>HomeAssistant Integrations</li>
</ul>

<p>Anyway I think that’s enough rambling, but that’s the status of the lab and why it is in its current state. I’m looking forward to working on these projects and hope to transition into working this type of stack full-time. I’m excited about the possibilities, where it can lead and I’m certainly not short on ideas of how to leverage this in so many settings.</p>

<p>Thanks for sticking around
Cheers</p>]]></content><author><name>Thomas Wimprine</name><email>thomas@thomaswimprine.com</email></author><category term="kubernetes" /><category term="networking" /><category term="talos" /><category term="vault" /><summary type="html"><![CDATA[Update on the lab setup and what I've been working on recently.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.thomaswimprine.com/assets/img/blog/Kub_gnomes_building_cluster_blog.jpg" /><media:content medium="image" url="https://blog.thomaswimprine.com/assets/img/blog/Kub_gnomes_building_cluster_blog.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Home Lab Setup</title><link href="https://blog.thomaswimprine.com/blog/2024-09-30-Home-Lab-Setup/" rel="alternate" type="text/html" title="Home Lab Setup" /><published>2024-09-30T00:00:00+00:00</published><updated>2025-12-10T20:02:35+00:00</updated><id>https://blog.thomaswimprine.com/blog/Home-Lab-Setup</id><content type="html" xml:base="https://blog.thomaswimprine.com/blog/2024-09-30-Home-Lab-Setup/"><![CDATA[<p>As I’ve hinted in previous posts, my home lab serves as a playground for testing and development, allowing me to experiment with a variety of hardware and configurations. Here’s a peek under the hood at the hardware running the show:</p>
<ul>
  <li><a href="https://store.ui.com/us/en/category/all-cloud-gateways/products/udm-pro">Ubiquiti Router</a><a href="https://amzn.to/4h3PfZQ">Amazon Link</a></li>
  <li>Cisco Switches - Cisco (Because you can never go wrong with reliability.)</li>
  <li><a href="https://tinyurl.com/lenovotiny">Lenovo Tiny Desktops</a>
    <ul>
      <li>Dedicated to Kubernetes Master Nodes, freeing me from relying on my Pi Clusters.</li>
      <li>One system runs the automation systems via HomeAssistant.</li>
    </ul>
  </li>
  <li><a href="https://amzn.to/42rIYAW">Raspberry Pi CM4 Cluster</a> (6 nodes, 8GB RAM each, NVMe storage)</li>
  <li><a href="https://amzn.to/432Tlvh">DeskPi Super6C Raspberry Pi CM4 Cluster Board</a></li>
  <li>Raspberry Pi 4 - Solely handling DNS duties.</li>
  <li>Synology NAS - It’s currently hanging around but soon to be retired.
All the systems are statically assigned IPv4 addresses, and DHCP/DNS records are maintained using Ansible (Stay tuned—I’ll be releasing the role soon!). Basic configurations—like NTP settings, package installations, updates, and user setups—are also automated with Ansible.</li>
</ul>

<p>Here’s how I typically bring a new system online:</p>
<ol>
  <li>Flash the system image (using PI Imager or a custom Debian image with a predefined hostname and user account).</li>
  <li>Boot up and assign a static IP via the router.</li>
  <li>Add the system to the /etc/hosts file on my workstation.</li>
  <li>Update the Ansible inventory.yml to reflect the new system’s role—DNS names only, no IP addresses.</li>
  <li>Use ssh-copy-id to transfer SSH keys to the new system (FQDN all the way).</li>
  <li>Verify SSH is set up and functioning properly.</li>
  <li>Run the necessary Ansible playbooks:
    <ul>
      <li>Master Playbook</li>
      <li>DNS Playbook</li>
    </ul>
  </li>
</ol>

<p>By this point, the new system should be ready, configured to the baseline, and potentially even further if its role calls for it (e.g., becoming a Kubernetes worker node).
Most of my systems are headless, and I rarely need to interact with them directly. They’re here for testing and workload purposes, so unless I need to tweak a configuration or investigate something, they’re largely hands-off. These servers run themselves, allowing me to focus on the more interesting stuff.
Cheers!</p>]]></content><author><name>Thomas Wimprine</name><email>thomas@thomaswimprine.com</email></author><category term="development" /><category term="pi" /><summary type="html"><![CDATA[In this post, I offer an overview of my home lab setup, detailing the hardware and systems I use for testing and development. From Ubiquiti routers to Raspberry Pi clusters, I explain how each component is configured and managed using Ansible for automation. I also walk through my process for bringing new systems online and maintaining a hands-off, streamlined environment.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.thomaswimprine.com/assets/img/blog/fibreConnections.jpg" /><media:content medium="image" url="https://blog.thomaswimprine.com/assets/img/blog/fibreConnections.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Wireguard Vpn Ansible</title><link href="https://blog.thomaswimprine.com/blog/2023-12-26-WireGuard-VPN-Ansible/" rel="alternate" type="text/html" title="Wireguard Vpn Ansible" /><published>2023-12-26T00:00:00+00:00</published><updated>2025-12-10T20:02:35+00:00</updated><id>https://blog.thomaswimprine.com/blog/WireGuard-VPN-Ansible</id><content type="html" xml:base="https://blog.thomaswimprine.com/blog/2023-12-26-WireGuard-VPN-Ansible/"><![CDATA[<p>title: Create WireGuard VPN with Ansible
tags: [ansible, security, vpn, devops]
author: twimprine
layout: post</p>

<p>image:
  path: /assets/img/blog/Ansible_Wireguard_blog.jpg
  srcset:
    1920w: /assets/img/blog/Ansible_Wireguard_blog@0,5x.jpg
    960w: /assets/img/blog/Ansible_Wireguard_blog@0,25x.jpg
    480w: /assets/img/blog/Ansible_Wireguard_blog@0,125x.jpg</p>

<hr />

<h2 id="create-wireguard-vpn-with-ansible">Create WireGuard VPN with Ansible</h2>

<p>Currently we are using <a href="https://www.cisco.com">Cisco AnyConnect</a> for our VPN solution.  It works well, but it is a bit of a pain to install and configure.  We are retiring the environment that the ASA is located in and it’s time to retire this piece of equipment since it doesn’t fit into our current infrastructure or future plans.</p>

<p>We are a fully remote company and as a result all of our resources and assets are also in the cloud. That said we would like to control access to our resources and have some ability to quickly revoke access if needed. Since we may have multiple endpoints or environments we would need to connect to we would like it to be simple and flexible. Additionally we would like to reduce expenses as much as possible so open-source is certainly preferred.</p>

<p>That said the simple and honest choices were between <a href="https://openvpn.net">OpenVPN</a> and <a href="https://wireguard.com">WireGuard</a>. Discussions with the technical resources and managers quickly settled on WireGuard. WireGuard is simple, lightweight, easy to configure and troubleshoot and has wide acceptance in the market. We can have multiple configurations on a single system without conflict. It’s simple enough to deploy and manage that if we needed to have multiple environments it’s pretty straight forward.</p>

<p>But we can always do better!</p>

<h3 id="scenario">Scenario</h3>
<p>Here is the basic scenario:</p>
<ul>
  <li>We are a small firm and we have a small presence in a co-located datacenter and we need to give our employees access to the datacenter resources. Management does not want us to use the resources provided by the datacenter to terminate the VPN due to cost, however we are able to spin up as many virtual machines as we want without incurring any additional cost.</li>
</ul>

<p>Seems simple enough and this is what we’re going to work through. We have a bunch of remote users that need to connect to the datacenter and we don’t have any real central method of authentication here so using files is acceptable to this company’s risk profile.</p>

<h4 id="why-vpn">WHY VPN?</h4>
<p>So it’s the end of 2023 and I’m about to explain why a VPN is important. People don’t understand the costs or risks of being spied on, either in a corporate sense or personal sense either. This is worth its own post which I’ll link here when I get to it :)</p>
<ul>
  <li>Corporate: We want to protect our customer’s data and prevent internet snoops<sup id="fnref:snoops" role="doc-noteref"><a href="#fn:snoops" class="footnote" rel="footnote">1</a></sup> from being able to read or prioritize our private data.</li>
  <li>Personal: We don’t want to be a larger target for advertising, data theft, man-in-the-middle attacks, etc. Using a VPN reduces the risks of a lot of these, even at home. Using a VPN will keep your ISP from being able to tell where you like to shop, what you like to watch, what you’re searching. Additionally, in some people’s lives it’s a matter of personal security.</li>
  <li>Government: I live in the United States where currently (Dec, 2023) we are not exceptionally concerned with government persecuting us for our political views however, if you’re in a country where this is a problem a VPN may be a life-line.</li>
</ul>

<h3 id="planning">Planning</h3>
<p>Generally when developing a playbook you’re going to complete a series of steps that would normally be completed on the commandline, (Linux assumptions here) so I like to run through those steps to build out my playbook(s) or role(s).</p>

<ol>
  <li>
    <p>Configure Server</p>

    <ul>
      <li>
        <p>Install Software (WireGuard package)</p>
      </li>
      <li>
        <p>Create server config file</p>
      </li>
      <li>
        <p>Create WireGuard service</p>
      </li>
      <li>
        <p>Configure service to start on boot</p>
      </li>
    </ul>
  </li>
  <li>
    <p>Create Client Configurations</p>

    <ul>
      <li>
        <p>Create list of users</p>
      </li>
      <li>
        <p>Create client configuration</p>
      </li>
    </ul>
  </li>
  <li>
    <p>TEST</p>

    <ul>
      <li>
        <p>TEST</p>
      </li>
      <li>
        <p>TEST</p>
      </li>
      <li>
        <p>TEST</p>
      </li>
    </ul>
  </li>
  <li>
    <p>Start playbook development</p>
  </li>
</ol>

<p>As you can tell we are just running through the process right now to make sure everything works as planned. We aren’t building the playbook or writing any code just doing the job. Take copious amounts of notes while doing this and make sure you’ve gotten every step. That is going to be the template you write the playbook from.<sup id="fnref:key" role="doc-noteref"><a href="#fn:key" class="footnote" rel="footnote">2</a></sup></p>

<h3 id="doing">Doing</h3>

<h4 id="create-wireguard-server">Create WireGuard Server</h4>
<ol>
  <li>
    <p>Install WireGuard and firewall</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nb">install </span>wireguard ufw <span class="nt">-y</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p>Create your WireGuard keys</p>

    <p>Create the private key with <code class="language-plaintext highlighter-rouge">wg genkey</code> which will output the private key. This private key is the input for the next step which derives the public key. <code class="language-plaintext highlighter-rouge">echo &lt;key&gt; | wg pubkey</code></p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wg genkey
GO/b82NFwTXApR1CzO2MHtwYg0qWSpGgGgO//GgL5Xo<span class="o">=</span>  <span class="c"># Create and use your own keys!</span>

<span class="nb">echo </span>GO/b82NFwTXApR1CzO2MHtwYg0qWSpGgGgO//GgL5Xo<span class="o">=</span> | wg pubkey
6Gj/JTnJjfFnqnAIA8l6pr718rfEGYK94G9RttzTUwE<span class="o">=</span>
</code></pre></div>    </div>

    <p>These are the private key and public key you will need for the server. The clients get the public key while the server retains the private key in the configuration file. These values will eventually be stored in the ansible vault so we can start creating that yml file now.</p>
  </li>
  <li>
    <p>(Side Quest) Create vault.yml file</p>

    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">server</span><span class="pi">:</span>
  <span class="na">private_key</span><span class="pi">:</span> <span class="s">GO/b82NFwTXApR1CzO2MHtwYg0qWSpGgGgO//GgL5Xo=</span>
  <span class="na">public_key</span><span class="pi">:</span> <span class="s">6Gj/JTnJjfFnqnAIA8l6pr718rfEGYK94G9RttzTUwE=</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p>Create the server config file <code class="language-plaintext highlighter-rouge">/etc/wireguard/wg0.conf</code></p>

    <div class="language-conf highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[<span class="n">Interface</span>]
<span class="n">Address</span> = <span class="m">172</span>.<span class="m">20</span>.<span class="m">100</span> .<span class="m">1</span>/<span class="m">23</span> <span class="c"># Private Address IPv4
</span><span class="n">Address</span> = <span class="n">fd0d</span>:<span class="m">27</span><span class="n">ad</span>:<span class="n">deda</span>::<span class="m">1</span>/<span class="m">64</span> <span class="c"># Private IPv6
</span><span class="n">SaveConfig</span> = <span class="n">false</span>
<span class="n">PostUp</span> = <span class="n">ufw</span> <span class="n">route</span> <span class="n">allow</span> <span class="n">in</span> <span class="n">on</span> <span class="n">wg0</span> <span class="n">out</span> <span class="n">on</span> <span class="n">eth0</span>
<span class="n">PostUp</span> = <span class="n">iptables</span> -<span class="n">t</span> <span class="n">nat</span> -<span class="n">I</span> <span class="n">POSTROUTING</span> -<span class="n">o</span> <span class="n">eth0</span> -<span class="n">j</span> <span class="n">MASQUERADE</span>
<span class="n">PostUp</span> = <span class="n">ip6tables</span> -<span class="n">t</span> <span class="n">nat</span> -<span class="n">I</span> <span class="n">POSTROUTING</span> -<span class="n">o</span> <span class="n">eth0</span> -<span class="n">j</span> <span class="n">MASQUERADE</span>
<span class="n">PreDown</span> = <span class="n">ufw</span> <span class="n">route</span> <span class="n">delete</span> <span class="n">allow</span> <span class="n">in</span> <span class="n">on</span> <span class="n">wg0</span> <span class="n">out</span> <span class="n">on</span> <span class="n">eth0</span>
<span class="n">PreDown</span> = <span class="n">iptables</span> -<span class="n">t</span> <span class="n">nat</span> -<span class="n">D</span> <span class="n">POSTROUTING</span> -<span class="n">o</span> <span class="n">eth0</span> -<span class="n">j</span> <span class="n">MASQUERADE</span>
<span class="n">PreDown</span> = <span class="n">ip6tables</span> -<span class="n">t</span> <span class="n">nat</span> -<span class="n">D</span> <span class="n">POSTROUTING</span> -<span class="n">o</span> <span class="n">eth0</span> -<span class="n">j</span> <span class="n">MASQUERADE</span>
<span class="n">ListenPort</span> = <span class="m">51820</span>
<span class="n">PrivateKey</span> = <span class="n">GO</span>/<span class="n">b82NFwTXApR1CzO2MHtwYg0qWSpGgGgO</span>//<span class="n">GgL5Xo</span>=
</code></pre></div>    </div>
  </li>
  <li>
    <p>Update server networking so it can route and forward packets</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo echo </span>net.ipv4.ip_forward<span class="o">=</span>1 <span class="o">&gt;&gt;</span> /etc/sysctl.conf
<span class="nb">sudo echo </span>net.ipv6.conf.all.forwarding<span class="o">=</span>1 <span class="o">&gt;&gt;</span> /etc/sysctl.conf
<span class="nb">sudo </span>sysctl <span class="nt">-p</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p>Update firewall so it can accept VPN connections and SSH. This block allows the correct traffic through, resets the firewall service then prints out the current status.</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>ufw allow 51820/udp
<span class="nb">sudo </span>ufw allow OpenSSH
<span class="nb">sudo </span>ufw disable
<span class="nb">sudo </span>ufw <span class="nb">enable
sudo </span>ufw status
</code></pre></div>    </div>
  </li>
  <li>
    <p>Start and enable the WireGuard service</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl <span class="nb">enable </span>wg-quick@wg0.service
<span class="nb">sudo </span>systemctl start wg-quick@wg0.service
<span class="nb">sleep </span>10
<span class="nb">sudo </span>systemctl status wg-quick@wg0.service
</code></pre></div>    </div>
  </li>
</ol>

<p>At this point you should get a lot of output but it should be green and show a status of ‘Running’</p>

<p>YAY! We have a server running!</p>

<p>But it can’t accept any connections… 
And we have no clients…</p>

<h4 id="creating-clients">Creating Clients</h4>

<p>Part of what makes WireGuard so easy to use is one o the thngs that makes it difficlyt to adopt. Each configuratin is different and there is no method to distribute or update cient configurations after deployment. We will distribute these files using features of <a href="https://www.vaultproject.io/">HashiCorp’s Vault</a>.</p>

<p>Since we are using ansible to configure the server and it already has the keys for the server let’s leverage that to create the clients files.</p>

<ol>
  <li>
    <p>Review the client config anatomomy</p>

    <div class="language-conf highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[<span class="n">Interface</span>]
<span class="n">PrivateKey</span> = &lt;<span class="n">CLIENT</span> <span class="n">PRIVATE</span> <span class="n">KEY</span>&gt;
<span class="n">Address</span> = &lt;<span class="n">CLIENT</span> <span class="n">STATIC</span> <span class="n">VPN</span> <span class="n">IP</span> <span class="n">ADDRESS</span>&gt;/<span class="m">32</span>
<span class="n">DNS</span> = <span class="m">9</span>.<span class="m">9</span>.<span class="m">9</span>.<span class="m">11</span>, <span class="m">149</span>.<span class="m">112</span>.<span class="m">112</span>.<span class="m">11</span>, <span class="m">2620</span>:<span class="n">fe</span>::<span class="m">11</span>, <span class="m">2620</span>:<span class="n">fe</span>::<span class="n">fe</span>:<span class="m">11</span> <span class="c"># DNS Servers to use when connected
</span>
[<span class="n">Peer</span>]
<span class="n">PublicKEy</span> = &lt;<span class="n">SERVER</span> <span class="n">PUBLIC</span> <span class="n">KEY</span>&gt;
<span class="n">AllowedIPs</span> = <span class="m">0</span>.<span class="m">0</span>.<span class="m">0</span>.<span class="m">0</span>/<span class="m">0</span>, ::/<span class="m">0</span>  <span class="c"># IPs to route through VPN
</span><span class="n">Endpoint</span> = <span class="n">vpn</span>.<span class="n">domain</span>.<span class="n">com</span>:<span class="m">51820</span> <span class="c"># VPN server DNS address
</span></code></pre></div>    </div>
  </li>
  <li>
    <p>Server Updates for client - We need to add the [Peer] Section for each</p>
    <div class="language-conf highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Entry for each peer on server
</span><span class="n">PublicKey</span> = &lt;<span class="n">SERVER</span> <span class="n">PUBLIC</span> <span class="n">KEY</span>&gt;
<span class="n">AllowedIPs</span> = &lt;<span class="n">CLIENT</span> <span class="n">STATIC</span> <span class="n">VPN</span> <span class="n">IP</span> <span class="n">ADDRESS</span>&gt;/<span class="m">32</span>
</code></pre></div>    </div>
  </li>
</ol>

<p>Looking at this we need four pieces of information to make our configs:</p>
<ul>
  <li>Client Private Key</li>
  <li>Client Public Key</li>
  <li>Server Public Key</li>
  <li>IP Address for client when connected to VPN</li>
</ul>

<p>Since all this needs to be retained and can’t change we are going to store this information in ansible vault. The datastructure is simple:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   <span class="na">users</span><span class="pi">:</span>
      <span class="na">ex_user_001</span><span class="pi">:</span>
         <span class="na">key</span><span class="pi">:</span> <span class="s">6AGIjiJIHoas8Ew0vENLJvslbsZGo6d+rerRPK++lUI=</span>
         <span class="na">pubkey</span><span class="pi">:</span> <span class="s">R08mgG3FAfNOMn/qjSUMWO7L83XV7y5IFpjHRg6CKiY=</span>
         <span class="na">number</span><span class="pi">:</span> <span class="m">148</span>

      <span class="na">ex_user_002</span><span class="pi">:</span>
         <span class="na">key</span><span class="pi">:</span> <span class="s">MPl1exnEnE2vAfhUoUKy9BZnWAucUpYtk5HAaTJnDUc=</span>
         <span class="na">pubkey</span><span class="pi">:</span> <span class="s">4q9IpJxBCZ6HRtWbHgogPlDY9G2LvUkmwQNupLmXrlQ=</span>
         <span class="na">number</span><span class="pi">:</span> <span class="m">149</span>
</code></pre></div></div>
<p>This gives us all the information we need to create all of our config entries for the clients. It’s tedious to create this for a bunch of users and keep it straight so I created a shell script to create the users. Execute the script to create the users we will copy the relevant entries into your vault file later.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat</span> /dev/null <span class="o">&gt;</span> new_clients.yml
<span class="nb">echo</span> <span class="s2">"users:"</span>
<span class="k">for </span>i <span class="k">in</span> <span class="o">{</span>1..150<span class="o">}</span><span class="p">;</span> <span class="k">do
    </span><span class="nv">KEY</span><span class="o">=</span><span class="si">$(</span>wg genkey<span class="si">)</span>
    <span class="nv">PUBKEY</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="nv">$KEY</span> | wg pubkey<span class="si">)</span>
    <span class="nb">echo</span> <span class="s2">"  user-</span><span class="nv">$i</span><span class="s2">"</span>: <span class="o">&gt;&gt;</span> new_clients.yml
    <span class="nb">echo</span> <span class="s2">"    key: </span><span class="nv">$KEY</span><span class="s2">"</span> <span class="o">&gt;&gt;</span> new_clients.yml
    <span class="nb">echo</span> <span class="s2">"    pubkey: </span><span class="nv">$PUBKEY</span><span class="s2">"</span> <span class="o">&gt;&gt;</span> new_clients.yml
    <span class="nb">echo</span> <span class="s2">"    number: </span><span class="nv">$i</span><span class="s2">"</span> <span class="o">&gt;&gt;</span> new_clients.yml
    <span class="nb">echo</span> <span class="s2">""</span> <span class="o">&gt;&gt;</span> new_clients.yml
<span class="k">done</span>
</code></pre></div></div>

<h3 id="create-ansible-playbook">Create Ansible Playbook</h3>

<p>Now we have all the steps we need to accomplish to make this work and all the data required.</p>

<ol>
  <li>Create MOTD that tells people this is an Ansible Managed system</li>
  <li>Deploy WireGuard to the server</li>
  <li>Create Server Configurations</li>
  <li>Create Client configs that need to be distributed to clients</li>
</ol>

<ul>
  <li>Secondary Objective: Configure so that it can configure new server from scratch
    <h4 id="create-the-project-directory-structure">Create the project directory structure</h4>
  </li>
</ul>

<p>Create the project directory and the required files. We are also creating and populating the pwfile for ansible vault bucause I know you would never store this in a public location or repo if it wasn’t encrypted right.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">PROJECT_DIR</span><span class="o">=</span>~/wg_project
<span class="nb">mkdir</span> <span class="nt">-P</span> <span class="nv">$PROJECT_DIR</span>/ansible
<span class="nb">mkdir</span> <span class="nt">-p</span> <span class="nv">$PROJECT_DIR</span>/ansible/handlers
<span class="nb">mkdir</span> <span class="nt">-p</span> <span class="nv">$PROJECT_DIR</span>/ansible/templates
<span class="nb">mkdir</span> <span class="nt">-p</span> <span class="nv">$PROJECT_DIR</span>/ansible/vars
<span class="nb">touch</span> <span class="nv">$PROJECT_DIR</span>/ansible/inventory.yml
<span class="nb">touch</span> <span class="nv">$PROJECT_DIR</span>/ansible/handlers/main.yml
<span class="nb">touch</span> <span class="nv">$PROJECT_DIR</span>/ansible/vars/vars.yml
<span class="nb">echo</span> <span class="s2">"ansible/pwfile.txt"</span> <span class="o">&gt;&gt;</span> .gitignore
openssl rand <span class="nt">-base64</span> 32 <span class="o">&gt;</span> <span class="nv">$PROJECT_DIR</span>/ansible/pwfile.txt  <span class="c"># Ansible Vault PW File</span>
</code></pre></div></div>
<h4 id="build-data--ansible-files">Build Data &amp; Ansible Files</h4>
<ol>
  <li>Populate Vault file
Populate the values with what you have previously or generate new values. These values will be used by the templates to create the config files. This keeps us from needing to have the key values in an unsecured code repo.
    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">wireguard_private_key</span><span class="pi">:</span> <span class="s">OPq2MSt6jd48rwz1Rl+xerrNGoD9x5nQwQm3aC15UlM=</span>
<span class="na">wireguard_public_key</span><span class="pi">:</span> <span class="s">Z1lsQcYzaGSrQhm8I9kDOezr+wWjPqJFS1JvuLgjM1A=</span>

<span class="na">users</span><span class="pi">:</span>
   <span class="na">cool-user</span><span class="pi">:</span>
      <span class="na">key</span><span class="pi">:</span> <span class="s">GALbYcYdUZAdj1Vimr/Dgd9+ig+O675jEas8/58GAUA=</span>
      <span class="na">pubkey</span><span class="pi">:</span> <span class="s">PUNU2maOfqX+Z1gCj4BG7pdVjxw7maDK+U/lHQ1nrFM=</span>
      <span class="na">number</span><span class="pi">:</span> <span class="m">15</span>

   <span class="na">bob</span><span class="pi">:</span>
      <span class="na">key</span><span class="pi">:</span> <span class="s">CO2K6Z5hqCOIth5e6DXDzD9mPXhkNadi99ytPNxHR0g=</span>
      <span class="na">pubkey</span><span class="pi">:</span> <span class="s">sTyfg//FYtNeTyUk78ZT0+16E8DhwoAeiPMWUzHa4WI=</span>
      <span class="na">number</span><span class="pi">:</span> <span class="m">46</span>

   <span class="na">jake</span><span class="pi">:</span>
      <span class="na">key</span><span class="pi">:</span> <span class="s">UF2zByFRLA63qj9aZ/ZGAdxrigWPasnfLjLojnPubnI=</span>
      <span class="na">pubkey</span><span class="pi">:</span> <span class="s">dQACpWMf93+JO3oPJqoIchT6UOW8BLBTNe9g5koNUls=</span>
      <span class="na">number</span><span class="pi">:</span> <span class="m">47</span>

   <span class="na">ishmail</span><span class="pi">:</span>
      <span class="na">key</span><span class="pi">:</span> <span class="s">ECu4XIhX9HZdwkshU/lvGQcaCNt4/OBZO9xXfmUaMF4=</span>
      <span class="na">pubkey</span><span class="pi">:</span> <span class="s">uo8iMNSFrQQ/Ynqp3FtBJuRsXgZwqx3luzU+VxMfwUM=</span>
      <span class="na">number</span><span class="pi">:</span> <span class="m">48</span>

   <span class="c1"># Repeat for each user</span>
</code></pre></div>    </div>
  </li>
  <li>Define our inventory file
    <ul>
      <li>We define the group of one so it can be incorporated into larger workflows later</li>
    </ul>

    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">all</span><span class="pi">:</span>
   <span class="na">hosts</span><span class="pi">:</span>
      <span class="na">wireguard-server</span><span class="pi">:</span>
         <span class="na">ansible_host</span><span class="pi">:</span> <span class="s">172.20.200.15</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p>Define the top matter for our playbook in <code class="language-plaintext highlighter-rouge">main.yml</code>.</p>

    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Update and Install WireGuard on Localhost</span>
  <span class="na">hosts</span><span class="pi">:</span> <span class="s">wireguard-server</span>
  <span class="na">become</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="na">gather_facts</span><span class="pi">:</span> <span class="kc">true</span>

  <span class="na">vars_files</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">vars/vars.yml</span>
    <span class="pi">-</span> <span class="s">vars/vault.yml</span>
</code></pre></div>    </div>
    <ul>
      <li>This will only be executed on the WireGuard server(s) not clients as limited to the group <code class="language-plaintext highlighter-rouge">wireguard-server</code> in the inventory file</li>
      <li>We are logging as not root server and will <code class="language-plaintext highlighter-rouge">become root</code> via sudo</li>
      <li>We are going to collect inventory of the host(s)</li>
    </ul>
  </li>
  <li>
    <p>Create the MOTD Template - <code class="language-plaintext highlighter-rouge">templates/motd.j2</code></p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   
   
================================================================================

This system is managed and configured via Ansible.  The Ansible playbooks are
located in the `ansible` directory of the repo.  

Repo: "{{ repo }}"
Last Execution: "{{ ansible_date_time.year }}-{{ ansible_date_time.month }}-{{ ansible_date_time.day }} {{ ansible_date_time.hour }}:{{ ansible_date_time.minute }} UTC"


================================================================================
   
   
</code></pre></div>    </div>
    <ul>
      <li>Update the message as appropriate for your organization</li>
      <li>Ensure the <code class="language-plaintext highlighter-rouge">repo</code> variable is defined - <code class="language-plaintext highlighter-rouge">vars/vars.yaml</code></li>
      <li>Last Execution fills in the values from Ansible</li>
    </ul>
  </li>
  <li>
    <p>Execute the Ansible template to create the MOTD</p>

    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   <span class="na">tasks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Update</span><span class="nv"> </span><span class="s">MOTD"</span>
         <span class="na">ansible.builtin.template</span><span class="pi">:</span>
         <span class="na">src</span><span class="pi">:</span> <span class="s2">"</span><span class="s">motd.j2"</span>
         <span class="na">dest</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/etc/motd"</span>
         <span class="na">owner</span><span class="pi">:</span> <span class="s">root</span>
         <span class="na">group</span><span class="pi">:</span> <span class="s">root</span>
         <span class="na">mode</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0644"</span>
</code></pre></div>    </div>
    <ul>
      <li>name: Defines the name of the task and allows it to be identified if needed</li>
      <li>ansible.builtin.template: this is the full reference to the module we are using</li>
      <li>src:  (required) the filename in the <code class="language-plaintext highlighter-rouge">templates/</code> directory to use</li>
      <li>owner: (required)</li>
      <li>group:  (required)</li>
      <li>mode:  (required) input as string and allow ansible to interpret - hence quotes</li>
    </ul>
  </li>
  <li>
    <p>Install WireGuard and Firewall</p>

    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Install WireGuard and Firewall</span>
     <span class="na">ansible.builtin.apt</span><span class="pi">:</span>
       <span class="na">state</span><span class="pi">:</span> <span class="s">present</span>
       <span class="na">pkg</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">wireguard</span>
        <span class="pi">-</span> <span class="s">ufw</span>
       <span class="na">update_cache</span><span class="pi">:</span> <span class="kc">true</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p>Create the template for the server config - <code class="language-plaintext highlighter-rouge">templates/wg.conf.j2</code></p>

    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      
   <span class="pi">[</span><span class="nv">Interface</span><span class="pi">]</span>
   <span class="s">Address = 172.20.100.1/23</span>
   <span class="s">Address = fd0d:27ad:abcd::1/64</span>
   <span class="s">SaveConfig = </span><span class="kc">false</span>
   <span class="s">PostUp = ufw route allow in on wg0 out on eth0</span>
   <span class="s">PostUp = iptables -t nat -I POSTROUTING -o eth0 -j MASQUERADE</span>
   <span class="s">PostUp = ip6tables -t nat -I POSTROUTING -o eth0 -j MASQUERADE</span>
   <span class="s">PreDown = ufw route delete allow in on wg0 out on eth0</span>
   <span class="s">PreDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE</span>
   <span class="s">PreDown = ip6tables -t nat -D POSTROUTING -o eth0 -j MASQUERADE</span>
   <span class="s">ListenPort = </span><span class="m">51820</span>
   <span class="s">PrivateKey = {{ wireguard_private_key }}</span>


   <span class="s">{% for user_name, user_data in users.items() %}</span>

   <span class="s">[Peer]</span>
   <span class="s"># {{ user_name }}</span>
   <span class="s">PublicKey = {{ user_data.pubkey }}</span>
   <span class="s">Address = 172.20.100.{{ user_data.number }}, fd0d:27ad:abcd::{{ user_data.number }}/128</span>

   <span class="s">{% endfor %}</span>
      
</code></pre></div>    </div>

    <ul>
      <li>This is a copy of the configuration we had prior with the sensitive and repetitive variables swapped out</li>
      <li><code class="language-plaintext highlighter-rouge">wireguard_private_key</code> is in the vault from a prior step</li>
      <li>The data for the users was placed in the vault from a prior step and is looped through creating a [Peer] stanza for each</li>
      <li>user_name: is the id in the data structure in vault</li>
      <li>user_data.pubkey: pubkey in the vault</li>
      <li>user_data.number: defines the IP address of the client since it must be static</li>
    </ul>
  </li>
  <li>Create Ansible step to create config from template
    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   
 <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Create WireGuard Server Config</span>
   <span class="na">ansible.builtin.template</span><span class="pi">:</span>
     <span class="na">src</span><span class="pi">:</span> <span class="s2">"</span><span class="s">wg0.conf.j2"</span>
     <span class="na">dest</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/etc/wireguard/wg0.conf"</span>
     <span class="na">owner</span><span class="pi">:</span> <span class="s">root</span>
     <span class="na">group</span><span class="pi">:</span> <span class="s">root</span>
     <span class="na">mode</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0600"</span>
   <span class="c1"># notify: Restart WireGuard Server</span>
</code></pre></div>    </div>

    <ul>
      <li>Using the fine <code class="language-plaintext highlighter-rouge">templates/wg0.conf.j2</code></li>
      <li>Create the file based off the template in <code class="language-plaintext highlighter-rouge">/etc/wireguard/wg0.conf</code>
<!-- - notify: This notifies the event to restart the service if this step is executed  --></li>
    </ul>
  </li>
  <li>
    <p>Create the Client Config Directory
This needs to be treated reasonably securely - I’m putting it in the directory here as an example but it should really be retained in Vault or someplace similar.</p>

    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Create Client config directory</span>
     <span class="na">ansible.builtin.file</span><span class="pi">:</span>
       <span class="na">dest</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/etc/wireguard/clients"</span>
       <span class="na">owner</span><span class="pi">:</span> <span class="s">root</span>
       <span class="na">group</span><span class="pi">:</span> <span class="s">root</span>
       <span class="na">state</span><span class="pi">:</span> <span class="s">directory</span>
       <span class="na">mode</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0600"</span>

</code></pre></div>    </div>
  </li>
  <li>
    <p>Create the template for the client configs - <code class="language-plaintext highlighter-rouge">templates/client.conf.j2</code></p>

    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      
   <span class="c1"># Configuration for {{ item.key }} ONLY</span>
   <span class="pi">[</span><span class="nv">Interface</span><span class="pi">]</span>
   <span class="s">PrivateKey = {{ item.value.key }}</span>
   <span class="s">Address = 172.20.100.{{ item.value.number }}/32, fd0d:27ad:abcd::{{ item.value.number }}/128</span>

   <span class="s">[Peer]</span>
   <span class="s">PublicKey = {{ wireguard_public_key}}</span>
   <span class="s">AllowedIPs = 0.0.0.0/0, ::/0</span>
   <span class="s">Endpoint = vpn.domain.com:51820</span>


   
</code></pre></div>    </div>
    <p>This loops through the times and creates the file for each of them.</p>
  </li>
  <li>Create Ansible step to create configs from template
    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   
   <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Create Client Configurations</span>
     <span class="na">ansible.builtin.template</span><span class="pi">:</span>
       <span class="na">src</span><span class="pi">:</span> <span class="s2">"</span><span class="s">client.conf.j2"</span>
       <span class="na">dest</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/etc/wireguard/clients/{{</span><span class="nv"> </span><span class="s">item.key</span><span class="nv"> </span><span class="s">}}.conf"</span>
       <span class="na">owner</span><span class="pi">:</span> <span class="s">root</span>
       <span class="na">group</span><span class="pi">:</span> <span class="s">root</span>
       <span class="na">mode</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0600"</span>
     <span class="na">loop</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">lookup('dict',</span><span class="nv"> </span><span class="s">users)</span><span class="nv"> </span><span class="s">}}"</span>
     <span class="na">loop_control</span><span class="pi">:</span>
       <span class="na">label</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">item.key</span><span class="nv"> </span><span class="s">}}"</span>
   
</code></pre></div>    </div>

    <ul>
      <li>This loops through every item of <code class="language-plaintext highlighter-rouge">users</code></li>
      <li>Creates an individual file for each <code class="language-plaintext highlighter-rouge">item</code> with the contents of the template</li>
    </ul>
  </li>
  <li>
    <p>Setting services to start on boot and starting them</p>

    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
   <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Enable wg-quick@wg0.service</span>
      <span class="s">ansible.builtin.systemd</span><span class="err">:</span>
      <span class="na">name</span><span class="pi">:</span> <span class="s">wg-quick@wg0.service</span>
      <span class="na">enabled</span><span class="pi">:</span> <span class="s">yes</span>

   <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Start wg-quick@wg0.service</span>
      <span class="s">ansible.builtin.systemd</span><span class="err">:</span>
      <span class="na">name</span><span class="pi">:</span> <span class="s">wg-quick@wg0.service</span>
      <span class="na">state</span><span class="pi">:</span> <span class="s">started</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p>Check the services</p>

    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   
   <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Check status of wg-quick@wg0.service</span>
      <span class="s">ansible.builtin.command</span><span class="err">:</span>
      <span class="na">cmd</span><span class="pi">:</span> <span class="s">systemctl status wg-quick@wg0.service</span>
      <span class="na">register</span><span class="pi">:</span> <span class="s">service_status</span>

   <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Show service status</span>
      <span class="s">ansible.builtin.debug</span><span class="err">:</span>
      <span class="na">msg</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">service_status.stdout_lines</span><span class="nv"> </span><span class="s">}}"</span>
   
</code></pre></div>    </div>
    <p>Honestly - this is a personal preference that I’m getting into the habit of doing as a final check. This will eventually be put into OpenSearch but that’s a topic for another day. ;)</p>
  </li>
  <li>
    <p>Define Handler(s) <code class="language-plaintext highlighter-rouge">handlers/main.yml</code></p>

    <p>The purpose of the handler is to take some action when called. Multiple tasks can call the handler but it will only execute once when all the actions that call that handler are complete. This can be anything that needs to happen - here we are using it to restart the service.</p>

    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   <span class="s">---</span>
   <span class="s">- name</span><span class="err">:</span> <span class="s">Restart_wireguard</span>
      <span class="s">ansible.builtin.systemd</span><span class="err">:</span>
         <span class="na">name</span><span class="pi">:</span> <span class="s">wg-quick@wg0.service</span>  <span class="c1"># Replace wg0 with your WireGuard interface name if different</span>
         <span class="na">state</span><span class="pi">:</span> <span class="s">restarted</span>
         <span class="na">daemon_reload</span><span class="pi">:</span> <span class="kc">true</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p>Register Handler in playbook <code class="language-plaintext highlighter-rouge">${PROJECT_DIR}/main.yml</code></p>

    <p>In the top matter of the file include the include lines. This tells Ansible where to look for the handlers.</p>
    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
   <span class="na">handlers</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">include</span><span class="pi">:</span> <span class="s">handlers/main.yml</span>
   
</code></pre></div>    </div>
  </li>
</ol>

<h4 id="execute-ansible-playbook">Execute Ansible Playbook</h4>
<ol>
  <li>Encrypt the Vault
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> <span class="nv">$PROJECT_DIR</span>/ansible
ansible-vault encrypt vars/vault.yml <span class="nt">--vault-password-file</span><span class="o">=</span>pwfilt.txt
</code></pre></div>    </div>
  </li>
  <li>Execute Playbook
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ansible-playbook <span class="nt">-i</span> inventory.yml playbook.yml <span class="nt">--vault-password-file</span><span class="o">=</span>pwfile.txt
</code></pre></div>    </div>
  </li>
  <li>
    <p>Output<sup id="fnref:test_server" role="doc-noteref"><a href="#fn:test_server" class="footnote" rel="footnote">3</a></sup></p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ansible-playbook -i inventory.yml main.yml --vault-password-file=pwfile.txt --limit wireguard-test
   [DEPRECATION WARNING]: "include" is deprecated, use include_tasks/import_tasks instead. See https://docs.ansible.com/ansible-core/2.14/user_guide/playbooks_reuse_includes.html for details. This feature will be 
   removed in version 2.16. Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg.

   PLAY [Update and Install WireGuard on Localhost] ******************************************************************************************************************************************************************
   skipping: no hosts matched

   PLAY [Configure WireGuard Server] *********************************************************************************************************************************************************************************

   TASK [Gathering Facts] ********************************************************************************************************************************************************************************************
   ok: [wireguard-test]

   TASK [Update MOTD] ************************************************************************************************************************************************************************************************
   changed: [wireguard-test]

   TASK [Install WireGuard and Firewall] *****************************************************************************************************************************************************************************
   ok: [wireguard-test]

   TASK [Create WireGuard Server Config] *****************************************************************************************************************************************************************************
   ok: [wireguard-test]

   TASK [Create Client config directory] *****************************************************************************************************************************************************************************
   ok: [wireguard-test]

   TASK [Create Client Configurations] *******************************************************************************************************************************************************************************
   ok: [wireguard-test] =&gt; (item=temp_user_10)
   ok: [wireguard-test] =&gt; (item=temp_user_20)
   ok: [wireguard-test] =&gt; (item=temp_user_30)
   ok: [wireguard-test] =&gt; (item=temp_user_40)
   ok: [wireguard-test] =&gt; (item=temp_user_50)
   ok: [wireguard-test] =&gt; (item=temp_user_60)

   TASK [Enable wg-quick@wg0.service] ********************************************************************************************************************************************************************************
   changed: [wireguard-test]

   TASK [Start wg-quick@wg0.service] *********************************************************************************************************************************************************************************
   changed: [wireguard-test]

   TASK [Check status of wg-quick@wg0.service] ***********************************************************************************************************************************************************************
   changed: [wireguard-test]

   TASK [Show service status] ****************************************************************************************************************************************************************************************
   ok: [wireguard-test] =&gt; {
      "msg": [
         "● wg-quick@wg0.service - WireGuard via wg-quick(8) for wg0",
         "     Loaded: loaded (/lib/systemd/system/wg-quick@.service; enabled; preset: enabled)",
         "     Active: active (exited) since Tue 2023-12-26 15:12:18 UTC; 1s ago",
         "       Docs: man:wg-quick(8)",
         "             man:wg(8)",
         "             https://www.wireguard.com/",
         "             https://www.wireguard.com/quickstart/",
         "             https://git.zx2c4.com/wireguard-tools/about/src/man/wg-quick.8",
         "             https://git.zx2c4.com/wireguard-tools/about/src/man/wg.8",
         "    Process: 4897 ExecStart=/usr/bin/wg-quick up wg0 (code=exited, status=0/SUCCESS)",
         "   Main PID: 4897 (code=exited, status=0/SUCCESS)",
         "        CPU: 152ms"
      ]
   }

   PLAY RECAP ********************************************************************************************************************************************************************************************************
   wireguard-test             : ok=10   changed=4    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   


</code></pre></div>    </div>
  </li>
</ol>

<h2 id="conclusion">Conclusion</h2>

<p>This process should successfully execute and configure your WireGuard server from start to finish. Initially, we manually configured the server to develop the Ansible playbook. While this approach was sufficient to get the server up and running, it lacked the scalability and replicability essential for efficient management. Without Ansible, each new user configuration would require manual setup, a time-consuming and error-prone process. Now, by simply making an entry into the vault and executing the playbook, everything is deployed in a standardized and consistent manner. This automation not only saves time but also ensures uniformity across different environments or machines. Imagine the complexity of explaining the addition of a new user over the phone using the manual method versus the streamlined process we have now established with Ansible. This approach significantly simplifies the management of the VPN server, making it more accessible and manageable.</p>

<p>– Cheers</p>

<p><a href="https://leanpub.com/ansible-for-devops">Ansible for DevOps</a> - If you’re just starting with Ansible this is the book you want. Get it from Leanpub and if there are any updates to the book you will also receive them. (Digital Only)</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:snoops" role="doc-endnote">
      <p>Corporate entities that handle your data such as ATT, Google, Amazon, Microsoft are known to read or analyze their customer’s un-encrypted data or network traffic. <a href="#fnref:snoops" class="reversefootnote" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
    </li>
    <li id="fn:key" role="doc-endnote">
      <p>Do not use the keys in the examples. I intentionally change them so they won’t work if you copy and paste. Generating them is simple and I really want you to be secure <a href="#fnref:key" class="reversefootnote" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
    </li>
    <li id="fn:test_server" role="doc-endnote">
      <p>I’m executing this against the testing server not the production wireguard servers. You are too, right? <a href="#fnref:test_server" class="reversefootnote" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[title: Create WireGuard VPN with Ansible tags: [ansible, security, vpn, devops] author: twimprine layout: post]]></summary></entry><entry><title type="html">Pihole with Windows DNS</title><link href="https://blog.thomaswimprine.com/blog/2023-03-30-Pihole-with-Windows-DNS/" rel="alternate" type="text/html" title="Pihole with Windows DNS" /><published>2023-03-30T00:00:00+00:00</published><updated>2025-12-10T20:02:35+00:00</updated><id>https://blog.thomaswimprine.com/blog/Pihole-with-Windows-DNS</id><content type="html" xml:base="https://blog.thomaswimprine.com/blog/2023-03-30-Pihole-with-Windows-DNS/"><![CDATA[<p>I’ve had an idea for a bit and wanted to try it out. There’s no reason why it wouldn’t work but I can certainly think of a few reasons why would woudn’t want to do this.</p>
<ul>
  <li>Bad Reasons
    <ul>
      <li>Active Directory Replication - Depending on your convergence time, number of domain controllers and DNS zones. This might be a bad idea! I’m doing this in my home lab with two domain controllers so I expect it’s not going to be a big deal</li>
      <li>You can turn Pihole off if it’s not working properly. This would require you to delete the zone from DNS and allow it to replicate - See first reason! Additionally you would need to stop the scheduled process if you have it automatically updating on a schedule.</li>
      <li>It’s exceptionally easy to have Pihole running in a docker container and forward your DNS to the container then to the internet.</li>
    </ul>
  </li>
  <li>Maybe Reasons
    <ul>
      <li>Malware domain lists - It’s all about tradeoffs so it may be worth saving a bit of a potential headache especally if you have click happy users.</li>
    </ul>
  </li>
</ul>

<p>First we need to setup our lab which is simply going to consist of two AD/DNS servers. DNS will be replicated as AD integrated zones and forwarding to local DNS.</p>

<table>
  <thead>
    <tr>
      <th>DC Name</th>
      <th>IP Address</th>
      <th>Primary DNS</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>DC01</td>
      <td>10.10.10.10</td>
      <td>10.10.10.20</td>
    </tr>
    <tr>
      <td>DC02</td>
      <td>10.10.10.20</td>
      <td>10.10.10.10</td>
    </tr>
  </tbody>
</table>

<ul>
  <li>Install ActiveDirectory Domain Services on both systems
    <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Install-WindowsFeature</span><span class="w"> </span><span class="nt">-name</span><span class="w"> </span><span class="nx">AD-Domain-Services</span><span class="w"> </span><span class="nt">-IncludeManagementTools</span><span class="w">
</span></code></pre></div>    </div>
  </li>
  <li>This is a new forest and domain so let’s get it all going. Starting on DC01:
    <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Install-ADDSForest</span><span class="w"> </span><span class="nt">-DomainName</span><span class="w"> </span><span class="nx">lab.local</span><span class="w"> </span><span class="nt">-DomainMode</span><span class="w"> </span><span class="nx">Win2012</span><span class="w"> </span><span class="nt">-ForestMode</span><span class="w"> </span><span class="nx">Win2012</span><span class="w"> </span><span class="nt">-InstallDNS</span><span class="p">:</span><span class="bp">$true</span><span class="w">
</span></code></pre></div>    </div>
    <ul>
      <li>It will prompt you for a SafeMode Password so enter your super secret password.</li>
      <li>Confirm reboot - I hit ‘A’ for Yes to All</li>
    </ul>
  </li>
  <li>Installation starts and wait for it to complete and reboot. On the second system (DC02) we need to wait for the first to finish then we can add the system to the domain.
    <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Install-ADDSDomainController</span><span class="w"> </span><span class="nt">-DomainName</span><span class="w"> </span><span class="nx">lab.local</span><span class="w"> </span><span class="nt">-Credential</span><span class="w"> </span><span class="p">(</span><span class="n">Get-Credential</span><span class="w"> </span><span class="nx">lab\Administrator</span><span class="p">)</span><span class="w"> </span><span class="nt">-InstallDNS</span><span class="p">:</span><span class="bp">$true</span><span class="w">
</span></code></pre></div>    </div>
  </li>
  <li>I always use the <code class="language-plaintext highlighter-rouge">Get-Credential</code> when possible. The scripts and command-line are horrible places for passwords.</li>
</ul>

<p>Everything is complete so let’s check to make sure DNS is installed and running:</p>
<ul>
  <li>
    <p>Simple check of DNS to make sure everything is good and we notice we don’t have any reverse lookup zone for our network <code class="language-plaintext highlighter-rouge">10.10.10.0/24</code> so let’s create it real quick
<img src="/assets/img/blog/reverseDNSZone.jpg" alt="DNS Reverse" /></p>
  </li>
  <li>
    <p>Check and make sure it’s replicated to the second DC
<img src="/assets/img/blog/reverseDNSZone-DC02.jpg" alt="DC02 DNS Replication" /></p>
  </li>
</ul>

<p>OK! We have our domain setup and configured without any major issues so let’s get our DNS data and see what we can do.</p>

<p>Going for the simple list we are going to be using <a href="https://github.com/StevenBlack" target="_blank">Stephen Black’s</a> block list. Stephen has an awesome setup that let’s you update the <code class="language-plaintext highlighter-rouge">/etc/hosts</code> file on your system to block ads, malware, etc. We are just extending this to AD DNS. His host files are formatted as <code class="language-plaintext highlighter-rouge">0.0.0.0 &lt;domain.name&gt;</code> so the query fails. It’s very effective and I use it on my personal laptop for when I’m not behind my home filters.</p>

<p>We are going to pull the domain names from the second column and use that to create a zone. Within that zone we will have a record doing the exact same as Stephen and point to 0.0.0.0.</p>

<ul>
  <li>First let’s create some sample data - I’ll just copy the first few lines out of his files on (GitHub)[https://github.com]{:”target”=”_blank”}</li>
</ul>

<p>May do this again with BIND just because…</p>]]></content><author><name>Thomas Wimprine</name><email>thomas@thomaswimprine.com</email></author><category term="powershell" /><category term="scripting" /><category term="activedirectory" /><summary type="html"><![CDATA[I’ve had an idea for a bit and wanted to try it out. There’s no reason why it wouldn’t work but I can certainly think of a few reasons why would woudn’t want to do this. Bad Reasons Active Directory Replication - Depending on your convergence time, number of domain controllers and DNS zones. This might be a bad idea! I’m doing this in my home lab with two domain controllers so I expect it’s not going to be a big deal You can turn Pihole off if it’s not working properly. This would require you to delete the zone from DNS and allow it to replicate - See first reason! Additionally you would need to stop the scheduled process if you have it automatically updating on a schedule. It’s exceptionally easy to have Pihole running in a docker container and forward your DNS to the container then to the internet. Maybe Reasons Malware domain lists - It’s all about tradeoffs so it may be worth saving a bit of a potential headache especally if you have click happy users.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.thomaswimprine.com/assets/img/blog/powershell.jpg" /><media:content medium="image" url="https://blog.thomaswimprine.com/assets/img/blog/powershell.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">User Maintenance Script with PowerShell</title><link href="https://blog.thomaswimprine.com/blog/2023-03-30-User-Maintenance-PowerShell-Script/" rel="alternate" type="text/html" title="User Maintenance Script with PowerShell" /><published>2023-03-30T00:00:00+00:00</published><updated>2025-12-10T20:02:35+00:00</updated><id>https://blog.thomaswimprine.com/blog/User-Maintenance-PowerShell-Script</id><content type="html" xml:base="https://blog.thomaswimprine.com/blog/2023-03-30-User-Maintenance-PowerShell-Script/"><![CDATA[<p>Throughout my years as a sysadmin, I have needed to synchronize user information and HR data. In a previous job at a university, I developed a script to synchronize Oracle user information with AD data. I have written this script many times and believe that others may find it useful. While the university and OU names have been changed for privacy reasons, the underlying premise remains the same.</p>

<p>At the university, we had students whose status changed frequently throughout the day - they would enroll, drop classes, and so on. We needed to create, enable, or disable accounts and update majors. These changes were made in Oracle software and were not directly connected to AD. The Oracle administrators created a script that would export all the changes since the last run into a CSV file, which I would then use to make the necessary updates.</p>

<p>The next version of the script was supposed to query the database directly, eliminating the need for file swapping, but I left that job before it could be developed.</p>

<p><a href="https://github.com/twimprine/UserMaintPowerShell" target="_blank">User Maintenance with PowerShell</a></p>]]></content><author><name>Thomas Wimprine</name><email>thomas@thomaswimprine.com</email></author><category term="powershell" /><category term="scripting" /><category term="activedirectory" /><summary type="html"><![CDATA[Throughout my years as a sysadmin, I have needed to synchronize user information and HR data. In a previous job at a university, I developed a script to synchronize Oracle user information with AD data. I have written this script many times and believe that others may find it useful. While the university and OU names have been changed for privacy reasons, the underlying premise remains the same.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.thomaswimprine.com/assets/img/blog/powershell.jpg" /><media:content medium="image" url="https://blog.thomaswimprine.com/assets/img/blog/powershell.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Create Static Website on AWS with CDK</title><link href="https://blog.thomaswimprine.com/blog/2023-03-07-Create-Static-AWS-Site/" rel="alternate" type="text/html" title="Create Static Website on AWS with CDK" /><published>2023-03-07T00:00:00+00:00</published><updated>2025-12-10T20:02:35+00:00</updated><id>https://blog.thomaswimprine.com/blog/Create-Static-AWS-Site</id><content type="html" xml:base="https://blog.thomaswimprine.com/blog/2023-03-07-Create-Static-AWS-Site/"><![CDATA[<p><a href="https://github.com/twimprine/static-website-example" target="_blank">Github Project Code</a></p>

<p>Here’s the basic senario, you need to create a static website and get it deployed but don’t know what resources you need, don’t want to deal with servers or a large hosting provider. You want to have control of your assets, be able to add services as needed all with the ability to deploy your site with some level of change managment.</p>

<p>Simple!</p>

<p>Use Amazon Cloud Development Kit!</p>

<p>The AWS CDK allows for Infrastructure as Code for your applications or systems. Simply put it allows you to define your resources the application or in this case website, is going to need so everything is deployed and managed as one cohesive unit. If you need to add a database you define the database resources, security and connections. Need an ec2 server that’s fine too, define the size, type, availability and VPC and you’re off to the races. The same goes for almost any Amazon service, DynamoDB, Lex, S3, EKS, etc… I think you get the point. Everything is defined in code so the same development tools used for management and version control apply here, including CI/CD pipelines.</p>

<ol>
  <li>CDK Basics and Installation</li>
  <li>Defining your project</li>
  <li>Creating Resources</li>
  <li>Creating your static site</li>
</ol>

<p class="lead">CDK Basics and Installation</p>

<p>I would alway recommend checking out the docs and information, Amazon does a great job of documenting their services which can be found here –&gt; <a href="https://aws.amazon.com/cdk">AWS CDK</a>. I use multiple operating systems so I’ve installed it via the installer (Windows), npm (Linux) and brew (Mac) with some mix depending on issues or requirements. The default language for CDK is TypeScript so if you have Node installed it’s the most forward way to get everything working. Node is used on the backend of CDK so it’s a requirement and can be found here <a href="https://nodejs.org/en/download/">Node Download</a>.</p>

<p>Here’s the currently supported languages from the AWS documentation. I’ll be using TypeScript and Linux in this tutorial.</p>

<table>
  <thead>
    <tr>
      <th>Language</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>TypeScript</td>
    </tr>
    <tr>
      <td>JavaScript</td>
    </tr>
    <tr>
      <td>Python</td>
    </tr>
    <tr>
      <td>Java</td>
    </tr>
    <tr>
      <td>C#</td>
    </tr>
    <tr>
      <td>Go</td>
    </tr>
  </tbody>
</table>

<p>Now that you have Node and the CDK installed you need to configure the environment the easiest way is with the cli by executing the configure command.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws configure
</code></pre></div></div>

<p>CDK leverages AWS CloudFormation and S3 to deploy and manage the enviroment for those to be created and configured execure the bootstrap command.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cdk bootstrap aws://&lt;ACCOUNT-NUMBER&gt;/&lt;DESIRED-REGION&gt;
</code></pre></div></div>
<p>This will create a bucket similar to this:
<img src="/assets/img/blog/cdk-created-bucket.jpg" alt="Bootstrap Configured Bucket" class="lead" width="1526" height="201" loading="lazy" /></p>

<p>And a CloudFormation template like this:
<img src="/assets/img/blog/cdk-cloudformation-template.jpg" alt="Bootstrap Configured CloudFormation Template" class="lead" width="1526" height="201" loading="lazy" /></p>

<p>We are now ready to create our project!!!</p>

<p class="lead">Creating our Project</p>

<p>Lets create or working directory and initialize our project</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> ~/static-website
<span class="nb">cd</span> ~/static-website
cdk init app <span class="nt">--language</span> typescript
</code></pre></div></div>

<p>This will create the project and initialize a git repository for you so all changes from here will be tracked. The directory stucture in your project should look like this:</p>

<p><img src="/assets/img/blog/cdk-staticsite-new-project-directories.jpg" alt="new Project Directory Structure" class="height=auto" loading="lazy" /></p>

<p>Now that your project is created we need to define some environment variables AWS and deployment.
Open the file <code class="language-plaintext highlighter-rouge">./bin/static-website.ts</code> and uncomment and update the line</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">env</span><span class="p">:</span> <span class="p">{</span> <span class="nl">account</span><span class="p">:</span> <span class="dl">'</span><span class="s1">123456789012</span><span class="dl">'</span><span class="p">,</span> <span class="nx">region</span><span class="p">:</span> <span class="dl">'</span><span class="s1">us-east-1</span><span class="dl">'</span> <span class="p">},</span>
</code></pre></div></div>
<p>so that it matches your AWS account and region you want to deploy to.</p>

<p>The file will look like this:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#!/usr/bin/env node
</span><span class="k">import</span> <span class="dl">'</span><span class="s1">source-map-support/register</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="o">*</span> <span class="kd">as </span><span class="nx">cdk</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">aws-cdk-lib</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">StaticWebsiteStack</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../lib/static-website-stack</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">cdk</span><span class="p">.</span><span class="nc">App</span><span class="p">();</span>
<span class="k">new</span> <span class="nc">StaticWebsiteStack</span><span class="p">(</span><span class="nx">app</span><span class="p">,</span> <span class="dl">'</span><span class="s1">StaticWebsiteStack</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
  <span class="cm">/* If you don't specify 'env', this stack will be environment-agnostic.
   * Account/Region-dependent features and context lookups will not work,
   * but a single synthesized template can be deployed anywhere. */</span>

  <span class="cm">/* Uncomment the next line to specialize this stack for the AWS Account
   * and Region that are implied by the current CLI configuration. */</span>
  <span class="c1">// env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },</span>

  <span class="cm">/* Uncomment the next line if you know exactly what Account and Region you
   * want to deploy the stack to. */</span>
 <span class="na">env</span><span class="p">:</span> <span class="p">{</span> <span class="na">account</span><span class="p">:</span> <span class="dl">'</span><span class="s1">123456789012</span><span class="dl">'</span><span class="p">,</span> <span class="na">region</span><span class="p">:</span> <span class="dl">'</span><span class="s1">us-east-1</span><span class="dl">'</span> <span class="p">},</span> <span class="c1">//  &lt;------------Change this line</span>

  <span class="cm">/* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */</span>
<span class="p">});</span>
</code></pre></div></div>

<p>Now we get to add the resources you will need to deploy your site.</p>

<p>You need to have a few things we are going to configure. All resource configuration happens in the <code class="language-plaintext highlighter-rouge">./lib/static-website-stack.ts</code> file.</p>
<ol>
  <li>DNS Domain hosted in S3</li>
  <li>S3 Bucket to host your content</li>
  <li>Content to host</li>
</ol>

<p>First let’s create the directory for your web content - depending on what you’re deploying this will be the content we are putting in S3.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> ./web-content
</code></pre></div></div>

<p>Since we need to have those resources we need to import the associated libraries for them. Add these lines to the top of the file under the <code class="language-plaintext highlighter-rouge">Construct</code>:</p>

<p class="faded">You can comment out or delete the aws-sqs line since we aren’t using that service.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="o">*</span> <span class="kd">as </span><span class="nx">s3</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">aws-cdk-lib/aws-s3</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="o">*</span> <span class="kd">as </span><span class="nx">s3Deployment</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">aws-cdk-lib/aws-s3-deployment</span><span class="dl">'</span>
<span class="k">import</span> <span class="o">*</span> <span class="kd">as </span><span class="nx">route53</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">aws-cdk-lib/aws-route53</span><span class="dl">'</span>
</code></pre></div></div>

<p>This imports the constructs, methods and other various parts we need to for these services.</p>
<ul>
  <li>We need s3 because it’s going to be hosting our content</li>
  <li>s3Deployment gives us the ability to populate or S3 Bucket from local disk or other buckets</li>
  <li>route53 is AWS’ DNS service and we will need to create and update the records there</li>
</ul>

<p>Let’s first define the subdomain ‘www’, domain and web asset directory:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">class</span> <span class="nc">StaticWebsiteStack</span> <span class="kd">extends</span> <span class="nc">cdk</span><span class="p">.</span><span class="nx">Stack</span> <span class="p">{</span>
  <span class="nf">constructor</span><span class="p">(</span><span class="nx">scope</span><span class="p">:</span> <span class="nx">Construct</span><span class="p">,</span> <span class="nx">id</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">props</span><span class="p">?:</span> <span class="nx">cdk</span><span class="p">.</span><span class="nx">StackProps</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">super</span><span class="p">(</span><span class="nx">scope</span><span class="p">,</span> <span class="nx">id</span><span class="p">,</span> <span class="nx">props</span><span class="p">);</span>

    <span class="kd">const</span> <span class="nx">recordName</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">www</span><span class="dl">'</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">siteDomainName</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">mydomain.com</span><span class="dl">'</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">webAssetDirectory</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">web-content</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>

<p>We are going to use those variables later and it’s a good practice to not include static definitions in your code, if possible.</p>

<p>Define our s3 bucket properties:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      <span class="kd">const</span> <span class="nx">webBucket</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">s3</span><span class="p">.</span><span class="nc">Bucket</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="dl">'</span><span class="s1">webbucket</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">bucketName</span><span class="p">:</span> <span class="p">[</span><span class="nx">recordName</span><span class="p">,</span> <span class="nx">siteDomainName</span><span class="p">].</span><span class="nf">join</span><span class="p">(</span><span class="dl">'</span><span class="s1">.</span><span class="dl">'</span><span class="p">),</span>     <span class="c1">// Bucket name needs to match the website DNS </span>
        <span class="na">publicReadAccess</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>                                 <span class="c1">// Enables public access for a website</span>
        <span class="na">removalPolicy</span><span class="p">:</span> <span class="nx">cdk</span><span class="p">.</span><span class="nx">RemovalPolicy</span><span class="p">.</span><span class="nx">DESTROY</span><span class="p">,</span>               <span class="c1">// When we update it's actually a delete and replace</span>
        <span class="na">autoDeleteObjects</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>                                <span class="c1">// Don't ask us for permission just do it </span>
        <span class="na">websiteIndexDocument</span><span class="p">:</span> <span class="dl">'</span><span class="s1">index.html</span><span class="dl">'</span>                      <span class="c1">// Entry file for the site</span>
      <span class="p">});</span>
</code></pre></div></div>

<p>Copy our data from the ./web-content directory to S3</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      <span class="c1">// Copy the web assets to the S3 bucket</span>
      <span class="k">new</span> <span class="nx">s3Deployment</span><span class="p">.</span><span class="nc">BucketDeployment</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="dl">"</span><span class="s2">deployStaticWebsite</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">sources</span><span class="p">:</span> <span class="p">[</span><span class="nx">s3Deployment</span><span class="p">.</span><span class="nx">Source</span><span class="p">.</span><span class="nf">asset</span><span class="p">(</span><span class="nx">webAssetDirectory</span><span class="p">)],</span>
        <span class="na">destinationBucket</span><span class="p">:</span> <span class="nx">webBucket</span><span class="p">,</span>
      <span class="p">});</span>
</code></pre></div></div>

<p>Now we have our bucket created at <code class="language-plaintext highlighter-rouge">www.mydomain.com</code> and we have assets from the <code class="language-plaintext highlighter-rouge">./web-content</code> directory placed in the bucket. Now we need to redirect traffic from the bucket of <code class="language-plaintext highlighter-rouge">mydomain.com</code> so everyone ends up in the same place.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      <span class="c1">// redirect the root domain bucket to the www bucket</span>
      <span class="kd">const</span> <span class="nx">redirectBucket</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">s3</span><span class="p">.</span><span class="nc">Bucket</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="dl">"</span><span class="s2">redirectBucket</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">bucketName</span><span class="p">:</span> <span class="nx">siteDomainName</span><span class="p">,</span>                                             <span class="c1">// defines the name of the bucket </span>
        <span class="na">websiteRedirect</span><span class="p">:</span> <span class="p">{</span> <span class="na">hostName</span><span class="p">:</span> <span class="p">[</span><span class="nx">recordName</span><span class="p">,</span> <span class="nx">siteDomainName</span><span class="p">].</span><span class="nf">join</span><span class="p">(</span><span class="dl">'</span><span class="s1">.</span><span class="dl">'</span><span class="p">)</span> <span class="p">}</span>   <span class="c1">// Redirects 'mydomain.com'</span>
      <span class="p">});</span>
</code></pre></div></div>

<p>Update the DNS records as needed</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="c1">// Get the DNS base zone to update the record in the next step</span>
    <span class="kd">const</span> <span class="nx">zone</span> <span class="o">=</span> <span class="nx">route53</span><span class="p">.</span><span class="nx">HostedZone</span><span class="p">.</span><span class="nf">fromLookup</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="dl">'</span><span class="s1">baseZone</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
      <span class="na">domainName</span><span class="p">:</span> <span class="nx">siteDomainName</span>
    <span class="p">});</span>

    <span class="k">new</span> <span class="nx">route53</span><span class="p">.</span><span class="nc">ARecord</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="dl">'</span><span class="s1">AliasRecord</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
      <span class="nx">zone</span><span class="p">,</span>                             <span class="c1">// mydomain.com</span>
      <span class="nx">recordName</span><span class="p">,</span>                       <span class="c1">// www</span>
      <span class="c1">// creates the target record with the bucket information</span>
      <span class="na">target</span><span class="p">:</span> <span class="nx">route53</span><span class="p">.</span><span class="nx">RecordTarget</span><span class="p">.</span><span class="nf">fromAlias</span><span class="p">(</span><span class="k">new</span> <span class="nx">cdk</span><span class="p">.</span><span class="nx">aws_route53_targets</span><span class="p">.</span><span class="nc">BucketWebsiteTarget</span><span class="p">(</span><span class="nx">webBucket</span><span class="p">)),</span>
    <span class="p">});</span>
</code></pre></div></div>

<p>Our CDK configuration is complete!</p>

<p>Head back to the command line and execute <code class="language-plaintext highlighter-rouge">cdk synth</code> if everything is configured correctly you should get a CloudFormation template with all the changes it will deploy to your environment.</p>

<p>Now we create the web content - to keep it simple we are going to create a simple <code class="language-plaintext highlighter-rouge">Hello World</code> page and you can go wild from there!</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ./web-content
<span class="nb">touch </span>index.html
</code></pre></div></div>

<p>Open the file index.html in your favorite editor and add the following content.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;!DOCTYPE HTML&gt;</span>
<span class="nt">&lt;html&gt;</span>
    <span class="nt">&lt;head&gt;</span>
        <span class="nt">&lt;title&gt;</span>Hello World!<span class="nt">&lt;/title&gt;</span>
    <span class="nt">&lt;/head&gt;</span>
    <span class="nt">&lt;body&gt;</span>
        <span class="nt">&lt;h1&gt;</span>Hello World!<span class="nt">&lt;/h1&gt;</span>
    <span class="nt">&lt;/body&gt;</span>
<span class="nt">&lt;/html&gt;</span>
</code></pre></div></div>

<p>Back at the command line execute <code class="language-plaintext highlighter-rouge">cdk deploy</code> in the root directory of your project. It’s going to take a minute to deploy the resources and will ask for any changes to permissions. Once complete browse to your address at <code class="language-plaintext highlighter-rouge">http://www.mydomain.com</code> and you should get your new static Hello World page!</p>

<p>Cheers!</p>]]></content><author><name>Thomas Wimprine</name><email>thomas@thomaswimprine.com</email></author><category term="aws" /><category term="cdk" /><category term="development" /><summary type="html"><![CDATA[Github Project Code]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.thomaswimprine.com/assets/img/blog/html_img.jpg" /><media:content medium="image" url="https://blog.thomaswimprine.com/assets/img/blog/html_img.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Controlling Window AC with HomeAssistant</title><link href="https://blog.thomaswimprine.com/blog/2023-02-16-Control-Window-AC-with-HomeAssistant/" rel="alternate" type="text/html" title="Controlling Window AC with HomeAssistant" /><published>2023-02-16T00:00:00+00:00</published><updated>2025-12-10T20:02:35+00:00</updated><id>https://blog.thomaswimprine.com/blog/Control-Window-AC-with-HomeAssistant</id><content type="html" xml:base="https://blog.thomaswimprine.com/blog/2023-02-16-Control-Window-AC-with-HomeAssistant/"><![CDATA[<p>Do you ever feel like you’re at the mercy of your home appliances, instead of the other way around? I certainly did when the central AC compressor in our rented house decided to call it quits. Replacing it would have cost a small fortune, so my landlord opted to install several separate window AC units instead.</p>

<p>While these units got the job done, they were far from perfect. Not only were they unable to communicate with each other, they also lacked any kind of smart features. But I wasn’t about to let some basic appliances dictate the temperature of my home. With the power of HomeAssistant, I was determined to take back control.</p>

<p>My first step was to determine if the window units would maintain their settings when unplugged. After a few hours, I was relieved to find that they did. Next, I put my trusty smart-plugs to the test, using them to turn the AC units on and off remotely through HomeAssistant.</p>

<p>But simply turning the units on and off wasn’t enough. I wanted to take things to the next level and create a smart thermostat that would allow me to control the temperature across the house, rather than just in the individual rooms. Using a combination of Sonoff and ESP-based temperature sensors, I was able to create a generic thermostat that would keep the house at just the right temperature.</p>

<p>Create the direcories for your configs</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> config/climate 
</code></pre></div></div>

<p>edit the configuration.yaml file to contain this line and read the configs from the directory that was just created.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">climate</span><span class="pi">:</span> <span class="kt">!include_dir_merge_list</span> <span class="s">config/climate/</span>
</code></pre></div></div>

<p>Next create a file in the directory that was just created - mine is <code class="language-plaintext highlighter-rouge">climate.yaml</code> and paste the following code.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="pi">-</span> <span class="na">platform</span><span class="pi">:</span> <span class="s">generic_thermostat</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">Master Bedroom Thermostat</span>
  <span class="na">unique_id</span><span class="pi">:</span> <span class="s1">'</span><span class="s">123456789456123456789'</span>
  <span class="na">heater</span><span class="pi">:</span> <span class="s">switch.master_bedroom_ac</span>
  <span class="na">ac_mode</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="na">target_sensor</span><span class="pi">:</span> <span class="s">sensor.master_bedroom_temp</span>
  <span class="na">min_temp</span><span class="pi">:</span> <span class="m">64</span>
  <span class="na">max_temp</span><span class="pi">:</span> <span class="m">85</span>
  <span class="na">cold_tolerance</span><span class="pi">:</span> <span class="m">0.3</span>
  <span class="na">hot_tolerance</span><span class="pi">:</span> <span class="m">0.3</span>
  <span class="na">precision</span><span class="pi">:</span> <span class="m">1.0</span>
  <span class="na">min_cycle_duration</span><span class="pi">:</span>
    <span class="na">minutes</span><span class="pi">:</span> <span class="m">3</span>

</code></pre></div></div>

<table>
  <tbody>
    <tr>
      <td>Field</td>
      <td>Value</td>
    </tr>
    <tr>
      <td>platform</td>
      <td>This is just a thermostat as far as HomeAssistant same as a Nest but one we created</td>
    </tr>
    <tr>
      <td>name</td>
      <td>Any friendly name you like</td>
    </tr>
    <tr>
      <td>unique_id</td>
      <td>A unique value that’s different to your installation - I use a GUID generally so I know they are globally unique, people are really bad at randomness and it’s easy</td>
    </tr>
    <tr>
      <td>ac_mode</td>
      <td> </td>
    </tr>
    <tr>
      <td>target_sensor</td>
      <td>Sensor entity it will be reading for the temperature</td>
    </tr>
    <tr>
      <td>min_temp</td>
      <td>Min temp you will allow the thermostat to be set to - My AC only goes to 64F so that’s what I put</td>
    </tr>
    <tr>
      <td>max_temp:</td>
      <td>Max temp it can be set to</td>
    </tr>
    <tr>
      <td>cold_tolerance</td>
      <td>Wiggle room cold</td>
    </tr>
    <tr>
      <td>hot_tolerance</td>
      <td>Wiggle room hot</td>
    </tr>
    <tr>
      <td>precision</td>
      <td>The nearest number you will set it to - 1.0 will only allow whole numbers</td>
    </tr>
    <tr>
      <td>min_cycle_duration</td>
      <td>Min amount of time it will stay in the state once set</td>
    </tr>
    <tr>
      <td>minutes</td>
      <td>How many minutes - Could be days/hours/minutes/seconds/milliseconds</td>
    </tr>
  </tbody>
</table>

<p>Reload your integrations and you should have a thermostat on your dashboard</p>

<p><img src="/assets/img/blog/2023-02-16-142040-generic-thermostat.png" alt="Generic Thermostat" height="50%" width="50%" /></p>

<p>With the help of a bit of code and some creative problem-solving, I was able to turn my basic window units into fully-functioning smart appliances.</p>

<p>Cheers</p>

<table>
  <tbody>
    <tr>
      <td>Parts List</td>
      <td> </td>
    </tr>
    <tr>
      <td>Sonoff Temperature and Humidity Sensors</td>
      <td><a href="https://amzn.to/4nHgNGS">https://amzn.to/4nHgNGS</a></td>
    </tr>
    <tr>
      <td>Smart Plug with Energy Monitoring</td>
      <td><a href="https://amzn.to/4nCKCIn">https://amzn.to/4nCKCIn</a></td>
    </tr>
  </tbody>
</table>]]></content><author><name>Thomas Wimprine</name><email>thomas@thomaswimprine.com</email></author><category term="homeassistant" /><category term="automation" /><summary type="html"><![CDATA[Do you ever feel like you’re at the mercy of your home appliances, instead of the other way around? I certainly did when the central AC compressor in our rented house decided to call it quits. Replacing it would have cost a small fortune, so my landlord opted to install several separate window AC units instead.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.thomaswimprine.com/assets/img/blog/fire-207353.jpg" /><media:content medium="image" url="https://blog.thomaswimprine.com/assets/img/blog/fire-207353.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Assign missing tag to VM</title><link href="https://blog.thomaswimprine.com/blog/2018-10-19-Assign-missing-tag-to-VM/" rel="alternate" type="text/html" title="Assign missing tag to VM" /><published>2018-10-19T00:00:00+00:00</published><updated>2025-12-10T20:02:35+00:00</updated><id>https://blog.thomaswimprine.com/blog/Assign-missing-tag-to-VM</id><content type="html" xml:base="https://blog.thomaswimprine.com/blog/2018-10-19-Assign-missing-tag-to-VM/"><![CDATA[<p>We run our backup job schedules based on the tags assigned to the VM. This keeps it relatively simple and to add a system to a backup job you only need to tag it, not go into another software and edit a backup job.Recently, while working on a production system I noticed it wasn’t tagged! Whoever had created it never tagged it and so it’s been in production for a while and has never been protected. Obviously, this is a bad situation and I needed to make sure it was fixed across the board.
Did a quick PowerCLI script to find and assign a nightly backup job to anything it found. We can go back and audit this later if a system needs better protection.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$Tag</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">get-tag</span><span class="w"> </span><span class="nt">-Category</span><span class="w"> </span><span class="s2">"BDR"</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nx">Bronze</span><span class="w">
</span><span class="n">Get-VM</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{(</span><span class="n">Get-TagAssignment</span><span class="w"> </span><span class="bp">$_</span><span class="p">)</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="bp">$null</span><span class="p">}</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">New-TagAssignment</span><span class="w"> </span><span class="nt">-Tag</span><span class="w"> </span><span class="nv">$Tag</span><span class="w">

</span></code></pre></div></div>]]></content><author><name>Thomas Wimprine</name><email>thomas@thomaswimprine.com</email></author><category term="backup" /><category term="powershell" /><category term="scripting" /><category term="vmware" /><summary type="html"><![CDATA[We run our backup job schedules based on the tags assigned to the VM. This keeps it relatively simple and to add a system to a backup job you only need to tag it, not go into another software and edit a backup job.Recently, while working on a production system I noticed it wasn’t tagged! Whoever had created it never tagged it and so it’s been in production for a while and has never been protected. Obviously, this is a bad situation and I needed to make sure it was fixed across the board. Did a quick PowerCLI script to find and assign a nightly backup job to anything it found. We can go back and audit this later if a system needs better protection.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.thomaswimprine.com/assets/img/blog/tag.jpg" /><media:content medium="image" url="https://blog.thomaswimprine.com/assets/img/blog/tag.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>