<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nick Schmidt</title>
    <description>The latest articles on DEV Community by Nick Schmidt (@ngschmidt).</description>
    <link>https://dev.to/ngschmidt</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F347506%2F2a9799f0-45c8-4a6b-8e2f-7560fc902686.jpeg</url>
      <title>DEV Community: Nick Schmidt</title>
      <link>https://dev.to/ngschmidt</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ngschmidt"/>
    <language>en</language>
    <item>
      <title>Starting an IaC Repository with GitHub and Terraform</title>
      <dc:creator>Nick Schmidt</dc:creator>
      <pubDate>Sat, 13 Dec 2025 09:00:00 +0000</pubDate>
      <link>https://dev.to/ngschmidt/starting-an-iac-repository-with-github-and-terraform-54bk</link>
      <guid>https://dev.to/ngschmidt/starting-an-iac-repository-with-github-and-terraform-54bk</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;There are only two hard things in Computer Science: cache invalidation, naming things, and off by one errors.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;Loosely attributed to Phil Karlton&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's start off with a bit of a hot take - Terraform isn't particularly hard to learn. It does use unique configuration languages, but most people don't struggle with learning the code.&lt;/p&gt;

&lt;p&gt;Infrastructure-as-Code (IaC) isn't about the programming language - &lt;em&gt;it's about establishing a body of discipline around managing infrastructure&lt;/em&gt;. Tools like Ansible and Terraform simply facilitate the practice.&lt;/p&gt;

&lt;p&gt;Instead of focusing on some programmatically elegant tricks here, let's try to focus on how to build a "starter kit" of sorts to build upon this practice. The &lt;em&gt;managed resources&lt;/em&gt; in this example will be intentionally simple to shift focus to the structure, naming, and release management aspects of Infrastructure-as-Code.&lt;/p&gt;

&lt;p&gt;[&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F66swsmobt38z2st5lefz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F66swsmobt38z2st5lefz.png" alt="IaC Starter Kit" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;](iac_starter_kit.png)&lt;/p&gt;

&lt;h2&gt;
  
  
  Repositories (Structure and Naming)
&lt;/h2&gt;

&lt;p&gt;Start a GitHub repository with some basic documentation before contributing code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;README.md&lt;/code&gt; should describe what the project is for, describe the project structure: how the software works.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;USAGE.md&lt;/code&gt; should describe how to consume resources within the project, how release management works.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CONTRIBUTING.md&lt;/code&gt; should describe how to contribute to the codebase: the branch and merge workflows and rules of conduct go here.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CHANGELOG.md&lt;/code&gt; should be created based on the &lt;a href="https://keepachangelog.com/en/1.0.0/" rel="noopener noreferrer"&gt;Keep a Changelog&lt;/a&gt; standards&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.gitignore&lt;/code&gt; should make sure that any temporary files created by tools, like &lt;code&gt;pycache&lt;/code&gt;, Terraform locks don't accidentally get committed to the repository&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;markdownlint.json&lt;/code&gt; and any other linting rules - automated code QC is a good thing&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;img/&lt;/code&gt; should be created to contain rendered images for documentation. Use illustrations to make the repository easy to understand!&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dwg/&lt;/code&gt; should be created to contain unrendered diagrams, e.g. &lt;code&gt;svg&lt;/code&gt;, &lt;code&gt;d2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;doc/&lt;/code&gt; may be created for any automatically rendered documentation, e.g. ReadTheDocs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once these are created, start mapping out what loose structures should be included in the repository. Here are some examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;conf.d/&lt;/code&gt; for any flat file configurations that may get deployed

&lt;ul&gt;
&lt;li&gt;Make subdirectories for any machine targets&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;
&lt;code&gt;roles/&lt;/code&gt; for any Ansible roles. Since this is IaC, breaking this down into roles instead of one giant pile will be simpler

&lt;ul&gt;
&lt;li&gt;Within each &lt;code&gt;role&lt;/code&gt;:&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;templates/&lt;/code&gt; should contain any Jinja2 templates. Ansible will auto-detect this folder by name, and it simplifies structure quite a bit.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;requirements.txt&lt;/code&gt; should contain any software prerequisites for the Ansible playbooks. This facilitates CI/CD tooling with virtual environments, in addition to better documenting software dependencies.&lt;/li&gt;
&lt;li&gt;Playbooks and truth files, of course&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;
&lt;code&gt;terraform/&lt;/code&gt; for any Terraform code

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;modules/&lt;/code&gt; for any Terraform re-usable modules&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;accounts/&lt;/code&gt; for any Terraform tenants, e.g. AWS Accounts, CloudFlare accounts, or other unrelated resources to keep them separate and organized&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;
&lt;code&gt;python/&lt;/code&gt; for any Python code&lt;/li&gt;

&lt;li&gt;
&lt;code&gt;js/&lt;/code&gt; for any JavaScript&lt;/li&gt;

&lt;li&gt;...and so on.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Now that the raw structure is somewhat laid out, we can shift focus to the Terraform account's subdirectory (in &lt;code&gt;/terraform/accounts/{{ account_type }}_{{ account_id }}_{{account_name}}&lt;/code&gt;) structure. Here's what I've seen lead to a maintainable code base:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/terraform/accounts/cloudflare_12345_engyak_co&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;templates/&lt;/code&gt; for any &lt;code&gt;gotmpl&lt;/code&gt; templates&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;provider.tf&lt;/code&gt; should declare any Terraform pre-requisites, e.g. the Cloudflare provider minimum version&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vars.tf&lt;/code&gt; should declare any input variables. In my experience, this is a good place for module inputs, but not as useful for actual infrastructure declarations&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;locals.tf&lt;/code&gt; should declare any Don't Repeat Yourself (DRY) variables. I typically use them for consistent resource names and IDs. There are a lot of opinions about &lt;code&gt;vars&lt;/code&gt; versus &lt;code&gt;locals&lt;/code&gt;, but there are a few key differences:&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vars&lt;/code&gt; should actually be variable (non-static multiples of a &lt;code&gt;resource&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;locals&lt;/code&gt; can render and iterate on an input, e.g. with &lt;code&gt;for_each&lt;/code&gt; loops&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;backend.tf&lt;/code&gt; should indicate where &lt;code&gt;terraform.tfstate&lt;/code&gt; is placed, any file locking. Normally, this points to an S3 bucket and provides authorization for it&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data.tf&lt;/code&gt; should have any external data resources. This example doesn't need any, but AWS IAM policy documents and S3 bucket policies fit this category. Any resource prefixed with &lt;code&gt;data&lt;/code&gt; instead of &lt;code&gt;resource&lt;/code&gt; goes here, essentially&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Now that all that's out of the way, we're able to &lt;em&gt;actually create resources&lt;/em&gt;. Things can be a lot more free-form here, because the definition of &lt;em&gt;related resources&lt;/em&gt; can vary greatly based on who's doing the work.&lt;/p&gt;

&lt;p&gt;My personal preference is to maintain small, easily readable files that function independently wherever possible. In this example, we'll use one file for each DNS zone. Here's &lt;code&gt;/terraform/accounts/cloudflare_youwish_engyak_co/engyak.co.tf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 1resource "cloudflare_record" "engyak_co_blog" {
 2 content = "blog-engyak-co.pages.dev"
 3 name = "blog"
 4 proxied = false
 5 ttl = 1
 6 type = "CNAME"
 7 zone_id = "redacted"
 8}
 9
10resource "cloudflare_record" "engyak_co_root" {
11 content = "blog-engyak-co.pages.dev"
12 name = "engyak.co"
13 proxied = true
14 ttl = 1
15 type = "CNAME"
16 zone_id = "redacted"
17}
18
19resource "cloudflare_record" "engyak_co_uri_blog" {
20 name = "engyak.co"
21 priority = 1
22 proxied = false
23 ttl = 1
24 type = "URI"
25 zone_id = "redacted"
26 data {
27 target = "blog.engyak.co"
28 weight = 1
29 }
30}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These &lt;code&gt;resource&lt;/code&gt;s are built according to the &lt;code&gt;provider&lt;/code&gt; in &lt;code&gt;provider.tf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 1terraform {
 2 required_providers {
 3 cloudflare = {
 4 source = "cloudflare/cloudflare"
 5 version = "~&amp;gt; 4"
 6 }
 7 }
 8}
 9
10provider "cloudflare" {
11}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always consult the &lt;code&gt;provider&lt;/code&gt;'s documentation on how to use their &lt;code&gt;resource&lt;/code&gt;s.&lt;/p&gt;

&lt;h2&gt;
  
  
  Actions (Release Management)
&lt;/h2&gt;

&lt;p&gt;The biggest advantage a Git repository has for Infrastructure-as-Code is its versioning capability, but the ability to control the release of changes can really take things to the next level.&lt;/p&gt;

&lt;p&gt;First, I'd recommend starting out with a &lt;em&gt;branch management plan&lt;/em&gt;. It can start simple, like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Don't allow any commits directly to &lt;code&gt;main&lt;/code&gt; (GitHub branch protection rules, plus general threads in &lt;code&gt;CONTRIBUTING.md&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Only allow code to be pushed to &lt;code&gt;main&lt;/code&gt; via a successful pull request (GitHub branch protection rules do this as well)

&lt;ul&gt;
&lt;li&gt;At least 1 approving peer review&lt;/li&gt;
&lt;li&gt;All testing must &lt;strong&gt;PASS&lt;/strong&gt; (more on this later)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;All prospective changes must start as a diverging branch (or fork, but forking is &lt;em&gt;much&lt;/em&gt; more advanced) that is &lt;strong&gt;up-to-date&lt;/strong&gt; with &lt;code&gt;main&lt;/code&gt;
&lt;/li&gt;

&lt;li&gt;Outline appropriate change windows, if applicable&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;At this point, the rules are in place, but none of it actually controls release. GitHub doesn't have credentials to release changes; ideally no users should either. The objective here is to &lt;strong&gt;prevent all direct changes to infrastructure&lt;/strong&gt;. This can be achieved with AWS IAM roles, Cloudflare RBAC, or an equivalent. Take away the keys!&lt;/p&gt;

&lt;p&gt;GitHub Actions provides a (usually free or cheap) amnesic container service to run ephemeral code from source control. This is going to be the foundation for this example moving forward, but other providers like GitLab and Atlassian have equivalents as well. If the source control provider doesn't have a built-in service, plenty of other CI tools exist to fill that gap, like Jenkins and Concourse.&lt;/p&gt;

&lt;p&gt;For a Terraform pipeline, there should be two Actions per account:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;terraform plan&lt;/code&gt;: This will test your code for validity, and also explain any potential impacts the change might have&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform apply&lt;/code&gt;: This will implement tested changes. This Action &lt;em&gt;should&lt;/em&gt; be restricted to the &lt;code&gt;main&lt;/code&gt; branch!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's an example &lt;code&gt;plan&lt;/code&gt; Action. I named it based on `{{ event trigger }}: {{ provider }} {{ action }} to keep things organized.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;br&gt;
 1---&lt;br&gt;
 2name: 'On-Commit: Cloudflare Terraform Plan'&lt;br&gt;
 3&lt;br&gt;
 4on:&lt;br&gt;
 5 push:&lt;br&gt;
 6&lt;br&gt;
 7permissions:&lt;br&gt;
 8 contents: read&lt;br&gt;
 9&lt;br&gt;
10jobs:&lt;br&gt;
11 plan:&lt;br&gt;
12 name: 'Terraform Plan'&lt;br&gt;
13 env:&lt;br&gt;
14 CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}&lt;br&gt;
15 runs-on: ubuntu-latest&lt;br&gt;
16 steps:&lt;br&gt;
17 - uses: actions/checkout@v4&lt;br&gt;
18 - name: 'Terraform Setup'&lt;br&gt;
19 uses: hashicorp/setup-terraform@v3&lt;br&gt;
20 with:&lt;br&gt;
21 terraform_version: '&amp;gt;= 1.10.5'&lt;br&gt;
22 - name: 'Terraform Plan'&lt;br&gt;
23 run: |&lt;br&gt;
24 terraform init&lt;br&gt;
25 terraform validate&lt;br&gt;
26 terraform plan -input=false&lt;br&gt;&lt;br&gt;
27 working-directory: terraform/accounts/cloudflare_youwish_engyak_co/&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Here's a rundown on how the testing works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We use the &lt;code&gt;env&lt;/code&gt; directive to expose &lt;code&gt;CLOUDFLARE_API_TOKEN&lt;/code&gt; (specified in the &lt;code&gt;cloudflare&lt;/code&gt; provider as the way to pass credentials)&lt;/li&gt;
&lt;li&gt;We use &lt;code&gt;actions/checkout@v4&lt;/code&gt; (or latest version) to load a copy of &lt;code&gt;main&lt;/code&gt; into the Actions runner&lt;/li&gt;
&lt;li&gt;We use &lt;code&gt;hashicorp/setup-terraform@v3&lt;/code&gt;. Previous Actions runners shipped with Terraform, but the base image didn't update this package frequently enough. Now it doesn't ship with the image - but this tool lets us restrict and control software versions as part of the pipeline. This lets us slow releases if breaking changes occur with &lt;code&gt;terraform&lt;/code&gt; without having to monkey around with internals - it's a much better system.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;Terraform Plan&lt;/code&gt; step is where most of the work gets done. We initialize Terraform in &lt;strong&gt;non-interactive mode&lt;/strong&gt; (&lt;code&gt;-input=false&lt;/code&gt;) using our workspace with the &lt;code&gt;working-directory&lt;/code&gt; key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This will now run every time code is committed to the repository, and it'll display any expected changes every time code is contributed. If it fails, it will produce an error and (ideally) notify engineers/developers on where to fix it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note: &lt;code&gt;terraform validate&lt;/code&gt; and &lt;code&gt;terraform plan&lt;/code&gt; do not catch all problems, just test for config validity. Resource conflicts, API idiosyncrasies will pass this step and only reveal things on &lt;code&gt;apply&lt;/code&gt;!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now, we can finally start releasing changes:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;br&gt;
 1---&lt;br&gt;
 2name: 'Cron-Demand: Cloudflare Terraform Apply'&lt;br&gt;
 3&lt;br&gt;
 4on:&lt;br&gt;
 5 workflow_dispatch:&lt;br&gt;
 6 branches: ['main']&lt;br&gt;
 7 schedule:&lt;br&gt;
 8 - cron: "15 4,5 * * *"&lt;br&gt;
 9&lt;br&gt;
10permissions:&lt;br&gt;
11 contents: read&lt;br&gt;
12&lt;br&gt;
13jobs:&lt;br&gt;
14 plan:&lt;br&gt;
15 name: 'Terraform Plan'&lt;br&gt;
16 env:&lt;br&gt;
17 CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}&lt;br&gt;
18 runs-on: ubuntu-latest&lt;br&gt;
19 steps:&lt;br&gt;
20 - uses: actions/checkout@v4&lt;br&gt;
21 - name: 'Terraform Setup'&lt;br&gt;
22 uses: hashicorp/setup-terraform@v3&lt;br&gt;
23 with:&lt;br&gt;
24 terraform_version: '&amp;gt;= 1.10.5'&lt;br&gt;
25 - name: 'Terraform Plan'&lt;br&gt;
26 id: tf_plan&lt;br&gt;
27 run: |&lt;br&gt;
28 terraform init&lt;br&gt;
29 terraform validate&lt;br&gt;
30 terraform plan -input=false --detailed-exitcode&lt;br&gt;&lt;br&gt;
31 continue-on-error: true&lt;br&gt;
32 working-directory: terraform/accounts/cloudflare_youwish_engyak_co/&lt;br&gt;
33 - name: 'Terraform Apply'&lt;br&gt;
34 run: |&lt;br&gt;
35 terraform apply&lt;br&gt;&lt;br&gt;
36 working-directory: terraform/accounts/cloudflare_youwish_engyak_co/&lt;br&gt;
37 if: github.ref != 'refs/heads/main' &amp;amp;&amp;amp; needs.tf_plan.outputs.exit-code == 2&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This Action will either run daily at 0415-0515 UTC or if executed manually. We've established a "change window", and there are quite a few more complexities added to this workflow to implemet change safety:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;detailed-exitcode&lt;/code&gt; and &lt;code&gt;id: tf_plan&lt;/code&gt; allow us to "catch" the results of &lt;code&gt;terraform plan&lt;/code&gt;. A return code of &lt;code&gt;0&lt;/code&gt; means no changes required, and &lt;code&gt;2&lt;/code&gt; means changes are required.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;if:&lt;/code&gt; conditionals restrict the dangerous parts of the workflow to &lt;strong&gt;only&lt;/strong&gt; execute when the branch is &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;plan&lt;/code&gt; is valid and expects changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Terraform Starter Kit
&lt;/h2&gt;

&lt;p&gt;This template should act as a foundational "starter kit" for establishing an effective, robust, mature Infrastructure-as-Code practice. I've found that it's easier to modify and improve an existing process than to start anew - the objective here is to get engineers past that "writer's block."&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>devops</category>
      <category>github</category>
      <category>tutorial</category>
      <category>terraform</category>
    </item>
    <item>
      <title>Visualize and Report Ansible with OpenTelemetry and Syslog</title>
      <dc:creator>Nick Schmidt</dc:creator>
      <pubDate>Sun, 23 Nov 2025 09:00:00 +0000</pubDate>
      <link>https://dev.to/ngschmidt/visualize-and-report-ansible-with-opentelemetry-and-syslog-4ef2</link>
      <guid>https://dev.to/ngschmidt/visualize-and-report-ansible-with-opentelemetry-and-syslog-4ef2</guid>
      <description>&lt;p&gt;Ansible is a fantastic tool to manage fleets of machines, but it's difficult to provide effective reporting when the fleet massively scales. Imagine hundreds of lines like this; try to find the one that failed (and why):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1PLAY RECAP *********************************************************************
2dev.lab.engyak.net : ok=6 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0   

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...it's not difficult to read, but it doesn't decide what might deserve individual attention. It's possible to create Jinja reports that will be more executive-friendly, but they're focused on individual executions as well.&lt;/p&gt;

&lt;p&gt;Ansible &lt;a href="https://docs.ansible.com/projects/ansible/latest/plugins/callback.html" rel="noopener noreferrer"&gt;callback plugins&lt;/a&gt; provide us a framework to aggregate and analyze information about playbook execution without compromising idempotency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Types of Callback Plugins
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;aggregate&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;aggregate&lt;/code&gt; callback plugins modify the summary at the end of a task's output. They don't appear to impact recap, and don't have many useful examples.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.ansible.com/projects/ansible/latest/collections/callback_index_aggregate.html" rel="noopener noreferrer"&gt;Aggregate Plugin list&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;stdout&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;stdout&lt;/code&gt; callback plugins modify the continual output presented as Ansible completes work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1TASK [Update Apt!] *************************************************************
2ok: [dev.lab.engyak.net]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where the fun begins! Note that only one plugin for &lt;code&gt;stdout&lt;/code&gt; can be selected for a given playbook.&lt;/p&gt;

&lt;h4&gt;
  
  
  Using &lt;code&gt;stdout&lt;/code&gt; callbacks
&lt;/h4&gt;

&lt;p&gt;The process for &lt;a href="https://docs.ansible.com/projects/ansible/latest/reference_appendices/config.html#ansible-configuration-settings" rel="noopener noreferrer"&gt;enabling callback plugins in &lt;code&gt;ansible.cfg&lt;/code&gt;&lt;/a&gt;. Since this is executed from an environment (GitHub Actions), I prefer leveraging environment injection.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ANSIBLE_CALLBACK_RESULT_FORMAT&lt;/code&gt; controls how data is printed out from individual tasks on the screen, this is up to preference. I prefer &lt;code&gt;yaml&lt;/code&gt;, and recommend playing with this setting to see what works best for you.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ANSIBLE_PYTHON_INTERPRETER&lt;/code&gt; silences any chatter about the discovered Python interpreter. Since this is a consistent environment without any tight coupling to specific releases, I don't feel the need to pin one, and I don't want to see the messages.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DEFAULT_STDOUT_CALLBACK&lt;/code&gt; will let you set the &lt;code&gt;stdout&lt;/code&gt; callback plugin&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In GitHub Actions, you can use the &lt;code&gt;env&lt;/code&gt; key to manipulate outputs without having to change any code. I'm also &lt;a href="https://blog.engyak.co/2024/04/patching/" rel="noopener noreferrer"&gt;integrating Netbox into this pipeline&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 1jobs:
 2 build:
 3 name: 'Manage Lab Configurations'
 4 runs-on: self-hosted
 5 env:
 6 ANSIBLE_PYTHON_INTERPRETER: 'auto_silent'
 7 ANSIBLE_STDOUT_CALLBACK: 'default'
 8 ANSIBLE_CALLBACK_RESULT_FORMAT: 'yaml'
 9 NETBOX_TOKEN: ${{ secrets.NETBOX_TOKEN }}
10 NETBOX_API: ${{ vars.NETBOX_URL }}
11 steps:
12 - uses: actions/checkout@v4
13 - name: Execute Ansible Management Playbook
14 run: |
15 python3 -m venv .
16 source bin/activate
17 python3 -m pip install --upgrade pip
18 python3 -m pip install -r requirements.txt
19 ansible-inventory -i local.netbox.netbox.nb_inventory.yml --graph
20 ansible-playbook -i local.netbox.netbox.nb_inventory.yml lab-management.yml          

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For reference purposes, I've added all compatible fields here. The &lt;code&gt;yaml&lt;/code&gt; results format is considerably more compact given the character limit per line.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dense&lt;/code&gt; seems to be a popular callback, and it uses colorization to generate play output, and tries to place things all on one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1task 1.task 1: ns2.lab.engyak.nettask 1: ns2.lab.engyak.net ns.lab.engyak.nettask 2.task 2: ns2.lab.engyak.nettask 2: ns2.lab.engyak.net ns.lab.engyak.nettask 3.task 3: ns2.lab.engyak.nettask 3: ns2.lab.engyak.net ns.lab.engyak.nettask 4.task 4: ns.lab.engyak.nettask 4: ns.lab.engyak.net ns2.lab.engyak.nettask 5.task 5: ns2.lab.engyak.nettask 5: ns2.lab.engyak.net ns.lab.engyak.nettask 6.task 6: ns2.lab.engyak.nettask 6: ns2.lab.engyak.nettask 6: ns2.lab.engyak.nettask 6: ns2.lab.engyak.net ns.lab.engyak.nettask 6: ns2.lab.engyak.net ns.lab.engyak.nettask 6: ns2.lab.engyak.net ns.lab.engyak.nettask 7.task 7: ns.lab.engyak.nettask 7: ns.lab.engyak.net ns2.lab.engyak.nettask 7: ns.lab.engyak.net ns2.lab.engyak.nettask 7: ns.lab.engyak.net ns2.lab.engyak.nettask 7: ns.lab.engyak.net ns2.lab.engyak.nettask 7: ns.lab.engyak.net ns2.lab.engyak.nettask 8.task 8: ns2.lab.engyak.nettask 8: ns2.lab.engyak.net ns.lab.engyak.nettask 9.task 9: ns2.lab.engyak.nettask 9: ns2.lab.engyak.net ns.lab.engyak.nettask 10.task 10: ns2.lab.engyak.nettask 10: ns2.lab.engyak.net ns.lab.engyak.net

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's definitely compact, but not super readable. &lt;code&gt;oneline&lt;/code&gt; is probably the best non-default plugin of the bunch, but it's much more verbose than the default one. It also displays a lot of system-specific information, so no snippet here.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;notification&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is where things get really good for those of us with execution environments! &lt;code&gt;notification&lt;/code&gt; callback plugins send data to external systems when a play finishes.&lt;/p&gt;

&lt;h4&gt;
  
  
  Directing results to OpenTelemetry
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://opentelemetry.io/" rel="noopener noreferrer"&gt;OpenTelemetry&lt;/a&gt; is a truly neat open standard for exchanging "trace information" between systems.&lt;/p&gt;

&lt;p&gt;This is incredibly useful, but also difficult to explain in a way that's clear without providing concrete examples. Essentially, OpenTelemetry-based traces allow debugging systems that do not all exist in the same software package, and it offers a timeline for each step. As it happens, Ansible's callback plugin is well-architected and a good example of the value that a trace can have, even from an application perspective.&lt;/p&gt;

&lt;p&gt;First, we'll need to assemble an OpenTelemetry-compliant platform to stream Ansible results to. I've selected &lt;a href="https://www.jaegertracing.io/docs/2.12/getting-started/" rel="noopener noreferrer"&gt;Jaeger&lt;/a&gt; for this purpose. It has an all-in-one quickstart function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1docker run --rm --name jaeger \
2 -p 16686:16686 \
3 -p 4317:4317 \
4 -p 4318:4318 \
5 -p 5778:5778 \
6 -p 9411:9411 \
7 cr.jaegertracing.io/jaegertracing/jaeger:2.12.0

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once it's running, we need to instruct Ansible to forward data. This is achievable exclusively with environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 env:
2 ANSIBLE_CALLBACKS_ENABLED: 'community.general.opentelemetry'
3 ANSIBLE_OPENTELEMETRY_ENABLE_FROM_ENVIRONMENT: 'ANSIBLE_OPENTELEMETRY_ENABLED'
4 ANSIBLE_OPENTELEMETRY_ENABLED: 'true'
5 OTEL_EXPORTER_OTLP_ENDPOINT: 'http://jaeger.lab.engyak.net:4317'
6 OTEL_EXPORTER_INSECURE: 'true'
7 OTEL_SERVICE_NAME: 'ansible'

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In addition to these variables, the module requires the following additions to &lt;code&gt;requirements.txt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1opentelemetry-sdk
2opentelemetry-exporter-otlp-proto-grpc
3opentelemetry-exporter-otlp-proto-http

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once these changes get applied, with _no other required changes to the Ansible code &lt;strong&gt;itself&lt;/strong&gt; _, all subsequent runs submit OTLP traces to Jaeger It looks like this:&lt;/p&gt;

&lt;p&gt;[&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fly6oa48wnylkgpwb0igz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fly6oa48wnylkgpwb0igz.png" alt="Jaeger UI #1" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;](jaeger_1.png)[&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkv3xxp8ux3m16p1ky70j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkv3xxp8ux3m16p1ky70j.png" alt="Jaeger UI #2" width="800" height="169"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;](jaeger_2.png)&lt;/p&gt;

&lt;p&gt;This provides a comprehensive "drill down" for every step taken by Ansible, and I've honestly never seen this level of detail before. Every single programmatic step is logged with a timestamp, allowing an engineer to find out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which node took too long&lt;/li&gt;
&lt;li&gt;Which step slowed things down the most&lt;/li&gt;
&lt;li&gt;Whether that matches the baseline for other nodes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a transactional application this has to be even more useful.&lt;/p&gt;

&lt;h4&gt;
  
  
  Directing Results to Syslog
&lt;/h4&gt;

&lt;p&gt;Now, for something quite a bit more boring (but equally important). If OpenTelemetry is a microscope, Syslog is the 10,000 foot view. This can also be set up by CI, and should run in parallel with OpenTelemetry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 env:
2 ANSIBLE_CALLBACKS_ENABLED: 'community.general.opentelemetry,community.general.syslog_json'
3 ANSIBLE_OPENTELEMETRY_ENABLE_FROM_ENVIRONMENT: 'ANSIBLE_OPENTELEMETRY_ENABLED'
4 ANSIBLE_OPENTELEMETRY_ENABLED: 'true'
5 OTEL_EXPORTER_OTLP_ENDPOINT: 'http://jaeger.lab.engyak.net:4317'
6 OTEL_EXPORTER_INSECURE: 'true'
7 OTEL_SERVICE_NAME: 'ansible'
8 SYSLOG_PORT: '54514'
9 SYSLOG_SERVER: '127.0.0.1'

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each of these callback plugins serves a different purpose. Syslog callbacks provide a shorter summary as JSON, which can easily be dashboarded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1&amp;lt;14&amp;gt;1 2025-11-30T07:41:00-09:00 10.66.1.143 gh-runner2 - - - ansible-command: task execution OK; host: ns.lab.engyak.net; message: {"changed": false, "checksum": "a46e7011b00c560dddcc193ef16f01fd2d05970e", "dest": "/etc/unbound/unbound.conf", "gid": 0, "group": "root", "mode": "0640", "owner": "root", "path": "/etc/unbound/unbound.conf", "size": 4531, "state": "file", "uid": 0}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some example conditions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;"changed": true&lt;/code&gt; would indicate how many modifications were made per hostname (identified by &lt;code&gt;host: ns.lab.engyak.net&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;!= 'task execution OK'&lt;/code&gt; would search for job failures&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Modernizing the Monitoring Stack
&lt;/h2&gt;

&lt;p&gt;Ansible, despite being an infrastructure tool, provides a good example of the different types of modern monitoring. Thematically, these concepts &lt;strong&gt;should&lt;/strong&gt; be applied to actual applications.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Traces are an excellent tool to identify software process bottlenecks. Any tool that has long-running jobs can benefit from tracing. They're computationally costly, so they should be saved for any tool where performance degradation truly matters.&lt;/li&gt;
&lt;li&gt;Syslog is the "swiss army knife" of monitoring. It's the best tool for simple events, and can be the foundation for event-driven programming.&lt;/li&gt;
&lt;li&gt;Metrics allow infrastructure engineers to "just send the important bits" via tools like &lt;code&gt;protobuf&lt;/code&gt;, sort of like SNMP but better. In the network realm, this is where Model-Driven Telemetry reigns supreme, and in the application stack Prometheus is a popular option.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing I did find interesting - Grafana + Alloy allowed the unification of all of these data types. Here's a preview of what Jaeger in Grafana looks like:&lt;/p&gt;

&lt;p&gt;[&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fimmva4qy9kj8nxigic39.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fimmva4qy9kj8nxigic39.png" alt="Grafana Preview" width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;](grafana_1.png)&lt;/p&gt;

</description>
      <category>monitoring</category>
      <category>devops</category>
      <category>automation</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Automate DNS Zone Generation and Deployment with Ansible and Netbox</title>
      <dc:creator>Nick Schmidt</dc:creator>
      <pubDate>Sun, 10 Nov 2024 09:00:00 +0000</pubDate>
      <link>https://dev.to/ngschmidt/automate-dns-zone-generation-and-deployment-with-ansible-and-netbox-1pke</link>
      <guid>https://dev.to/ngschmidt/automate-dns-zone-generation-and-deployment-with-ansible-and-netbox-1pke</guid>
      <description>&lt;p&gt;In a &lt;a href="https://blog.engyak.co/2024/01/dns-automation/" rel="noopener noreferrer"&gt;previous post&lt;/a&gt;, I covered a method to automatically generate DNS zones from an embedded YAML list.&lt;/p&gt;

&lt;p&gt;This wasn't the most useful on its own, only ensuring that forward and reverse DNS entries match each other (you'll be shocked by how many places it isn't!) - and we need a good way to simplify DNS administration with tooling less expensive that, say, Infoblox.&lt;/p&gt;

&lt;p&gt;This isn't to say that Infoblox is bad, but a fully loaded Infoblox license is a little pricy for home labs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pattern
&lt;/h2&gt;

&lt;p&gt;First, let's illustrate a potential design:&lt;/p&gt;

&lt;p&gt;[&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.engyak.co%2F2024%2F11%2Fdns-e2e%2Fpattern.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.engyak.co%2F2024%2F11%2Fdns-e2e%2Fpattern.svg" alt="Solution Diagram" width="733" height="604"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;](pattern.svg)&lt;/p&gt;

&lt;h2&gt;
  
  
  The Code
&lt;/h2&gt;

&lt;p&gt;In order to do this, we're going to need to find a good way to pull &lt;code&gt;pre-filtered data&lt;/code&gt; for ansible to work with, and Netbox has a GraphQL API (&lt;code&gt;/graphql/&lt;/code&gt;) that's perfect for this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 1{
 2 ip_address_list(filters: {dns_name: {i_contains: "example.net"}, family: 4}) {
 3 dns_name
 4 address
 5 }
 6}
 7{
 8 ip_address_list(filters: {dns_name: {i_contains: "example.net"}, family: 6}) {
 9 dns_name
10 address
11 }
12}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will give us a separate sheet for IPv4 and IPv6 addresses attached to a given zone - and we can assemble it without any postprocessing in Ansible.&lt;/p&gt;

&lt;p&gt;Netbox's GraphQL sandbox produces the following data output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 1{
 2 "data": {
 3 "ip_address_list": [
 4 {
 5 "dns_name": "ns.example.net",
 6 "address": "1.1.1.1/32"
 7 }
 8 ]
 9 }
10}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, this is received via a &lt;em&gt;graphical interface&lt;/em&gt;, which means we can't consume it programmatically. In order to do that, we'll need to package the GraphQL payload in JSON. Here's an Ansible task that does just that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 1 - name: "Try Fetching `lab.engyak.net` IPv4 GraphQL!"
 2 ansible.builtin.uri:
 3 url: "https://netbox/graphql/"
 4 method: POST
 5 body:
 6 query: "query { ip_address_list(filters: {dns_name: {i_contains: \"example.com\"}, family: 4}) { dns_name address }}"
 7 body_format: "json"
 8 headers:
 9 Authorization: "Token {{ lookup('ansible.builtin.env', 'NETBOX_TOKEN') }}"
10 Content-Type: "application/json"
11 Accept: "application/json"
12 validate_certs: false
13 register: result_example_net_v4

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There aren't any &lt;code&gt;pynetbox&lt;/code&gt; based modules that automate this into Ansible, so here we're using the &lt;code&gt;ansible.builtin.uri&lt;/code&gt; module (also known as the &lt;strong&gt;Jack of All Trades&lt;/strong&gt; module) to pull JSON data. It also uses the environment variable &lt;code&gt;NETBOX_TOKEN&lt;/code&gt;, which must be exposed by secrets management / CI processes.&lt;/p&gt;

&lt;p&gt;In this case, I'm pulling IPv4 and IPv6 records separately. Jinja doesn't know the difference between types of record, so I cheat on postprocessing and let GraphQL do all the heavy lifting. IPv6 is the same, but with &lt;code&gt;family: 6&lt;/code&gt;/&lt;code&gt;result_example_net_v6&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The next step is to build Jinja templates to define the zonefiles. I created them in a previous post, but will include all of them in a Gist at the end of this post. They need to be modified to process output from GraphQL, because we don't control any of the field names with it.&lt;/p&gt;

&lt;p&gt;The Jinja templates used in this example are unique to ansible - the custom filter &lt;code&gt;ansible.utils.ipaddr&lt;/code&gt; is amazing, converting Netbox's &lt;code&gt;{{ address }}/{{ cidr }}&lt;/code&gt; notation is compact and efficient, but it doesn't work as an A record target. Invocations like &lt;code&gt;|ansible.utils.ipaddr('address')&lt;/code&gt; or &lt;code&gt;|ansible.utils.ipaddr('revdns')&lt;/code&gt; are particularly useful here.&lt;/p&gt;

&lt;p&gt;Finally, it's good to test the resulting zonefiles for sanity. It's included in the Gist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrospective
&lt;/h2&gt;

&lt;p&gt;Netbox's GraphQL API is a really effective tool for aggregating pre-filtered data and driving automation processes. I was quite impressed that I could just ask an API endpoint for this nice and tidy report, already pre-formatted for me!&lt;/p&gt;

&lt;p&gt;Lack of field and format control is an issue with GraphQL (you're stuck with whatever data structure the application architect has in store for you) - but Ansible and Jinja2 empower you to present the back-end data in any front-end manner you prefer (in my case, as DNS data loaded into an Unbound instance).&lt;/p&gt;

&lt;p&gt;Nearly any business reporting process can be driven from Netbox in this fashion, as long as the resulting format can be Jinjafied. Here are some ideas on how this can be used further:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Report on Circuits per &lt;code&gt;Region&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Report on IT-Managed assets in a given &lt;code&gt;Site&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Report on how many &lt;code&gt;Site&lt;/code&gt;s have IPv6 coverage&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Gist
&lt;/h2&gt;

&lt;p&gt;As promised, here's the raw code I created to automate DNS zonefile management from Netbox:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@import url('https://cdn.rawgit.com/lonekorean/gist-syntax-themes/d49b91b3/stylesheets/idle-fingers.css');

@import url('https://fonts.googleapis.com/css?family=Open+Sans');
body {
  font: 16px 'Open Sans', sans-serif;
}
body .gist .gist-file {
  border-color: #555 #555 #444
}
body .gist .gist-data {
  border-color: #555
}
body .gist .gist-meta {
  color: #ffffff;
  background: #373737; 
}
body .gist .gist-meta a {
  color: #ffffff
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;a href="https://gist.github.com/ngschmidt/33ce644c3873d1fe3e82f91378eaa2fc" rel="noopener noreferrer"&gt;GitHub Link&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>VM Deployment Pipelines with Proxmox</title>
      <dc:creator>Nick Schmidt</dc:creator>
      <pubDate>Sat, 31 Aug 2024 09:00:00 +0000</pubDate>
      <link>https://dev.to/ngschmidt/vm-deployment-pipelines-with-proxmox-ihm</link>
      <guid>https://dev.to/ngschmidt/vm-deployment-pipelines-with-proxmox-ihm</guid>
      <description>&lt;p&gt;Decoupled approaches to deployment of IaaS workloads are the way of the future.&lt;/p&gt;

&lt;p&gt;Here, we'll try to construct a VM deployment pipeline leveraging GitHub Actions and Ansible's community modules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proxmox Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not featured here&lt;/strong&gt; : Loading a VM ISO is particular to the Proxmox deployment, but it's necessary for future steps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's create a VM named &lt;code&gt;deb12.6-template&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="proxmox-1.png"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuaemcz7paksgkpskifml.png" alt="First creation screen" width="721" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I set a separate VM ID range for templates to simplify visual automatic sorting.&lt;/p&gt;

&lt;p&gt;&lt;a href="proxmox-2.png"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3k63308bmzmox2iij92y.png" alt="Second creation screen" width="720" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="proxmox-3.png"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9cmuruvgb1vbrvg1cbo7.png" alt="Third creation screen" width="716" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note: Paravirtualized hardware is still the optimal choice, like with vSphere - but in this case, &lt;code&gt;VirtIO&lt;/code&gt; is the code supplier.&lt;/p&gt;

&lt;p&gt;&lt;a href="proxmox-4.png"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyxyn31w1eoj59u1nwpul.png" alt="Fourth creation screen" width="719" height="540"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note: SSD Emulation and &lt;code&gt;qemu-agent&lt;/code&gt; are required for virtual disk reclamation with QEMU. This is particularly important in my lab.&lt;/p&gt;

&lt;p&gt;&lt;a href="proxmox-5.png"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3k4d7uumtu18b18skxdk.png" alt="Fifth creation screen" width="717" height="535"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this installation, I'm using paravirtualized network adapters and have separated my management(&lt;code&gt;vmbr0&lt;/code&gt;) and data plane(&lt;code&gt;vmbr1&lt;/code&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Debian Linux Setup
&lt;/h2&gt;

&lt;p&gt;I'll skip the Linux installer parts for brevity, Debian's installer is excellent and easy to use.&lt;/p&gt;

&lt;p&gt;At a high level, we'll want to do some preparatory steps before declaring this a usable base image:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create users

&lt;ul&gt;
&lt;li&gt;Recommended approach: Create a bootstrap user, then shred it&lt;/li&gt;
&lt;li&gt;Leave the &lt;code&gt;bootstrap&lt;/code&gt; user with an SSH key on the base image&lt;/li&gt;
&lt;li&gt;After creation, build a &lt;code&gt;takeover&lt;/code&gt; playbook that installs the latest and greatest username table, &lt;code&gt;sssd&lt;/code&gt;, SSH keys, APM, anything with confidential cryptographic material that should not be left unencrypted on the hypervisor&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;This won't slow the VM deployment speed by as much as you think&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Install packages

&lt;ul&gt;
&lt;li&gt;This is just a list of some basics that I prefer to add to each machine. It's more network-centric; anything more comprehensive should be part of a build playbook specific to whatever's being deployed.&lt;/li&gt;
&lt;li&gt;Note: This is an Ansible playbook, and therefore, it needs Ansible to run (&lt;code&gt;apt install ansible&lt;/code&gt;)
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Debian&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;machine&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;prep"&lt;/span&gt;
  &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;localhost&lt;/span&gt;
  &lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Install&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;standard&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;packages"&lt;/span&gt;
    &lt;span class="na"&gt;ansible.builtin.apt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;pkg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;curl'&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dnsutils'&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;diffutils'&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ethtool'&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;git'&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mtr'&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;net-tools'&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;netcat-traditional'&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;python3-requests'&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;python3-jinja2'&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tcpdump'&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;telnet'&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;traceroute'&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;qemu-guest-agent'&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;vim'&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;wget'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Clean up the disk. This will make our base image more compact - each clone will inherit any wasted space, so consider it a 10,20x savings in disk usage. I leave this as a file on the base image and name it &lt;code&gt;reset_vm.sh&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c"&gt;# Clean Apt&lt;/span&gt;
apt clean

&lt;span class="c"&gt;# Cleaning logs.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/audit/audit.log &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /dev/null &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /var/log/audit/audit.log
&lt;span class="k"&gt;fi
if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/wtmp &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /dev/null &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /var/log/wtmp
&lt;span class="k"&gt;fi
if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/lastlog &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /dev/null &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /var/log/lastlog
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Cleaning udev rules.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/udev/rules.d/70-persistent-net.rules &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; /etc/udev/rules.d/70-persistent-net.rules
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Cleaning the /tmp directories&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /tmp/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/tmp/&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="c"&gt;# Cleaning the SSH host keys&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/ssh/ssh_host_&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="c"&gt;# Cleaning the machine-id&lt;/span&gt;
&lt;span class="nb"&gt;truncate&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; 0 /etc/machine-id
&lt;span class="nb"&gt;rm&lt;/span&gt; /var/lib/dbus/machine-id
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/machine-id /var/lib/dbus/machine-id

&lt;span class="c"&gt;# Cleaning the shell history&lt;/span&gt;
&lt;span class="nb"&gt;unset &lt;/span&gt;HISTFILE
&lt;span class="nb"&gt;history&lt;/span&gt; &lt;span class="nt"&gt;-cw&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/.bash_history
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-fr&lt;/span&gt; /root/.bash_history

&lt;span class="c"&gt;# Truncating hostname, hosts, resolv.conf and setting hostname to localhost&lt;/span&gt;
&lt;span class="nb"&gt;truncate&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; 0 /etc/&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt;,hosts,resolv.conf&lt;span class="o"&gt;}&lt;/span&gt;
hostnamectl set-hostname localhost

&lt;span class="c"&gt;# Clean cloud-init - deprecated because cloud-init isn't currently used&lt;/span&gt;
&lt;span class="c"&gt;# cloud-init clean -s -l&lt;/span&gt;

&lt;span class="c"&gt;# Force a filesystem sync&lt;/span&gt;
&lt;span class="nb"&gt;sync&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shutdown the Virtual Machine. I prefer to start it back up and shut it down from the hypervisor to ensure that &lt;code&gt;qemu-guest-agent&lt;/code&gt; is working properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment Pipeline
&lt;/h2&gt;

&lt;p&gt;First, we will want to create an API token under "Datacenter -&amp;gt; Permissions -&amp;gt; API Tokens":&lt;/p&gt;

&lt;p&gt;&lt;a href="proxmox-6.png"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl485gv6d5ydv1zqypcls.png" alt="Proxmox API token screen" width="600" height="207"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are some oddities with the Ansible &lt;code&gt;proxmoxer&lt;/code&gt; based module and Ansible to keep in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;api_user&lt;/code&gt; is needed and used by the API client, formatted as &lt;code&gt;{{ user }}@domain&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;api_token_id&lt;/code&gt; is not the same as the output from the command, it's what you put into the "Token ID" field.

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;{{ api_user}}!{{ api_token_id }}&lt;/code&gt; should form the combined credential presented to the API, and match the created token.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;If you attempt to use the output from the API creation screen under &lt;code&gt;api_user&lt;/code&gt; or &lt;code&gt;api_token_id&lt;/code&gt;, it'll return a &lt;code&gt;401 Invalid user&lt;/code&gt; without much explanation as to what might be the issue.&lt;/p&gt;

&lt;p&gt;Here's the pipeline. Github's primary job is to set up the Python/Ansible environment, and translate the workflow inputs into something that Ansible can properly digest.&lt;/p&gt;

&lt;p&gt;I also added some &lt;code&gt;cat&lt;/code&gt; steps - this allows us to use the GitHub Actions log to store intent until Netbox registration completes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;On-Demand:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;VM&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Proxmox"&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;machine_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Machine&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Name"&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;examplename"&lt;/span&gt;
      &lt;span class="na"&gt;machine_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VM&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ID&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(can't&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;re-use)"&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VM&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Template&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Name"&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;choice&lt;/span&gt;
        &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;deb12.6-template&lt;/span&gt;
        &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deb12.6-template"&lt;/span&gt;
      &lt;span class="na"&gt;hardware_cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VM&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;vCPU&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Count"&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
      &lt;span class="na"&gt;hardware_memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VM&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Memory&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Allocation&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;MB)"&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;512"&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;self-hosted&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create Variable YAML File&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;cat &amp;lt;&amp;lt;EOF &amp;gt; roles/proxmox_kvm/parameters.yaml&lt;/span&gt;
          &lt;span class="s"&gt;---&lt;/span&gt;
            &lt;span class="s"&gt;vm_data:&lt;/span&gt;
              &lt;span class="s"&gt;name: "${{ github.event.inputs.machine_name }}"&lt;/span&gt;
              &lt;span class="s"&gt;id: ${{ github.event.inputs.machine_id }}&lt;/span&gt;
              &lt;span class="s"&gt;template: "${{ github.event.inputs.template }}"&lt;/span&gt;
              &lt;span class="s"&gt;node: node&lt;/span&gt;
              &lt;span class="s"&gt;hardware:&lt;/span&gt;
                &lt;span class="s"&gt;cpus: ${{ github.event.inputs.hardware_cpus }}&lt;/span&gt;
                &lt;span class="s"&gt;memory: ${{ github.event.inputs.hardware_memory }}&lt;/span&gt;
                &lt;span class="s"&gt;storage: ssd-tier&lt;/span&gt;
                &lt;span class="s"&gt;format: qcow2&lt;/span&gt;
          &lt;span class="s"&gt;EOF&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build VM&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;cd roles/proxmox_kvm/&lt;/span&gt;
          &lt;span class="s"&gt;cat parameters.yaml&lt;/span&gt;
          &lt;span class="s"&gt;python3 -m venv .&lt;/span&gt;
          &lt;span class="s"&gt;source bin/activate&lt;/span&gt;
          &lt;span class="s"&gt;python3 -m pip install --upgrade pip&lt;/span&gt;
          &lt;span class="s"&gt;python3 -m pip install -r requirements.txt&lt;/span&gt;
          &lt;span class="s"&gt;python3 --version&lt;/span&gt;
          &lt;span class="s"&gt;ansible --version&lt;/span&gt;

          &lt;span class="s"&gt;export PAPIUSER="${{ secrets.PAPIUSER }}"&lt;/span&gt;
          &lt;span class="s"&gt;export PAPI_TOKEN="${{ secrets.PAPI_TOKEN }}"&lt;/span&gt;
          &lt;span class="s"&gt;export PAPI_SECRET="${{ secrets.PAPI_SECRET }}"&lt;/span&gt;
          &lt;span class="s"&gt;export PHOSTNAME="${{ secrets.PHOSTNAME }}"&lt;/span&gt;
          &lt;span class="s"&gt;export NETBOX_TOKEN="${{ secrets.NETBOX_TOKEN }}"&lt;/span&gt;
          &lt;span class="s"&gt;export NETBOX_URL="${{ secrets.NETBOX_URL }}"&lt;/span&gt;
          &lt;span class="s"&gt;export NETBOX_CLUSTER="${{ secrets.NETBOX_CLUSTER_PROX }}"&lt;/span&gt;
          &lt;span class="s"&gt;ansible-playbook build_vm_prox.yml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In addition, a &lt;code&gt;requirements.txt&lt;/code&gt; is required by GitHub to set up the &lt;code&gt;venv&lt;/code&gt;, and belongs in the role folder (&lt;code&gt;roles/proxmox_kvm&lt;/code&gt; as above):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;###### Requirements without Version Specifiers ######
pytz
netaddr
django
jinja2
requests
pynetbox

###### Requirements with Version Specifiers ######
ansible &amp;gt;= 8.4.0              # Mostly just don't use old Ansible (e.g. v2, v3)
proxmoxer &amp;gt;= 2.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This Ansible playbook also integrates Netbox, as my vSphere workflow did, and uses a common schema to simplify code re-use. There are a few quirks with the Proxmox playbooks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There's no module to grab VM Guest network information, but the API provides it, so I can get it with &lt;code&gt;uri&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Proxmox has a nasty habit of breaking Ansible with JSON keys that include &lt;code&gt;-&lt;/code&gt;. The best way to fix it is with a debug action: &lt;code&gt;{{ prox_network_result.json.data | replace('-','_') }}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Proxmox's VM copy needs a timeout configured, and announces it's done before the VM is ready for actions. I added an &lt;code&gt;ansible.builtin.pause&lt;/code&gt; step before starting the VM, and after (to allow it to boot)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;VM&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Proxmox"&lt;/span&gt;
  &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;localhost&lt;/span&gt;
  &lt;span class="na"&gt;gather_facts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="c1"&gt;# Before executing ensure that the prerequisites are installed&lt;/span&gt;
  &lt;span class="c1"&gt;# `ansible-galaxy collection install netbox.netbox`&lt;/span&gt;
  &lt;span class="c1"&gt;# `python3 -m pip install aiohttp pynetbox`&lt;/span&gt;
  &lt;span class="c1"&gt;# We start with a pre-check playbook, if it fails, we don't want to&lt;/span&gt;
  &lt;span class="c1"&gt;# make changes&lt;/span&gt;
  &lt;span class="na"&gt;any_errors_fatal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;vars_files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parameters.yaml"&lt;/span&gt;

  &lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Debug"&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.debug&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;msg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;vm_data&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Test&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;connectivity&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;authentication"&lt;/span&gt;
      &lt;span class="na"&gt;community.general.proxmox_node_info&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;api_host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"PHOSTNAME")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;api_user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"PAPIUSER")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;api_token_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"PAPI_TOKEN")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;api_token_secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"PAPI_SECRET")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
      &lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prox_node_result&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Display&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Node&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Data"&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.debug&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;msg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;prox_node_result&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;VM"&lt;/span&gt;
      &lt;span class="na"&gt;community.general.proxmox_kvm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;api_host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"PHOSTNAME")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;api_user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"PAPIUSER")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;api_token_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"PAPI_TOKEN")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;api_token_secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"PAPI_SECRET")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;vm_data.name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;node&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;vm_data.node&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;vm_data.hardware.storage&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;newid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;vm_data.id&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;clone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;vm_data.template&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;vm_data.hardware.format&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;present&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Wait&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;VM&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;fully&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;register"&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.pause&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Start&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;VM"&lt;/span&gt;
      &lt;span class="na"&gt;community.general.proxmox_kvm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;api_host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"PHOSTNAME")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;api_user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"PAPIUSER")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;api_token_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"PAPI_TOKEN")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;api_token_secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"PAPI_SECRET")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;vm_data.name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;started&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Wait&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;VM&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;fully&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;boot"&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.pause&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;45&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Get&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;VM&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;information"&lt;/span&gt;
      &lt;span class="na"&gt;community.general.proxmox_vm_info&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;api_host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"PHOSTNAME")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;api_user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"PAPIUSER")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;api_token_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"PAPI_TOKEN")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;api_token_secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"PAPI_SECRET")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;vmid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;vm_data.id&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
      &lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prox_vm_result&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Report&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;VM!"&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.debug&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;var&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prox_vm_result&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Fetch&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;VM&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Networking&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;information"&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"PHOSTNAME")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}:8006/api2/json/nodes/{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;vm_data.node&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}/qemu/{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;vm_data.id&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}/agent/network-get-interfaces'&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;GET'&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;application/json'&lt;/span&gt;
          &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PVEAPIToken={{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"PAPIUSER")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}!{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"PAPI_TOKEN")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}={{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"PAPI_SECRET")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;validate_certs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prox_network_result&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Refactor&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Network&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Information"&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.debug&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;msg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;prox_network_result.json.data&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;replace('-','_')&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
      &lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prox_network_result_modified&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Register&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;VM&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Netbox!"&lt;/span&gt;
      &lt;span class="na"&gt;netbox.netbox.netbox_virtual_machine&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;netbox_token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"NETBOX_TOKEN")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;netbox_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"NETBOX_URL")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;validate_certs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cluster&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"NETBOX_CLUSTER")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;vm_data.name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Built&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;by&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;GH&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Actions&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Pipeline!'&lt;/span&gt;
          &lt;span class="na"&gt;local_context_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;prox_vm_result&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;vm_data.hardware.memory&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
          &lt;span class="na"&gt;vcpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;vm_data.hardware.cpus&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Configure&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;VM&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Interface&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Netbox!"&lt;/span&gt;
      &lt;span class="na"&gt;netbox.netbox.netbox_vm_interface&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;netbox_token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"NETBOX_TOKEN")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;netbox_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"NETBOX_URL")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;validate_certs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;vm_data.name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}_intf_{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;item.hardware_address&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;replace(":",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;safe&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
          &lt;span class="na"&gt;virtual_machine&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;vm_data.name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
          &lt;span class="na"&gt;vrf&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Campus'&lt;/span&gt;
          &lt;span class="na"&gt;mac_address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;item.hardware_address&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
      &lt;span class="na"&gt;with_items&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;prox_network_result_modified.msg.result&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
      &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;item.hardware_address != '00:00:00:00:00:00'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Reserve&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;IP"&lt;/span&gt;
      &lt;span class="na"&gt;netbox.netbox.netbox_ip_address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;netbox_token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"NETBOX_TOKEN")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;netbox_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"NETBOX_URL")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;validate_certs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;item.ip_addresses[0].ip_address&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}/{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;item.ip_addresses[0].prefix&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
          &lt;span class="na"&gt;vrf&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Campus'&lt;/span&gt;
          &lt;span class="na"&gt;assigned_object&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;virtual_machine&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;vm_data.name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;present&lt;/span&gt;
      &lt;span class="na"&gt;with_items&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;prox_network_result_modified.msg.result&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
      &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;item.hardware_address != '00:00:00:00:00:00'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Finalize&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;VM&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Netbox!"&lt;/span&gt;
      &lt;span class="na"&gt;netbox.netbox.netbox_virtual_machine&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;netbox_token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"NETBOX_TOKEN")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;netbox_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"NETBOX_URL")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;validate_certs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cluster&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup("env",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"NETBOX_CLUSTER")&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
          &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lab_debian_machines'&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lab_linux_machines'&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lab_apt_updates'&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;vm_data.name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
          &lt;span class="na"&gt;primary_ip4&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;item.ip_addresses[0].ip_address&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}/{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;item.ip_addresses[0].prefix&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
            &lt;span class="na"&gt;vrf&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Campus"&lt;/span&gt;
      &lt;span class="na"&gt;with_items&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;prox_network_result_modified.msg.result&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
      &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;item.hardware_address != '00:00:00:00:00:00'&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Overall, the Proxmox API/playbooks are quite a bit simpler to use than the VMware ones. The &lt;code&gt;proxmoxer&lt;/code&gt; based modules are relatively feature complete compared to &lt;code&gt;vmware_rest&lt;/code&gt;, but the largest exception I found (examples not in this post) was that I could always fall back to Ansible's comprehensive Linux foundation to fill any gaps I needed to. It's a refreshing change.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Starting from scratch with Netbox IPAM</title>
      <dc:creator>Nick Schmidt</dc:creator>
      <pubDate>Sat, 11 May 2024 09:00:00 +0000</pubDate>
      <link>https://dev.to/ngschmidt/starting-from-scratch-with-netbox-ipam-4afj</link>
      <guid>https://dev.to/ngschmidt/starting-from-scratch-with-netbox-ipam-4afj</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Spreadsheets are not an adequate method to manage IP addressing&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Different IP design strategies
&lt;/h2&gt;

&lt;h3&gt;
  
  
  IPv4
&lt;/h3&gt;

&lt;h3&gt;
  
  
  Bogons, and the basics
&lt;/h3&gt;

&lt;p&gt;There are a number of valid and invalid prefixes for use internally within an enterprise. Here's a list of &lt;em&gt;invalid&lt;/em&gt; prefixes in the global routing table; of those, the RFC 1918 prefixes are available for use:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prefix&lt;/th&gt;
&lt;th&gt;RFC&lt;/th&gt;
&lt;th&gt;Usable Internally?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.0.0.0&lt;/td&gt;
&lt;td&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc1122#section-3.2.1.3"&gt;1122&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;🤪 Everybody more or less agreed not to use it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10.0.0.0/8&lt;/td&gt;
&lt;td&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc1918"&gt;1918&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;✅ Use this block for &lt;em&gt;large&lt;/em&gt; prefix allocations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100.64.0.0/10&lt;/td&gt;
&lt;td&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc6598"&gt;6598&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;🙄 CG-NAT, can &lt;em&gt;technically&lt;/em&gt; be used, but will break in random cloud applications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;127.0.0.0/8&lt;/td&gt;
&lt;td&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc1122#section-3.2.1.3"&gt;1122&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;❌ loopback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;169.254.0.0/16&lt;/td&gt;
&lt;td&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc3927"&gt;3927&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;✅, but don't allocate it (APIPA)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;172.16.0.0/12&lt;/td&gt;
&lt;td&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc1918"&gt;1918&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;✅ Use this block for &lt;em&gt;medium&lt;/em&gt; prefix allocations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;192.0.0.0/24&lt;/td&gt;
&lt;td&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc5736"&gt;5736&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;❌ IETF skunkworks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;192.0.2.0/24&lt;/td&gt;
&lt;td&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc5737"&gt;5737&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;❌ Carrier test networks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;192.88.99.0/24&lt;/td&gt;
&lt;td&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc3068"&gt;3068&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;❌ 6to4 relays&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;192.168.0.0/16&lt;/td&gt;
&lt;td&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc1918"&gt;1918&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;✅ Avoid this block for &lt;em&gt;enterprises&lt;/em&gt;, it'll collide with home networks when people use VPN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;198.18.0.0/15&lt;/td&gt;
&lt;td&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc2544"&gt;2544&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;❌ device benchmarking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;198.51.100.0/24&lt;/td&gt;
&lt;td&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc5737"&gt;5737&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;❌ Carrier test networks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;203.0.113.0/24&lt;/td&gt;
&lt;td&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc5737"&gt;5737&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;❌ Carrier test networks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;224.0.0.0/4&lt;/td&gt;
&lt;td&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc3171"&gt;3171&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;❌ multicast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;240.0.0.0/4&lt;/td&gt;
&lt;td&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc1112#section-4"&gt;1122&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;🤯 madlad play, might work, might not. Linux seems to live in this space just fine&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All of these prefixes &lt;em&gt;must&lt;/em&gt; be dropped at any network perimeter, e.g. firewalls, extranet routers, to prevent internal traffic or misconfigured NATs from leaking. It also prevents protocol abuse, which is a cheap and easy way to improve security.&lt;/p&gt;

&lt;p&gt;In multi-site networks, dropping &lt;strong&gt;all&lt;/strong&gt; of these prefixes would be wise - an ethernet loop + APIPA can turn a switching issue into a network-wide outage pretty easily. &lt;a href="https://en.wikipedia.org/wiki/Longest_prefix_match"&gt;Longest Prefix Match&lt;/a&gt; can ensure that any allocated networks remain reachable.&lt;/p&gt;

&lt;p&gt;Each of these prefixes should be created in Netbox, so you can use it as a reference later. I'd recommend &lt;em&gt;tagging&lt;/em&gt; them with some form of hint to indicate usability, e.g.:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;IP:Usable&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IP:Unusable&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As you get more familiar with the API/search, tag-based filters become incredibly handy.&lt;/p&gt;

&lt;h3&gt;
  
  
  IPv6
&lt;/h3&gt;

&lt;p&gt;IPv6 is quite easy. All valid &lt;em&gt;routable&lt;/em&gt; addresses fall under one allocated prefix:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;2000::/3&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This means that you can implement a "default route" that won't accidentally leak bogons like in IPv4, but with a much simpler approach. Instead of implementing &lt;code&gt;::0/0&lt;/code&gt; for your default route, use &lt;code&gt;2000::/3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you insist on using private addressing, I'd encourage a thorough review of why - but this is the prefix available:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fc00::/7&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Link-local addressing, or addressing that is "always on" regardless of prefix allocation, is also allocated a specific prefix. This prevents the need of a &lt;em&gt;bunch&lt;/em&gt; of little helper protocols that simply don't need to exist, or become standardized. Traffic like Router Advertisements(RA), Routing Protocols, First Hop Redundancy Protocols have a distinct source address that can be pinged even before a network is online.&lt;/p&gt;

&lt;p&gt;It's also incredibly handy when bootstrapping new devices! All that's required is some form of helper on the default gateway to act as an SSH proxy and some neighbor discovery, and you suddenly have always-on remote management.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fe80::/10&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Multicast also has its own prefix:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ff80::/8&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is much simpler, but where to &lt;em&gt;get&lt;/em&gt; IPv6 addressing can be more complex. If it's in a lab environment and doesn't need internet access, &lt;code&gt;fc00::/7&lt;/code&gt; is just fine to use.&lt;/p&gt;

&lt;p&gt;The recommended method for acquiring an IPv6 prefix is to request it with &lt;a href="https://www.isc.org/blogs/dhcpv6-prefix-length-mode/"&gt;DHCP-PD&lt;/a&gt; or to request it through a &lt;a href="https://tunnelbroker.net/"&gt;tunnel broker&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;There's one more "gotcha" to keep in mind with IPv6 - weird stuff breaks if you go with a longer prefix than /64. I'd strongly encourage avoiding cutesy CIDR block allocations like /120 or /65; that's an IPv4 solution to a problem IPv6 doesn't have. Just request enough IP addressing for your site instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Constructing an IP hierarchy
&lt;/h2&gt;

&lt;p&gt;For the purposes of this post, we're going to use the following language to describe a &lt;em&gt;network address&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1{{ prefix }}{{ subprefix }}{{ host bits }}/{{ prefix length }}
2 10.99. 100. 0 /24

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The major first step here is to decide &lt;em&gt;how&lt;/em&gt; to break down your addressing. There are two major paths to follow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Location&lt;/em&gt; based addressing is used when &lt;em&gt;prefix scale&lt;/em&gt; is a concern. If you need to summarize routes on routers due to RIB limits, this is the way to go. This can be for a few reasons:

&lt;ul&gt;
&lt;li&gt;"I have a lot of routes / lot of sites and am worried about RIB capacity in my hardware"&lt;/li&gt;
&lt;li&gt;Most enterprise equipment can handle 16-64k routes; if this is not enough, follow this approach&lt;/li&gt;
&lt;li&gt;ISPs will follow this path&lt;/li&gt;
&lt;li&gt;Cloud providers will follow this path&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Purpose&lt;/em&gt; based addressing is used when &lt;em&gt;perimeter security&lt;/em&gt; is a concern. Easy summarization to a common prefix per "network role" allows for straightforward firewall policy creation, including a number of microsegmentation tools that may have laughably low table capacities.

&lt;ul&gt;
&lt;li&gt;"I want to keep my workloads separated from each other"&lt;/li&gt;
&lt;li&gt;Financial services will follow this path&lt;/li&gt;
&lt;li&gt;Healthcare will follow this path&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Committing to one or the other before allocating blocks will simplify your life later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note: With IPv6, only the largest of organizations (ones that need more than 2^8 or 2^16 networks per site) will need to allocate their own top-level prefix. It's easier to just run DHCP-PD and ask for a /56 or /48.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Guidance on prefix allocation
&lt;/h2&gt;

&lt;p&gt;To assess the proper 1918/bogon prefix for use, first assess the number of prefixes you would need as a ceiling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1num_sites*num_network_roles

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Attempt to select a site that will fit this prefix count with a minumum of 80% buffer (leaving a reserve for point-to-point connects, etc.)&lt;/p&gt;

&lt;p&gt;I would highly encourage &lt;em&gt;not&lt;/em&gt; getting creative with CIDR prefix lengths in IPv4-land. If possible, try and stick to &lt;code&gt;/24&lt;/code&gt; for a subprefix. &lt;a href="https://datatracker.ietf.org/doc/html/rfc4862"&gt;IPv6 does not support prefix lengths longer than &lt;code&gt;/64&lt;/code&gt; particularly well&lt;/a&gt; (with specific exceptions for point-to-point, &lt;code&gt;/126&lt;/code&gt; or &lt;code&gt;/127&lt;/code&gt; depending on hardware), and using prefixes like &lt;code&gt;/65&lt;/code&gt; for access segments will lead to trouble with end devices like Android.&lt;/p&gt;

&lt;p&gt;It's much simpler to translate the &lt;code&gt;/24&lt;/code&gt; in question linearly to a &lt;code&gt;/64&lt;/code&gt; and using that calculation to estimate what IPv6 prefix size you want. It's &lt;strong&gt;also much simpler to troubleshoot and maintain if you don't build a pile of weird stuff, even if it makes you feel smart!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As a starting point, it's good to set up a set of standard t-shirt sizes for networks in `{{ IPv4 }}/{{ IPv6 }} format. Here's an example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Large Site/Role: &lt;code&gt;/16&lt;/code&gt;/&lt;code&gt;/56&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Medium Site/Role: &lt;code&gt;/18&lt;/code&gt;/&lt;code&gt;/60&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Small Site/Role: &lt;code&gt;/22&lt;/code&gt;/&lt;code&gt;/62&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;"Normal" subprefix: &lt;code&gt;/24&lt;/code&gt;/&lt;code&gt;/64&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;"Small" subprefix: &lt;code&gt;/26&lt;/code&gt;/&lt;code&gt;/64&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Point-to-point: &lt;code&gt;/31&lt;/code&gt;/&lt;code&gt;/127&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note: Service providers don't always support weird DHCP-PD sizes, so options may be limited to the above.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note: Service providers are typically pretty generous with prefix allocations, and keep in mind a /56 is roughly equivalent to a /16. I'd recommend allocating a /56 per site in production or in the lab whenever permitted.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating it
&lt;/h2&gt;

&lt;p&gt;Once you have sizes set, it's actually pretty easy to let go of your artisanal, hand-crafted prefixes and automate aggressively. With Netbox and Ansible, it's incredibly easy to leverage the &lt;a href="https://docs.ansible.com/ansible/latest/collections/netbox/netbox/netbox_prefix_module.html#ansible-collections-netbox-netbox-netbox-prefix-module"&gt;&lt;code&gt;netbox.netbox.netbox_prefix&lt;/code&gt; module&lt;/a&gt;. The following example will grab a &lt;code&gt;/24&lt;/code&gt; from &lt;code&gt;10.99.0.0/16&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;br&gt;
 1- name: "Example Ansible Playbook"&lt;br&gt;
 2 connection: local&lt;br&gt;
 3 hosts: localhost&lt;br&gt;
 4 gather_facts: False&lt;br&gt;
 5 tasks:&lt;br&gt;
 6 - name: "Get next available prefix"&lt;br&gt;
 7 netbox.netbox.netbox_prefix:&lt;br&gt;
 8 netbox_url: "{{ netbox_url }}"&lt;br&gt;
 9 netbox_token: "{{ netbox_token }}"&lt;br&gt;
10 data:&lt;br&gt;
11 parent: "10.99.0.0/16"&lt;br&gt;
12 prefix_length: 24&lt;br&gt;
13 state: present&lt;br&gt;
14 first_available: yes&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;It's extremely rewarding to design, deploy, and automate an IP design in this manner - and you'll find that automation is considerably easier if &lt;em&gt;what&lt;/em&gt; to automate is well-defined.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Manage Linux patching with Ansible and Netbox!</title>
      <dc:creator>Nick Schmidt</dc:creator>
      <pubDate>Sun, 07 Apr 2024 09:00:00 +0000</pubDate>
      <link>https://dev.to/ngschmidt/manage-linux-patching-with-ansible-and-netbox-400d</link>
      <guid>https://dev.to/ngschmidt/manage-linux-patching-with-ansible-and-netbox-400d</guid>
      <description>&lt;h2&gt;
  
  
  Patching all of my random experiments took too much of my free time, so I automated it.
&lt;/h2&gt;

&lt;p&gt;This is a pretty cheesy thing to do, but over the years it became more and more time-consuming to maintain all the different deployed workloads and infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;p&gt;With all system design, it's best to consider all relevant needs ahead of time. Given that this is a home lab, I decided to adopt an intentionally aggressive, but theoretically viable in production approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nightly patching&lt;/li&gt;
&lt;li&gt;Nightly reboots&lt;/li&gt;
&lt;li&gt;No exempt packages&lt;/li&gt;
&lt;li&gt;Distribution-agnostic, it should patch multiple distributions at once&lt;/li&gt;
&lt;li&gt;This workflow should execute consistently from-code&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Iteration 1: Ansible with Jenkins
&lt;/h2&gt;

&lt;p&gt;The earliest implementation I built here had the least refinement by far. Here I tied Jenkins to an internal repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="//ansible_jenkins.svg"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VM1Y3Ipo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2024/04/patching/ansible_jenkins.svg" alt="Generation 1: Ansible + Jenkins" width="637" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To leverage this, I started out with an &lt;a href="https://docs.ansible.com/ansible/latest/collections/ansible/builtin/ini_inventory.html"&gt;INI inventory&lt;/a&gt;, but it quickly became problematic. I wanted a hierarchy, with each distribution potentially fitting multiple categories. This became pretty messy pretty quickly, so I moved to a &lt;a href="https://docs.ansible.com/ansible/latest/inventory_guide/intro_inventory.html"&gt;YAML Inventory&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;debian_machines:
  hosts:
    hostname_1:
      ansible_host: "1.1.1.1"
    hostname_2:
      ansible_host: "2.2.2.2"
ubuntu_machines:
  hosts:
    hostname_3:
      ansible_host: "3.3.3.3"
apt_updates:
  children:
    debian_machines:
    ubuntu_machines:
nameservers:
  children:
    hostname_2:
    hostname_3:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allowed me to simplify my playbooks and inventory by making "groups of groups", and avoid crazy stuff like taking down all nodes for an application at once. We'll use &lt;code&gt;nameservers:&lt;/code&gt; as an example here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
- name: "Reboot APT Machines, except DNS"
  hosts: apt_updates,!nameservers
  tasks:
    - name: "Ansible Self-Test!"
      ansible.builtin.ping:
    - name: "Reboot Apt Machines!"
      ansible.builtin.reboot:
- name: "Reboot nameservers"
  hosts: nameservers
  serial: 1
  tasks:
    - name: "Ansible Self-Test!"
      ansible.builtin.ping:
    - name: "Reboot nameservers serially!"
      ansible.builtin.reboot:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;serial: 1&lt;/code&gt; key instructs the Ansible controller to only execute this playbook on one machine at a time, so DNS continuity is preserved.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retrospective
&lt;/h3&gt;

&lt;p&gt;I had several issues with this approach, but to my surprise, Linux patching and actual Ansible issues haven't cropped up at all. With most mainstream distributions, the QC must be good enough to patch nightly like this.&lt;/p&gt;

&lt;p&gt;I did have issues with inventory management, however. To update the Ansible inventory, I could deploy as-code, which was nice, but it was still clunky. If I &lt;a href="https://blog.engyak.co/2023/01/why-automate-vm-deployment-with/"&gt;deployed 5 Alpine images in a day&lt;/a&gt;, I want them to automatically be added to my inventory for maximum laziness.&lt;/p&gt;

&lt;p&gt;I also quickly discovered that maintaining Jenkins was labor-intensive. It's a truly powerful engine, and great if you need all the extra features, but there aren't many low-friction ways to automate all the required maintenance, particularly around plugins. I was able to update Jenkins &lt;em&gt;itself&lt;/em&gt; with a package manager, but it seems like every few days I had to patch plugins (manually).&lt;/p&gt;

&lt;h2&gt;
  
  
  Iteration 2: Ansible, Netbox, GitHub Actions
&lt;/h2&gt;

&lt;p&gt;I'll be up-front - for parameterized builds, GitHub Actions &lt;em&gt;is less capable.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It has some pretty big upsides, however:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You don't have to maintain the GUI &lt;em&gt;at all&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Logging is excellent&lt;/li&gt;
&lt;li&gt;Integration with GitHub is excellent&lt;/li&gt;
&lt;li&gt;Pipelines are YAML defined in their own repository&lt;/li&gt;
&lt;li&gt;Status badges in Markdown (we don't need some stinkin' badges!)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="//ansible_github_netbox.svg"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--AAxYEFvB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2024/04/patching/ansible_github_netbox.svg" alt="GitHub Actions and Netbox" width="556" height="720"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This workflow has been much smoother to operate. Since the &lt;a href="https://blog.engyak.co/2023/09/vsphere-dayn/"&gt;deployment workflow already updates Netbox&lt;/a&gt;, all machines are added to the "maintenance loop after first boot.&lt;/p&gt;

&lt;p&gt;I was really surprised at how little work was required to convert these CI pipelines. This was naive of me - &lt;strong&gt;ease of conversion is the entire point of CI pipelines&lt;/strong&gt;, but it's still mind-boggling to realize how effective it is at times.&lt;/p&gt;

&lt;p&gt;To make this work, I first needed to create a CI process in &lt;code&gt;.github/workflows&lt;/code&gt; on my Lab repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: "Nightly: @0100 Update Linux Machines"

on:
  schedule:
    - cron: "0 9 * * *"

permissions:
  contents: read

jobs:
  build:
    runs-on: self-hosted
    steps:
    - uses: actions/checkout@v3
    - name: Execute Ansible Nightly Job
      run: |
        python3 -m venv .
        source bin/activate
        python3 -m pip install --upgrade pip
        python3 -m pip install -r requirements.txt
        python3 --version
        ansible --version
        export NETBOX_TOKEN=${{ secrets.NETBOX_TOKEN }}
        export NETBOX_API=${{ vars.NETBOX_URL }}
        ansible-galaxy collection install netbox.netbox
        ansible-inventory -i local.netbox.netbox.nb_inventory.yml --graph
        ansible-playbook -i local.netbox.netbox.nb_inventory.yml lab-nightly.yml      
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This executes on a &lt;a href="https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners"&gt;GitHub Self-Hosted runner&lt;/a&gt; in my lab with a Python Virtual Environment. The workflow will run a clean build, every time - by wiping out the workspace prior to each execution. &lt;em&gt;No configuration artifacts are left behind.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;With GitHub Actions, all processes are listed alphabetically, you can't do folders and trees to keep it more organized. I developed a naming convention:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{ workflow_type }}: {{ description}}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To keep things sane.&lt;/p&gt;

&lt;p&gt;From there, we need a way to point to Netbox as an inventory source. This requires a few files:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;requirements.txt&lt;/em&gt; is the Python 3 Pip inventory - since things are running in a virtual environment, it will only use python packages in this list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
txt&lt;/p&gt;
&lt;h6&gt;
  
  
  Requirements without Version Specifiers
&lt;/h6&gt;

&lt;p&gt;pytz&lt;br&gt;
netaddr&lt;br&gt;
django&lt;br&gt;
jinja2&lt;br&gt;
requests&lt;br&gt;
pynetbox&lt;/p&gt;
&lt;h6&gt;
  
  
  Requirements with Version Specifiers
&lt;/h6&gt;

&lt;p&gt;ansible &amp;gt;= 8.4.0              # Mostly just don't use old Ansible (e.g. v2, v3)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
The next step is to build an inventory file. This has to be named specifically for the plugin to work - `local.netbox.netbox.nb_inventory.yml`:

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;plugin: netbox.netbox.nb_inventory&lt;br&gt;
validate_certs: False&lt;br&gt;
config_context: True&lt;br&gt;
group_by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tags
device_query_filters:&lt;/li&gt;
&lt;li&gt;has_primary_ip: 'true'
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
- **Not featured here - The API Endpoint and API Token directives are handled by [GitHub Actions Secrets](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions), and therefore don't need to be in this file.**

This file is pretty straightforward. It indicates that we should use Netbox tags to develop our inventory, and we can assign multiple tags in the Netbox application to each Virtual Machine. I also added the `has_primary_ip` directive - if a machine doesn't get an IP address for some reason, it won't try to reach that VM and patch it, causing late night failures.

Here's a preview of the Netbox application with these tags:

[![Netbox Preview](https://blog.engyak.co/2024/04/patching/netbox_preview.png)](netbox_preview.png)

Refactoring the Ansible playbooks was hilariously easy. The Netbox inventory plugin prepends the `group_by` field onto the group, so all I had to do in each playbook was prepend `tags_` to each name. Here's an example:

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;name: "Apt Machines"
hosts: tags_lab_apt_updates
tasks:

&lt;ul&gt;
&lt;li&gt;name: "Ansible Self-Test!"
ansible.builtin.ping:&lt;/li&gt;
&lt;li&gt;name: "Update Apt!"
ansible.builtin.apt:
name: "*"
state: latest
update_cache: true&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;name: "Apk Machines"
hosts: tags_lab_apk_updates
tasks:

&lt;ul&gt;
&lt;li&gt;name: "Ansible Self-Test!"
ansible.builtin.ping:&lt;/li&gt;
&lt;li&gt;name: "Update Apk!"
ansible.builtin.apt:
available: true
upgrade: true
update_cache: true&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


After that, the CI tooling just takes care of it all for me!

### Retrospective

I'm going to stick with this method for a while. Netbox tagging makes inventory management much more intuitive, and I can develop tag "pre-sets" in my deployment pipeline to correctly categorize all the _stuff_ I deploy. Since it's effectively documentation, I have an easy place to put data I'll need to find later for those "what was I thinking?" moments.

I'll be honest - behind that, I haven't really given it much thought. This approach requires zero attention to continue, and it happens while I sleep. I haven't gotten any problems from it, and it allows me to focus my free time on things that are more important.

10/10 would recommend.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
    </item>
    <item>
      <title>Abstracting DNS Record Management with Ansible and Jinja 2</title>
      <dc:creator>Nick Schmidt</dc:creator>
      <pubDate>Sat, 06 Jan 2024 09:00:00 +0000</pubDate>
      <link>https://dev.to/ngschmidt/abstracting-dns-record-management-with-ansible-and-jinja-2-7mf</link>
      <guid>https://dev.to/ngschmidt/abstracting-dns-record-management-with-ansible-and-jinja-2-7mf</guid>
      <description>&lt;p&gt;Synchronizing properly implemented DNS zones is, to put it lightly, a &lt;em&gt;real chore&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating &lt;em&gt;forward&lt;/em&gt; DNS entries, e.g. &lt;code&gt;A&lt;/code&gt;, &lt;code&gt;AAAA&lt;/code&gt;, &lt;code&gt;CNAME&lt;/code&gt;. These names are used to resolve to resources.&lt;/li&gt;
&lt;li&gt;Creating &lt;em&gt;reverse&lt;/em&gt; DNS entries, e.g. &lt;code&gt;PTR&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Creating DNS entries that define the zone, e.g. &lt;code&gt;SOA&lt;/code&gt;, &lt;code&gt;NS&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a system to behave properly, your &lt;em&gt;forward&lt;/em&gt; and &lt;em&gt;reverse&lt;/em&gt; entries need to be identical, but software like BIND/Unbound rely on zonefiles that don't connect the two. Many information systems / DNS zones exist with improperly implemented &lt;em&gt;reverse&lt;/em&gt; DNS, or partially implemented &lt;em&gt;forward&lt;/em&gt; DNS asymptomatically for a time. Certain events (e.g. CA validation, discovery, implementing IPv6) can bring things to the forefront if ordinary network management practice doesn't.&lt;/p&gt;

&lt;p&gt;For this post, we'll first work on &lt;em&gt;abstracting&lt;/em&gt; the DNS zonefile - ensuring that a user can deploy zonefiles conformant to a standard - and then we'll illustrate how that can be used with Netbox to automatically populate DNS entries from Netbox.&lt;/p&gt;

&lt;p&gt;Abstracting the zonefile here will achieve a few goals - but the file size is &lt;em&gt;guaranteed&lt;/em&gt; to be longer than if you simply managed the zone files from source. Here are some advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This pipeline &lt;strong&gt;ABSOLUTELY MUST&lt;/strong&gt; establish forward and reverse records &lt;strong&gt;from the same data!&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;This pipeline &lt;strong&gt;must&lt;/strong&gt; test zonefiles, and avoid installing them if they aren't good (prevents outages)&lt;/li&gt;
&lt;li&gt;This pipeline &lt;strong&gt;must&lt;/strong&gt; establish documentation standards for a DNS zone (abstract the standard)&lt;/li&gt;
&lt;li&gt;This pipeline &lt;strong&gt;must&lt;/strong&gt; scale to support large quantities of DNS zones / records&lt;/li&gt;
&lt;li&gt;This pipeline &lt;strong&gt;must&lt;/strong&gt; be easy to use, even with inexperienced DNS administrators (we can't have it all be on the shoulders of &lt;em&gt;that one guy who can safely make DNS changes&lt;/em&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To achieve this, we'll first establish a YAML schema and Jinja2 template to structure the data. Here's the YAML schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 1zones:
 2 - name: filename
 3 zonename:
 4 soa:
 5 settings:
 6 ttl:
 7 serial:
 8 refresh:
 9 retry:
10 expiry:
11 nameservers: []
12 reverse_zones:
13 ip4:
14 ip6:
15 records: [{ "name": "", "type": "", "addr": ""}]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are also some subtle differences between IPv4 and IPv6 reverse zones, so in this case, we're going to use three Jinja2 templates (in the Gist below).&lt;/p&gt;

&lt;p&gt;It also assumes that there's a dedicated &lt;strong&gt;classful&lt;/strong&gt; prefix for each DNS zone. This isn't always true for more complex deployments, but they can also do stuff like buy Infoblox.&lt;/p&gt;

&lt;p&gt;I have also included a GitHub Action in the gist, because it provides a good place to demostrate best practices (e.g. using &lt;code&gt;venv&lt;/code&gt;) in one compact place. If you want to install generated zone files on-premises, you can run this on a self-hosted runner with an Ansible inventory group (e.g. &lt;code&gt;nameservers&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;It's still a little clunky, the next step should help with that (harvesting DDI information from Netbox IPAM data).&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@import url('https://cdn.rawgit.com/lonekorean/gist-syntax-themes/d49b91b3/stylesheets/idle-fingers.css');

@import url('https://fonts.googleapis.com/css?family=Open+Sans');
body {
  font: 16px 'Open Sans', sans-serif;
}
body .gist .gist-file {
  border-color: #555 #555 #444
}
body .gist .gist-data {
  border-color: #555
}
body .gist .gist-meta {
  color: #ffffff;
  background: #373737; 
}
body .gist .gist-meta a {
  color: #ffffff
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;a href="https://gist.github.com/ngschmidt/d0862985e382b052fd3f42bbc4082af3"&gt;GitHub Link&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Build and Consume Alpine Linux vSphere Images</title>
      <dc:creator>Nick Schmidt</dc:creator>
      <pubDate>Sun, 24 Dec 2023 09:00:00 +0000</pubDate>
      <link>https://dev.to/ngschmidt/build-and-consume-alpine-linux-vsphere-images-3g78</link>
      <guid>https://dev.to/ngschmidt/build-and-consume-alpine-linux-vsphere-images-3g78</guid>
      <description>&lt;h2&gt;
  
  
  Deploying Linux for the impatient
&lt;/h2&gt;

&lt;p&gt;If you've ever wanted to just "test something out really quick" in a live environment, Linux distributions have always been generally lightweight, but that's not the only implicit requirement for experimentation.&lt;/p&gt;

&lt;p&gt;A Linux IaaS distribution should be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reasonably secure (basic hardening applied, fewer packages == fewer vulnerabilities)&lt;/li&gt;
&lt;li&gt;Light on disk usage (shortening deployment times)&lt;/li&gt;
&lt;li&gt;Light on system resources, e.g. CPU/Memory&lt;/li&gt;
&lt;li&gt;Flexible (supports a package manager with a wide ecosystem of packages)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Package flexibility is usually the compromise made here - but when you're deploying programmable code, container images and virtual environments like Python's &lt;code&gt;venv&lt;/code&gt; should be able to bridge &lt;em&gt;some&lt;/em&gt; gaps.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.alpinelinux.org/about/"&gt;Alpine Linux&lt;/a&gt; focuses on these goals - but doesn't compromise on automation. Combined with a &lt;a href="https://blog.engyak.co/2023/09/vsphere-dayn/"&gt;dynamic inventory bootstrapping process&lt;/a&gt;, it's relatively straightforward to bring Alpine's &lt;a href="https://docs.ansible.com/ansible/latest/collections/community/general/apk_module.html"&gt;APK ansible module&lt;/a&gt; into play to build any extra software on a new machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Customizing and building the ISO
&lt;/h3&gt;

&lt;p&gt;First, let's upload the ISO to a datastore:&lt;/p&gt;

&lt;p&gt;&lt;a href="//1_addiso.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fsi7fUhR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/12/alpine/1_addiso.png" alt="Add ISO Image" width="800" height="205"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's create a new Virtual Machine. We'll attach the ISO to it&lt;/p&gt;

&lt;p&gt;&lt;a href="//2_createvm.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--M8VSU_P6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/12/alpine/2_createvm.png" alt="Create VM from cluster" width="351" height="165"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="//3_names.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0ihKKihy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/12/alpine/3_names.png" alt="Set VM Name" width="364" height="232"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Target any storage and compute as preferred. SSD datastores will be faster, of course.&lt;/p&gt;

&lt;p&gt;vSphere 8.0 Update 2 doesn't have a preset for Alpine Linux, and the guest OS options are &lt;em&gt;important&lt;/em&gt; - it defines what paravirtualized hardware is available:&lt;/p&gt;

&lt;p&gt;&lt;a href="//4_guest.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--merUN1On--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/12/alpine/4_guest.png" alt="Guest OS Customization: Linux Other 5.x (64 Bit)" width="787" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ensure that VMXNET 3 and PVSCSI are both available. The "New Network" will become the default port-group assigned to the template.&lt;/p&gt;

&lt;p&gt;CPU/Memory are mostly irrelevant, as the deployment pipeline can customize afterwards - and this OS doesn't need much in terms of resources:&lt;/p&gt;

&lt;p&gt;&lt;a href="//5_hardware.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Ndfla6CA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/12/alpine/5_hardware.png" alt="Guest Hardware Customization" width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Select the Alpine "Datastore ISO" and enable "Connect and Power On" for the assigned CD/DVD drive:&lt;/p&gt;

&lt;p&gt;&lt;a href="//6_iso.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zfLfvgkb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/12/alpine/6_iso.png" alt="ISO Configuration" width="547" height="225"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Start the machine - it'll boot to a command prompt &lt;em&gt;very quickly&lt;/em&gt;. Log in as root:&lt;/p&gt;

&lt;p&gt;&lt;a href="//7_alpine_start.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--OwNFW3a---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/12/alpine/7_alpine_start.png" alt="Start Alpine" width="800" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://wiki.alpinelinux.org/wiki/Installation"&gt;installation guide&lt;/a&gt; indicates to use the &lt;code&gt;setup-alpine&lt;/code&gt; script, and follow the prompts:&lt;/p&gt;

&lt;p&gt;&lt;a href="//8_alpine_setup.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--MsBW4fK5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/12/alpine/8_alpine_setup.png" alt="Alpine Setup" width="546" height="657"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The majority of setup here is extremely simple - because it's not installing a bunch of software. GUIs are also possible after the installation is complete - but it does defeat the point.&lt;/p&gt;

&lt;p&gt;Instead of rebooting as instructed, shut the virtual machine down and delete the disk drive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Note: The shutdown process isn't installed with Alpine, and the following command &lt;em&gt;does&lt;/em&gt; execute a graceful shutdown!&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;shutdown now
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="//9_remove_iso.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--izzhz73B--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/12/alpine/9_remove_iso.png" alt="Remove CD/DVD-ROM drive" width="800" height="652"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Start the machine up - we'll want to add some quality of life improvements to this machine like guest tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{ insert favorite editor here }} /etc/apk/repositories
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remove the &lt;code&gt;#&lt;/code&gt; from the line ending in &lt;code&gt;/alpine/v{{ version }}/community&lt;/code&gt; and save.&lt;/p&gt;

&lt;p&gt;Per &lt;a href="https://wiki.alpinelinux.org/wiki/Open-vm-tools"&gt;Alpine's guide&lt;/a&gt;, install and enable open-vm-tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apk add open-vm-tools open-vm-tools-guestinfo open-vm-tools-deploypkg
rc-service open-vm-tools start
rc-update add open-vm-tools boot

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once running, ensure that guest power actions are available:&lt;/p&gt;

&lt;p&gt;&lt;a href="//10_test_guest.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--D8BlsNdV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/12/alpine/10_test_guest.png" alt="Guest Power Actions" width="572" height="306"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Personally, I like testing, so instead of powering off the VM, I use the guest action to ensure everything is working. Either way, shut the VM down.&lt;/p&gt;

&lt;p&gt;Hit "Actions" on the VM, or right-click it, and select "Clone → Clone as Template to Library":&lt;/p&gt;

&lt;p&gt;&lt;a href="//11_clone_to_library.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--b2-SzyEN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/12/alpine/11_clone_to_library.png" alt="Clone as Template to Library" width="549" height="313"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Select whatever storage backing and content libraries are preferable at this point. It won't take long to clone in. Delete the old VM whenever it makes sense, I usually do so after testing a deployment:&lt;/p&gt;

&lt;p&gt;&lt;a href="//12_deploy_test.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--u8cD4cea--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/12/alpine/12_deploy_test.png" alt="Deploy Test VM" width="399" height="107"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Note: Set "Power on VM after creation" - this will clone &lt;em&gt;extremely&lt;/em&gt; quickly and boot even faster.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Modifying the deployment pipeline
&lt;/h3&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@import url('https://cdn.rawgit.com/lonekorean/gist-syntax-themes/d49b91b3/stylesheets/idle-fingers.css');

@import url('https://fonts.googleapis.com/css?family=Open+Sans');
body {
  font: 16px 'Open Sans', sans-serif;
}
body .gist .gist-file {
  border-color: #555 #555 #444
}
body .gist .gist-data {
  border-color: #555
}
body .gist .gist-meta {
  color: #ffffff;
  background: #373737; 
}
body .gist .gist-meta a {
  color: #ffffff
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The deployment pipeline code itself is available &lt;a href="https://gist.github.com/ngschmidt/88fe09a1c5733735a4232dd24c44f78e"&gt;here&lt;/a&gt;. I've made some modifications from previous versions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub Actions now supports the &lt;code&gt;choice&lt;/code&gt; type, which means we can select UUIDs. There isn't a way to build a "friendly name" mapping. We achieve this by creating a "lookup dictionary" with the friendly name as a key and a UUID as the value. This list will need to be populated via data collection (featured below).
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;First, we'll need to find out what the UUID of the template and cluster are. &lt;a href="https://gist.github.com/ngschmidt/0c7687cb62ba6f7bb98feb67ff936906"&gt;Here's an example&lt;/a&gt; to collect the required information. The UUIDs of system resources (and this template) are only available via the API. Use this information to form the &lt;code&gt;parameters.yml file created in the GitHub Action workflow&lt;/code&gt;, e.g. &lt;code&gt;datastore&lt;/code&gt;, &lt;code&gt;cluster&lt;/code&gt;, &lt;code&gt;folder&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Adjusting and running this workflow will allow an engineer to populate the previous workflow and expose vSphere assets to further deployment automation!&lt;/p&gt;

&lt;p&gt;For reference, this machine deployed in about 3 seconds on a shared SSD (iSCSI):&lt;/p&gt;

&lt;p&gt;&lt;a href="//13_deployment_benchmark.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Xo508zu9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/12/alpine/13_deployment_benchmark.png" alt="Deployment timeline" width="639" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The GitHub workflow takes &amp;gt;2 minutes to complete, but the workflow it's attached to has manual wait step:&lt;/p&gt;

&lt;p&gt;&lt;a href="//14_actions.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fpAkq_ED--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/12/alpine/14_actions.png" alt="GitHub Actions" width="678" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Apollo 13's "Failure is not an option", and how non-engineers misinterpret it</title>
      <dc:creator>Nick Schmidt</dc:creator>
      <pubDate>Sat, 25 Nov 2023 09:00:00 +0000</pubDate>
      <link>https://dev.to/ngschmidt/apollo-13s-failure-is-not-an-option-and-how-non-engineers-misinterpret-it-4g2g</link>
      <guid>https://dev.to/ngschmidt/apollo-13s-failure-is-not-an-option-and-how-non-engineers-misinterpret-it-4g2g</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Failure is not an option!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It might surprise you to know that this quote wasn't real - it &lt;em&gt;feels&lt;/em&gt; legendary, but was never said by Gene Kranz. &lt;a href="https://web.archive.org/web/20100123160551/http://www.spaceacts.com/notanoption.htm"&gt;It was written up for the film.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The aerospace engineering discipline isn't really something everybody gets to experience, so it makes sense that "spicing things up" for the movie would be generally accepted as reality.&lt;/p&gt;

&lt;p&gt;When you create a program (or release a new capability), it makes perfect sense to get all excited and release it as soon as you feel it's "done" - but this is just an example of how IT/Computer Science is relatively young compared to other engineering disciplines.&lt;/p&gt;

&lt;p&gt;With more traditional engineering disciplines, &lt;em&gt;testing&lt;/em&gt; is a key aspect to deployment and design. Everything is tested for safety. Concrete is thoroughly &lt;a href="https://www.astm.org/products-services/standards-and-publications/standards/cement-standards-and-concrete-standards.html"&gt;tested before integration in bridges and structures&lt;/a&gt;. &lt;em&gt;Most&lt;/em&gt; pickup trucks are tested &lt;a href="https://www.sae.org/standards/content/j2807_202002/"&gt;to their listed tow capacity&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This isn't a perfect ideal world, however. Bridges still fail, and in this case companies didn't follow the SAE J2807 standard until forced (Toyota: 2011, General Motors: 2015, Ford: 2015, Dodge: 2015).&lt;/p&gt;

&lt;h2&gt;
  
  
  Industry-wide changes take time
&lt;/h2&gt;

&lt;p&gt;Here's why: It's expensive to re-tool in the physical world. NASA just straight up didn't have the option, so they compensated by accounting for as many potential scenarios as possible, at the expense of cost. That's what "Failure is not an option" was intended to reflect. Everything is tested and planned ahead of time, and the mission systems didn't try anything truly new.&lt;/p&gt;

&lt;p&gt;Engineering is the practice of taking learned experiences and codifying them, ensuring that the same mistake doesn't happen twice. The safety codes and engineering artifacts we use in the physical world are "written in blood" - many structural engineering practices were learnt from a loss of life, it's why they're so important.&lt;/p&gt;

&lt;p&gt;I don't think anybody has died due to an email not getting through, but I'd counter that the same practices are &lt;em&gt;much&lt;/em&gt; easier to execute in IT and therefore should be followed. IT is a relatively young engineering-adjacent discipline, and the standards for performance are relatively low, albeit always increasing.&lt;/p&gt;

&lt;p&gt;Here's a rough estimate of each engineering discipline's age:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chemical Engineering (~1800s AD)&lt;/li&gt;
&lt;li&gt;Civil Engineering (BC, formalized in the 1700s AD)&lt;/li&gt;
&lt;li&gt;Electrical Engineering (1700s AD, formalized in the 1800s AD)&lt;/li&gt;
&lt;li&gt;Mechanical Engineering (BC, formalized in the 1800s AD)&lt;/li&gt;
&lt;li&gt;Software Engineering (1960s AD)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More recent engineering disciplines fit in these families, and one could argue (correctly) that while they are younger, they benefit from the preceding disciplines and the broader body of knowledge. This is particularly true in the field of aerospace.&lt;/p&gt;

&lt;p&gt;Systems Engineering practitioners have collected a number of practices together to integrate new technologies and disciplines in the &lt;a href="https://sebokwiki.org/wiki/Guide_to_the_Systems_Engineering_Body_of_Knowledge_(SEBoK)"&gt;SEBoK&lt;/a&gt; - which essentially forms a "starter kit" of practices and protocols for developing new solutions. The SEBoK is an excellent (albeit overwhelming) place to procure methods for continuous improvement, either as a team or individually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't fear failure, understand it
&lt;/h2&gt;

&lt;p&gt;Across all of these disciplines, we see a common pattern around failure; the natural reaction to failure is to avoid it. Humans don't want to be associated with failure, and this reflex must be overridden to be a successful engineer.&lt;/p&gt;

&lt;p&gt;I'd like to provide an example of good failure analysis instead of harping on past failures - my concern here is that any controversy may get in the way of the idea I want to convey - which deviates from the practice of &lt;a href="https://www.sciencedirect.com/topics/engineering/failure-analysis"&gt;failure analysis&lt;/a&gt; somewhat.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.wsdot.wa.gov/tnbhistory/collapse.htm"&gt;Washington State DOT's analysis of the Tacoma Narrows bridge failure&lt;/a&gt; is an example of well-executed failure analysis.&lt;/p&gt;

&lt;p&gt;In this case, the structure was too rigid - "common sense" would tell us that if a bridge is extremely strong, it won't have any issues standing up to high winds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applying failure analysis to IT
&lt;/h2&gt;

&lt;p&gt;It's important that we learn from these shortcomings and integrate solutions into future designs. Typically, this is where "system integration" comes into play - as a product is validated for release, all known tests are applied to it to ensure that failures don't recur. The NASA engineers supporting Apollo 13 didn't try &lt;em&gt;anything&lt;/em&gt; new on the mission system (Apollo 13 itself). NASA tested all solutions &lt;em&gt;thoroughly&lt;/em&gt; with the ground crew, astronauts, and QA engineers before rollout was ever considered an option.&lt;/p&gt;

&lt;p&gt;The Apollo program was extremely expensive compared to most of our IT budgets, but we're almost always testing &lt;em&gt;software&lt;/em&gt;. Failure Analysis practices are trivial with software debugging and mature unit testing, and eventually we're going to have to perform at the standards held by traditional engineering disciplines.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example - a maintenance window backfired
&lt;/h3&gt;

&lt;p&gt;We've all been here before - let's say that spanning tree did something unexpected during a maintenance window and caused unexpected downtime.&lt;/p&gt;

&lt;p&gt;The first and most effective aspect of failure analysis (at least for our careers) is to provide a compelling narrative. We need to invert the human reflexive reaction to failure and encourage interest over punitive behaviors. Writing a complete and compelling narrative both ensures that people will react more positively to the occurrence and provide confidence that due diligence will be performed to ensure it doesn't happen again.&lt;/p&gt;

&lt;p&gt;Sure, it'll always happen again with STP in some way, but other materials have common patterns and properties too. We didn't stop using aluminum because it isn't as strong as steel or as good of a conductor as copper; instead we learned its strengths and weaknesses, applying the solution judiciously. In this case, we need to prove that we will apply the solution more judiciously as well.&lt;/p&gt;

&lt;p&gt;Second, gather all possible data on the time of the outage. Don't try to filter it yet, and don't react slowly. Anything that can record system data is valuable here (telemetry in particular) - so automatic gathering is &lt;em&gt;extremely&lt;/em&gt; valuable.&lt;/p&gt;

&lt;p&gt;Third, find ways to locate precursors and the failure itself. This part should be automated and attached to any CI pipelines for the future, "set it and forget it" is the best way. As this practice evolves, a solution develops incredible mass and manually executing every failure analysis unit test after every change will quickly become tedious and slow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why?
&lt;/h2&gt;

&lt;p&gt;The pressure to follow this pattern is only going to grow in the future. The previous decade's reliability standards were hilariously low compared to the quality of technology and service today - just look at the standards people hold us to. Instead of fearing this trend, let's analyze it and find ways to improve. It'll give us a competitive edge in the future.&lt;/p&gt;

&lt;p&gt;As with Apollo 13, our greatest failures drive our greatest successes.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Internet Load Balancing with pfSense</title>
      <dc:creator>Nick Schmidt</dc:creator>
      <pubDate>Sun, 08 Oct 2023 09:00:00 +0000</pubDate>
      <link>https://dev.to/ngschmidt/internet-load-balancing-with-pfsense-3322</link>
      <guid>https://dev.to/ngschmidt/internet-load-balancing-with-pfsense-3322</guid>
      <description>&lt;h2&gt;
  
  
  With full-time remote work, internet outages transform from a nuisance to a real problem
&lt;/h2&gt;

&lt;p&gt;Prior to the pandemic, "working hours" were typically considered fair game by internet service providers to schedule necessary system maintenance. It's unrealistic to expect perfect uptime from any service provider - as the saying goes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Schedule maintenance on your equipment before your equipment schedules it for you!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;ISPs are terrible about this, mostly because "old and stable" means customers receive reliable service. Eventually that trusty Toyota Corolla dies, though, causing severe customer impact.&lt;/p&gt;

&lt;p&gt;I'd suggest taking matters into your own hands here. The technologies involved in internet load balancing are fairly complex, but if you follow a known formula it's doable for most tech-savvy users.&lt;/p&gt;

&lt;h3&gt;
  
  
  Internet Load Balancing
&lt;/h3&gt;

&lt;p&gt;Load balancing network traffic is traditionally a separate domain from routing and firewalling, with most of the general industry focus centering around Server Load Balancing (SLB). An Internet Load Balancer (IPv4) needs to provide the following functions reliably:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Monitor each available path for viability with some form of end-to-end test.&lt;/li&gt;
&lt;li&gt;Evenly (or with a ratio) balance new flows between each available path.&lt;/li&gt;
&lt;li&gt;Track related sessions and place "affinity" to a specific path, ensuring that protocols like RTP + RTCP work&lt;/li&gt;
&lt;li&gt;NAT Outbound traffic for its relevant link (IPv4)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To clarify, this doesn't cover SD-WAN and why it's more effective. Per-packet assessment and FEC lead to a &lt;em&gt;much&lt;/em&gt; higher quality user experience and can achieve much cleaner ratios than what I provide below, but home users typically have high individual bandwidth with their internet services and like the concept of using them to their fullest. If the connectivity options at home are sufficiently mismatched or slow, it would be worthwhile to take SD-WAN solutions into consideration.&lt;/p&gt;

&lt;p&gt;Let's establish an example topology, and cover the tunables that will provide a "good enough" WAN load balancing solution that centers around minimizing impact to remote work:&lt;/p&gt;

&lt;p&gt;[&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--oi01Fkyh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/10/internet-lb/wan-example.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--oi01Fkyh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/10/internet-lb/wan-example.svg" alt="WAN Redundancy Scenario" width="527" height="928"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;](wan-example.svg)&lt;/p&gt;

&lt;p&gt;In this scenario, we'll just assume that one's wireline and one isn't to make things easy to explain. The transport doesn't really matter much, but it simplifies any documentation from here on out.&lt;/p&gt;

&lt;p&gt;First, let's assign a new interface for the second internet link, and configure it for DHCP. This menu can be found under &lt;strong&gt;Interfaces ⇾ Assignments&lt;/strong&gt; :&lt;/p&gt;

&lt;p&gt;[&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--04l6KsDw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/10/internet-lb/pfsense_01_interfaces.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--04l6KsDw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/10/internet-lb/pfsense_01_interfaces.png" alt="pfSense Interface Assignment" width="425" height="301"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;](pfsense_01_interfaces.png)&lt;/p&gt;

&lt;p&gt;[&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--OLAjj3L4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/10/internet-lb/pfsense_02_interfaces.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--OLAjj3L4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/10/internet-lb/pfsense_02_interfaces.png" alt="pfSense Interface Assignment" width="682" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;](pfsense_02_interfaces.png)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note: Ensure that "Block private networks and loopback addresses" and "Block bogon networks" are checked. This is a WAN link, after all.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When using DHCP, the secondary WAN link &lt;em&gt;should&lt;/em&gt; automatically install a "gateway", but it won't load balance just yet. We need to create a &lt;strong&gt;Gateway Group&lt;/strong&gt; to enforce load balancing policies, and then assign it as the default gateway for things to take effect:&lt;/p&gt;

&lt;p&gt;[&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Y74RzItm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/10/internet-lb/pfsense_03_gateways.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Y74RzItm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/10/internet-lb/pfsense_03_gateways.png" alt="pfSense Gateway Creation" width="800" height="212"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;](pfsense_03_gateways.png)&lt;/p&gt;

&lt;p&gt;[&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--lOO6Eu-n--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/10/internet-lb/pfsense_04_gateways.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--lOO6Eu-n--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/10/internet-lb/pfsense_04_gateways.png" alt="pfSense Gateway Creation" width="800" height="516"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;](pfsense_04_gateways.png)&lt;/p&gt;

&lt;p&gt;[&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bwinY7EU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/10/internet-lb/pfsense_05_gateways.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bwinY7EU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/10/internet-lb/pfsense_05_gateways.png" alt="pfSense Gateway Creation" width="680" height="231"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;](pfsense_05_gateways.png)&lt;/p&gt;

&lt;p&gt;Now, let's create monitoring IPs so pfSense can periodically test for packet loss or latency on that link. The following menu is available by editing the service provider gateway under &lt;strong&gt;System → Routing → Gateways&lt;/strong&gt; :&lt;/p&gt;

&lt;p&gt;[&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--KbSNGWZu--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/10/internet-lb/pfsense_06_gateways.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KbSNGWZu--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/10/internet-lb/pfsense_06_gateways.png" alt="pfSense Gateway Monitoring" width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;](pfsense_06_gateways.png)&lt;/p&gt;

&lt;p&gt;I'd suggest using the Service Provider's DNS services or an anycast DNS provider you &lt;em&gt;don't typically use&lt;/em&gt; for the monitor addresses. pfSense installs a static route via that WAN for the monitor address, which means that it'll go down with the WAN link.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note: Duplicate this IP and create a DNS monitor with &lt;a href="https://github.com/louislam/uptime-kuma"&gt;Uptime Kuma&lt;/a&gt; if you want to monitor per-provider reliably. It's quick and easy!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is all that's required, assuming that you want to get the most even load balancing possible. Here are a few tunables that may apply to more specific scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pfSense won't load balance asymmetric link speeds by default. If the interface speeds are different, you will need to create a policy-based routing rule ( &lt;strong&gt;Firewall → Rules → LAN → New rule&lt;/strong&gt; ), and modify the &lt;em&gt;Advanced Option&lt;/em&gt; &lt;strong&gt;Gateway&lt;/strong&gt; : [&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7bRAFDfL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/10/internet-lb/pfsense_07_pbf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7bRAFDfL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/10/internet-lb/pfsense_07_pbf.png" alt="pfSense Gateway PBF" width="746" height="83"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;](pfsense_07_pbf.png)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;While editing the gateway ( &lt;strong&gt;System → Routing → Gateways&lt;/strong&gt; ), look for an &lt;em&gt;Advanced&lt;/em&gt; setting labeled &lt;strong&gt;Weight&lt;/strong&gt;. This will allow you to set a ratio between gateway groups, e.g. &lt;code&gt;2:1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;pfSense provides a simplified persistence mechanism that will pin each client to a specific WAN link. This is important, particularly if your remote work situation requires comprehensive use of voice and video services like Zoom or Teams. Please note that this feature &lt;em&gt;will&lt;/em&gt; impact load balancing evenness to a great degree! [&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Y1rBEsLl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/10/internet-lb/pfsense_08_sticky.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Y1rBEsLl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/10/internet-lb/pfsense_08_sticky.png" alt="pfSense Sticky Sessions" width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;](pfsense_08_sticky.png)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pfSense provides gateway status under &lt;strong&gt;Status → Gateways&lt;/strong&gt; , but I haven't found a way to externally track those statistics via SNMP.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Internet and its scaling issues
&lt;/h3&gt;

&lt;p&gt;We've created a problem with global routing that is just plain fascinating.&lt;/p&gt;

&lt;p&gt;Network Address Translation allows us to "spoof" our internal private networks with multiple public prefixes. This both solves and creates problems - as an upside, we're able to leverage WAN redundancy with service-provider public IPv4 addressing &lt;em&gt;somewhat&lt;/em&gt; easily. This matters, because the public internet routing table currently can't support a designated prefix for every home network, and we're already experiencing internet availability issues due to route propagation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In August 2014 we celebrated "512k Day" by enjoying a number of network outages related to TCAM capacity worldwide: &lt;a href="https://www.prodriveit.co.uk/blog/the-day-the-internet-broke-512k-day"&gt;link&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The Internet Service Providers (ISPs) at the time managed to postpone this issue by "carving" TCAM, re-allocating ternary memory from other purposes to postpone the doomsday clock. This provided a capacity of 256,000 routes, but the clock was ticking. &lt;a href="https://www.thousandeyes.com/blog/what-is-768k-day"&gt;This bought ~ 5 years of time&lt;/a&gt;, and this was generally enough time to bump up capacity and lifecycle hardware.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, we have a new problem.&lt;/p&gt;

&lt;p&gt;IPv4 routes consume 64 bits (8 bytes) of memory each assuming that no hardware optimization is used (you can store the number 1-32 as a 5-bit integer, but route lookups would be a multi-pass operation / require a lookup table), resulting in an internet routing table size of &lt;strong&gt;4 Megabytes&lt;/strong&gt; on 512k day, or &lt;strong&gt;6 Megabytes&lt;/strong&gt; on 768k day. It doesn't sound like much, but TCAM is designed for fast lookup and is somewhat limited in capacity.&lt;/p&gt;

&lt;p&gt;IPv6 requires 256 bits (32 bytes) of storage per prefix, but more cleanly summarizes. Apples-to apples at a million routes would be &lt;strong&gt;8 Megabytes (IPv4) + 32 Megabytes (IPv6), or 40 MB of TCAM&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Most of the absolute latest networking hardware is up to the task, but this is also with decades of hacks and best-practice engineering optimizing it. If I, as a household, establish my own /64, it's not that much of a problem, but every other network doing so would result in a table exponentially larger than hardware today can handle. This generally violates the design principle of "prefix summarization is hiding useful information," but it's driven by hardware limitations (as it &lt;em&gt;always&lt;/em&gt; has).&lt;/p&gt;

&lt;h3&gt;
  
  
  The Future (IPv6) Solution
&lt;/h3&gt;

&lt;p&gt;Interestingly enough, IPv6 is well-suited to this solution, and simple. Endpoints typically have &lt;em&gt;tons&lt;/em&gt; of compute resources available for simple tasks like internet load balancing - but the client &lt;em&gt;software&lt;/em&gt; isn't quite up to snuff. A dynamic IPv6 network leverages Router Advertisements and DHCPv6 to configure host devices with DNS and IP addresses, and there is &lt;em&gt;nothing&lt;/em&gt; restricting multiple routers from advertising multiple prefixes over the same network:&lt;/p&gt;

&lt;p&gt;[&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--JTqN6sok--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/10/internet-lb/ipv6-future.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--JTqN6sok--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/10/internet-lb/ipv6-future.svg" alt="IPv6 With Multiple Router Advertisements" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;](ipv6-future.svg)&lt;/p&gt;

&lt;p&gt;Well, nothing except our own internal limitations, and client software. This would require a client device to automatically test each "path" and decide which one to use for a given application. We're not quite there yet, but the key elements are in place to guarantee a much higher service quality than our core and home routers can execute.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retrospectives
&lt;/h3&gt;

&lt;p&gt;While researching this topic, I discovered a few things that might be good for budget-conscious or hands-on users:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You &lt;em&gt;don't need&lt;/em&gt; the biggest internet plan from each service provider. While 2 500 Megabit plans are definitely not going to be equal to a gigabit service, a typical household only uses a few megabits at a time. Right-sizing your internet services will save some serious cash, and may be cheaper than the single provider plan!&lt;/li&gt;
&lt;li&gt;Capped services can be reduced to a lower ratio, or shut off entirely when the cap is reached. This approach is particularly appealing if services in your area are capped, because bandwidth caps are a tiny fraction of your link speed, and pfSense will average out your ratio rather effectively with more diverse usage.&lt;/li&gt;
&lt;li&gt;Purchase an appliance with at least &lt;strong&gt;four&lt;/strong&gt; ethernet ports! If a second service provider makes sense, it's entirely possible that a third may become an option.&lt;/li&gt;
&lt;li&gt;If your ISP provides notice of maintenance, it's trivial to disable a gateway temporarily( &lt;strong&gt;System → Routing → Gateways → Edit&lt;/strong&gt; ): &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Ne28I9mL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/10/internet-lb/pfsense_09_disable.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Ne28I9mL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/10/internet-lb/pfsense_09_disable.png" alt="Disable Gateway" width="456" height="54"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Site-to-Site VPNs will need to be pinned to a specific WAN link via static routes, or by using dynamic tunnel IDs (&lt;em&gt;not IP address identities!&lt;/em&gt;)

&lt;ul&gt;
&lt;li&gt;Transport &lt;em&gt;within&lt;/em&gt; a service provider will typically have much higher available bandwidth and lower latency than transport crossing multiple ISPs. If a site is important, try to match the service providers on both sides and run a tunnel per service provider for best results.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Handoff to Day-N Automation with vSphere Content Libraries and Netbox</title>
      <dc:creator>Nick Schmidt</dc:creator>
      <pubDate>Sat, 30 Sep 2023 09:00:00 +0000</pubDate>
      <link>https://dev.to/ngschmidt/handoff-to-day-n-automation-with-vsphere-content-libraries-and-netbox-546</link>
      <guid>https://dev.to/ngschmidt/handoff-to-day-n-automation-with-vsphere-content-libraries-and-netbox-546</guid>
      <description>&lt;h2&gt;
  
  
  The challenge with build automation is &lt;em&gt;too much convenience&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Think about it. If it's easy to compose and deploy workloads, it's also easy to develop sprawl, and a good system designer would have methods in place to mitigate that.&lt;/p&gt;

&lt;p&gt;In a previous post I covered &lt;a href="https://blog.engyak.co/2023/02/deploy-vsphere-vms-with-ansible/"&gt;how to deploy vSphere VMs with Ansible&lt;/a&gt; and the Automation Value Proposition that comes with it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--nPaCRgC9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/images/avp_9570184842259471803.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--nPaCRgC9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/images/avp_9570184842259471803.png" alt="Automation Value Proposition" width="706" height="141"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Providing this capability to a company as-is is hazardous. Ask the following questions, in rough order of priority:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How do we track decommissions/unused machines?&lt;/li&gt;
&lt;li&gt;How do we track who owns / uses what?&lt;/li&gt;
&lt;li&gt;How do we track what OS images are end-of-life?&lt;/li&gt;
&lt;li&gt;How do we track resource consumption (e.g. IP usage) and avoid re-using addresses?&lt;/li&gt;
&lt;li&gt;How do we track certificates?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;VMs also don't do much good without customization, unless you're comfortable handing those root credentials to whomever wants them.&lt;/p&gt;

&lt;h3&gt;
  
  
  System Integration
&lt;/h3&gt;

&lt;p&gt;Linux heads live for this type of work - we return to the Unix design principles where a system or subsystem should excel at a single task instead of solving all possible issues at the expense of quality.&lt;/p&gt;

&lt;p&gt;Let's explore a multi-system integration:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--3RNEXlh8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/09/vsphere-dayn/integration.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3RNEXlh8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/09/vsphere-dayn/integration.svg" alt="Integration Diagram" width="800" height="591"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For this example, we'll re-implement the previous VM build process, but orchestrate it with &lt;a href="https://docs.github.com/en/actions"&gt;GitHub Actions&lt;/a&gt;. I'll provide a &lt;code&gt;gist&lt;/code&gt; at the end of this post.&lt;/p&gt;

&lt;p&gt;I don't keep my vCenter exposed to the internet, so there will be some preparation required for this Action to function. We're using several prerequisites, install them first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1python3 -m pip install aiohttp pynetbox
2ansible-galaxy collection install vmware.vmware_rest netbox.netbox

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This Action leverages parameterization heavily, with Ansible relying on variables injected from GitHub to the virtual environment (&lt;code&gt;venv&lt;/code&gt;). It provides a little "quiz" that will let consumers define attributes about the deployed machine, e.g. vCPU count and memory. &lt;strong&gt;Any input sanitization &lt;em&gt;should&lt;/em&gt; be done by Ansible in this context&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Once a VM deployed, the &lt;code&gt;vmware_rest&lt;/code&gt; module returns the virtual machine's Managed Object ID (MOID). We can use that to get operational data about the VM via VMware Tools.&lt;/p&gt;

&lt;p&gt;Ansible keeps all of this data as &lt;code&gt;register&lt;/code&gt;ed variables for future utilization. Now, we have to put the data somewhere &lt;em&gt;persistent&lt;/em&gt;. Netbox is a valuable tool for documenting information assets, but it can also be used as an &lt;a href="https://docs.ansible.com/ansible/latest/collections/netbox/netbox/nb_inventory_inventory.html"&gt;Inventory&lt;/a&gt;. We can dump all the information about the VM into netbox rather easily, and pave the way for further customization seamlessly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note: I excluded the Guest Customization play in this version of the deployment script. It hasn't been particularly stable across 8.x releases with my automated testing, either failing completely with a &lt;code&gt;Service Unavailable&lt;/code&gt; or crashing vCenter. It is possible, however, to change IP addresses, install packages, copy artifacts with Ansible after the fact. Customization via Ansible might even be a better approach in more complex deployments.&lt;/strong&gt;&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@import url('https://cdn.rawgit.com/lonekorean/gist-syntax-themes/d49b91b3/stylesheets/idle-fingers.css');

@import url('https://fonts.googleapis.com/css?family=Open+Sans');
body {
  font: 16px 'Open Sans', sans-serif;
}
body .gist .gist-file {
  border-color: #555 #555 #444
}
body .gist .gist-data {
  border-color: #555
}
body .gist .gist-meta {
  color: #ffffff;
  background: #373737; 
}
body .gist .gist-meta a {
  color: #ffffff
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
    </item>
    <item>
      <title>Circumventing Coder's block and starting a new project</title>
      <dc:creator>Nick Schmidt</dc:creator>
      <pubDate>Sat, 26 Aug 2023 09:00:00 +0000</pubDate>
      <link>https://dev.to/ngschmidt/circumventing-coders-block-and-starting-a-new-project-379d</link>
      <guid>https://dev.to/ngschmidt/circumventing-coders-block-and-starting-a-new-project-379d</guid>
      <description>&lt;h2&gt;
  
  
  It's difficult to start a new software project
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--sADMydks--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/08/writers-block/share.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--sADMydks--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.engyak.co/2023/08/writers-block/share.png" alt="Road Block" width="370" height="299"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Documentation
&lt;/h3&gt;

&lt;p&gt;Depending on how a software project starts, it can either be the easiest or the hardest aspect of a new project.&lt;/p&gt;

&lt;p&gt;Documentation suffers from a similar issue, so a good place to get things moving would be to simplify the basics of repository management. Here's a number of things that &lt;em&gt;should&lt;/em&gt; be in a Git repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1.gitignore
2CHANGELOG.md
3README.md

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each of these things can be simplified in some way, and will make your future life easier. Let's start with the easy stuff:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.gitignore&lt;/code&gt; is easily templated by your source control provider. At this point, it's smart to include one of their templates, this feature has really grown over the years.

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/github/gitignore"&gt;GitHub&lt;/a&gt; provides templates for common development setups, and integrates it into their new repository wizard.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.gitlab.com/ee/api/templates/gitignores.html"&gt;GitLab&lt;/a&gt; does too.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CHANGELOG.md&lt;/code&gt; takes very little effort to start, but is difficult to apply to an existing project.

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://keepachangelog.com/en/1.1.0/"&gt;Keep a changelog&lt;/a&gt; provides an excellent template and guidance on how to effectively write changelogs.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;README.md&lt;/code&gt; is the core of your project's documentation, and deserves separate mention because it's a powerful tool to organize how a project should function. Spend plenty of time on this part!&lt;/p&gt;

&lt;p&gt;Since &lt;code&gt;README.md&lt;/code&gt; is the page that renders by default in SCM, the objective &lt;em&gt;should&lt;/em&gt; be to provide everything a user needs to consume your software. I personally prefer to outline how the software should function and use it as a reference when writing the actual code. Here's a decent starting point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 1# {{ Name the Project }}
 2
 3## Goal(s)
 4
 5{{ Write what your project should do }}
 6
 7## Overview
 8
 9### Validation
10
11{{ Write how functional software should be evaluated at an end-to-end level }}
12
13### Unit Testing
14
15{{ Write how functional software should be evaluated at a component level }}
16
17## HOWTO
18
19{{ Indicate how the software should be used. Provide examples, fix it later when the functional code revises your intricate plans }}
20
21## Software Dependencies
22
23{{ Include what the software will need to run. Update as you `include` new libraries}}
24
25## Contributors
26
27{{ Set up a place for software contributors to put their names. It might encourage participation }}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  CI
&lt;/h3&gt;

&lt;p&gt;Continuous Integration is not easy to establish, but pays off over time as it catches small issues that appear throughout development. Since the testing strategy is written in plain language, CI tool setup is ideally started soon after the documentation. For a given CI tool, e.g. GitHub Actions or Jenkins, common practices can be templated for re-use.&lt;/p&gt;

&lt;h3&gt;
  
  
  ...Then start writing code
&lt;/h3&gt;

&lt;p&gt;From here, it's going to be &lt;em&gt;easier&lt;/em&gt; to begin authoring software from an outline. Start by writing out your plan ("pseudo-code") in comments, defining class structures if applicable.&lt;/p&gt;

&lt;p&gt;Infrastructure automation is typically a play-by-play implementation of an operating procedure, which won't necessarily need object-oriented coding. In this case, the same approach still works - transpose the operating procedure as comments, and implement it as code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hindsight
&lt;/h3&gt;

&lt;p&gt;After some practice, it quickly becomes easier to start a new software repository. Structuring a software project quickly becomes important after only a few hundred lines of code - so if you're stuck, put some work in on the structure, it'll get the process moving, and the effort spent multiplies itself as the project grows.&lt;/p&gt;

&lt;p&gt;Most Git providers also support &lt;em&gt;repoository templates&lt;/em&gt; - use this feature to create a form of "starter kit", and copy it whenever a new project is created to safe time.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
