<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Posts on YetiOps</title>
        <link>https://yetiops.net/posts/</link>
        <description>Recent content in Posts on YetiOps</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>en</language>
        <copyright>&lt;a href=&#34;https://creativecommons.org/licenses/by-nc/4.0/&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;CC BY-NC 4.0&lt;/a&gt;</copyright>
        <lastBuildDate>Sun, 05 Jun 2022 08:27:30 +0100</lastBuildDate>
        <atom:link href="https://yetiops.net/posts/index.xml" rel="self" type="application/rss+xml" />
        
        <item>
            <title>Home and Personal Infrastructure Overhaul: Part 7 - Using Drone with Terraform</title>
            <link>https://yetiops.net/posts/home-infra-overhaul-part7-drone-terraform/</link>
            <pubDate>Sun, 05 Jun 2022 08:27:30 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/home-infra-overhaul-part7-drone-terraform/</guid>
            <description>This post is the next in the series on how I overhauled my personal infrastructure to make it easier to manage, make changes and integrate new applications.
Previous posts in the series are: -
 Introduction Ansible Improvements SaltStack Improvements Introduction to Drone CI Using Drone with Ansible Using Drone with Salt  This post will cover using Drone to manage and deploy resources with Terraform.
Background When I first started using Drone, I wasn&amp;rsquo;t using it with Terraform.</description>
            <content type="html"><![CDATA[<p>This post is the next in the series on how I overhauled my personal infrastructure to make it easier to manage, make changes and integrate new applications.</p>
<p>Previous posts in the series are: -</p>
<ul>
<li><a href="https://yetiops.net/posts/home-infra-overhaul-part1/">Introduction</a></li>
<li><a href="https://yetiops.net/posts/home-infra-overhaul-part2-ansible-improvements/">Ansible Improvements</a></li>
<li><a href="https://yetiops.net/posts/home-infra-overhaul-part3-salt-improvements/">SaltStack Improvements</a></li>
<li><a href="https://yetiops.net/posts/home-infra-overhaul-part4-drone-getting-started/">Introduction to Drone CI</a></li>
<li><a href="https://yetiops.net/posts/home-infra-overhaul-part5-drone-ansible/">Using Drone with Ansible</a></li>
<li><a href="https://yetiops.net/posts/home-infra-overhaul-part6-drone-salt/">Using Drone with Salt</a></li>
</ul>
<p>This post will cover using Drone to manage and deploy resources with Terraform.</p>
<h2 id="background">Background</h2>
<p>When I first started using Drone, I wasn&rsquo;t using it with Terraform. In fact very little of my infrastructure was managed by Terraform. Recently though, I decided to start moving most external services to it. In keeping with my theme of trying to automate my infrastructure and avoid manual changes, it made sense to finally start using Terraform for my personal usage of external services.</p>
<p>Ansible and Salt still control what runs on my machines (including VPSs/instances on cloud providers), but managing external services makes a lot of sense with Terraform. While Ansible and Salt are good at making changes, they don&rsquo;t manage &ldquo;desired state&rdquo; very well, in the sense that Ansible can create a VPS on Hetzner, but it does not know that one may already exist.</p>
<p>With Terraform, it knows what resources it manages (using State management), and whether they should already exist or not. Terraform may still be incorrect as to the state of a resource, but usually only if the resource changed since the last time Terraform ran. Something like Ansible will just try to make the same changes it did last time, which could lead to duplicate resources, or the Playbook failing because it can&rsquo;t create a resource (because it already exists and can&rsquo;t be created again).</p>
<p>The following covers what I use: -</p>
<ul>
<li>Cloudflare for external DNS
<ul>
<li>This includes externally accessibly domains</li>
<li>It also includes &ldquo;internal&rdquo; domains I use that I want valid Let&rsquo;s Encrypt certificates for</li>
</ul>
</li>
<li>Digital Ocean and Hetzner for VPSs
<ul>
<li>My blog and other external services (RSS, Read-It-Later, Black Box Exporter for monitoring) are hosted on these</li>
</ul>
</li>
<li>Google Cloud Platform and Oracle Cloud Infrastructure for the always-free virtual machines</li>
<li>Lab/Proxmox for testing technologies</li>
</ul>
<p>For most of these, the steps are the same, with some minor differences in variable/secrets for each provider). In some cases, extra steps are required. I&rsquo;ll first show the most basic pipeline (the one used for Cloudflare DNS) and then show some of the extra steps in others.</p>
<h2 id="full-drone-pipeline---cloudflare">Full Drone Pipeline - Cloudflare</h2>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">kind</span>: pipeline
<span style="color:#66d9ef">name</span>: default
<span style="color:#66d9ef">type</span>: docker

<span style="color:#66d9ef">trigger</span>:
  <span style="color:#66d9ef">branch</span>:
    - main

<span style="color:#66d9ef">steps</span>:
  - <span style="color:#66d9ef">name</span>: Terraform FMT PR
    <span style="color:#66d9ef">image</span>: jmccann/drone-terraform:latest
    <span style="color:#66d9ef">settings</span>:
      <span style="color:#66d9ef">actions</span>:
        - fmt
      <span style="color:#66d9ef">fmt_options</span>:
        <span style="color:#66d9ef">write</span>: <span style="color:#66d9ef">false</span>
        <span style="color:#66d9ef">diff</span>: <span style="color:#66d9ef">true</span>
        <span style="color:#66d9ef">check</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">when</span>:
      <span style="color:#66d9ef">event</span>:
      - pull_request

  - <span style="color:#66d9ef">name</span>: Terraform Plan
    <span style="color:#66d9ef">image</span>: jmccann/drone-terraform:latest
    <span style="color:#66d9ef">settings</span>:
      <span style="color:#66d9ef">actions</span>:
        - validate
        - plan
    <span style="color:#66d9ef">environment</span>:
      <span style="color:#66d9ef">DIGITALOCEAN_TOKEN</span>:
        <span style="color:#66d9ef">from_secret</span>: digitalocean_token 
      <span style="color:#66d9ef">HCLOUD_TOKEN</span>:
        <span style="color:#66d9ef">from_secret</span>: hcloud_token
      <span style="color:#66d9ef">CLOUDFLARE_API_TOKEN</span>:
        <span style="color:#66d9ef">from_secret</span>: cloudflare_api_token
    <span style="color:#66d9ef">when</span>:
      <span style="color:#66d9ef">event</span>:
      - pull_request

  - <span style="color:#66d9ef">name</span>: slack-pr
    <span style="color:#66d9ef">image</span>: plugins/slack
    <span style="color:#66d9ef">settings</span>:
      <span style="color:#66d9ef">webhook</span>:
        <span style="color:#66d9ef">from_secret</span>: drone_builds_slack_webhook 
      <span style="color:#66d9ef">channel</span>: builds
      <span style="color:#66d9ef">template</span>: <span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">        {{#success build.status}}</span>
          {{repo.name}} PR build passed. 
          Merge in to apply.
          <span style="color:#66d9ef">PR</span>: https://git.noisepalace.co.uk/YetiOps/{{repo.name}}/pulls/{{build.pull}}
          <span style="color:#66d9ef">Build</span>: https://drone.noisepalace.co.uk/YetiOps/{{repo.name}}/{{build.number}}
        {{else}}
          {{repo.name}} PR build failed. 
          Please investigate. 
          <span style="color:#66d9ef">PR</span>: https://git.noisepalace.co.uk/YetiOps/{{repo.name}}/pulls/{{build.pull}}
          <span style="color:#66d9ef">Build</span>: https://drone.noisepalace.co.uk/YetiOps/{{repo.name}}/{{build.number}}
        {{/success}}            
    <span style="color:#66d9ef">when</span>:
      <span style="color:#66d9ef">status</span>:
      - failure
      - success
      <span style="color:#66d9ef">event</span>:
        - pull_request

  - <span style="color:#66d9ef">name</span>: slack-push-start
    <span style="color:#66d9ef">image</span>: plugins/slack
    <span style="color:#66d9ef">settings</span>:
      <span style="color:#66d9ef">webhook</span>:
        <span style="color:#66d9ef">from_secret</span>: drone_builds_slack_webhook 
      <span style="color:#66d9ef">channel</span>: builds
      <span style="color:#66d9ef">template</span>: <span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">        {{repo.name}} build is starting.</span>
        <span style="color:#66d9ef">Build</span>: https://drone.noisepalace.co.uk/YetiOps/{{repo.name}}/{{build.number}}      
    <span style="color:#66d9ef">when</span>:
      <span style="color:#66d9ef">branch</span>:
      - main
      <span style="color:#66d9ef">event</span>:
      - push
      - tag
  
  - <span style="color:#66d9ef">name</span>: Terraform FMT
    <span style="color:#66d9ef">image</span>: jmccann/drone-terraform:latest
    <span style="color:#66d9ef">settings</span>:
      <span style="color:#66d9ef">actions</span>:
        - fmt
      <span style="color:#66d9ef">fmt_options</span>:
        <span style="color:#66d9ef">write</span>: <span style="color:#66d9ef">false</span>
        <span style="color:#66d9ef">diff</span>: <span style="color:#66d9ef">true</span>
        <span style="color:#66d9ef">check</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">when</span>:
      <span style="color:#66d9ef">branch</span>:
      - main
      <span style="color:#66d9ef">event</span>:
      - push
      - tag


  - <span style="color:#66d9ef">name</span>: Terraform Apply
    <span style="color:#66d9ef">image</span>: jmccann/drone-terraform:latest
    <span style="color:#66d9ef">settings</span>:
      <span style="color:#66d9ef">actions</span>:
        - validate
        - plan
        - apply
    <span style="color:#66d9ef">environment</span>:
      <span style="color:#66d9ef">DIGITALOCEAN_TOKEN</span>:
        <span style="color:#66d9ef">from_secret</span>: digitalocean_token 
      <span style="color:#66d9ef">HCLOUD_TOKEN</span>:
        <span style="color:#66d9ef">from_secret</span>: hcloud_token
      <span style="color:#66d9ef">CLOUDFLARE_API_TOKEN</span>:
        <span style="color:#66d9ef">from_secret</span>: cloudflare_api_token
    <span style="color:#66d9ef">when</span>:
      <span style="color:#66d9ef">branch</span>:
      - main
      <span style="color:#66d9ef">event</span>:
      - push
      - tag

  - <span style="color:#66d9ef">name</span>: slack-push
    <span style="color:#66d9ef">image</span>: plugins/slack
    <span style="color:#66d9ef">settings</span>:
      <span style="color:#66d9ef">webhook</span>:
        <span style="color:#66d9ef">from_secret</span>: drone_builds_slack_webhook 
      <span style="color:#66d9ef">channel</span>: builds
      <span style="color:#66d9ef">template</span>: <span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">        {{#success build.status}}</span>
          {{repo.name}} build passed.
          <span style="color:#66d9ef">Build</span>: https://drone.noisepalace.co.uk/YetiOps/{{repo.name}}/{{build.number}}
        {{else}}
          {{repo.name}} build {{build.number}} failed. Please investigate. 
          <span style="color:#66d9ef">Build</span>: https://drone.noisepalace.co.uk/YetiOps/{{repo.name}}/{{build.number}}
        {{/success}}            
    <span style="color:#66d9ef">when</span>:
      <span style="color:#66d9ef">status</span>:
      - failure
      - success
      <span style="color:#66d9ef">branch</span>:
      - main
      <span style="color:#66d9ef">event</span>:
      - push
      - tag
</code></pre></div><p>This pipeline is very similar to the one used for <a href="/posts/home-infra-overhaul-part5-drone-ansible/#full-drone-pipeline">Ansible</a>. This uses the same Slack notification steps, the same kind of triggers/conditionals for running steps, and runs as a single Docker-based pipeline (rather than the multiple pipelines required for <a href="/posts/home-infra-overhaul-part6-drone-salt/#full-drone-pipeline">Salt</a>).</p>
<p>The bulk of the Terraform-specific steps in the pipelines use the <a href="https://plugins.drone.io/plugins/terraform">Terraform Drone plugin</a>. It is possible to use the official Hashicorp Terraform Docker image, but the Drone plugin is a little more convenient. If you use the official Hashicorp Terraform Docker image, you need to define each command you want to run, whereas the plugin reduces most commands to fields.</p>
<h3 id="steps---terraform-fmt">Steps - Terraform FMT</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">name</span>: Terraform FMT PR
  <span style="color:#66d9ef">image</span>: jmccann/drone-terraform:latest
  <span style="color:#66d9ef">settings</span>:
    <span style="color:#66d9ef">actions</span>:
      - fmt
    <span style="color:#66d9ef">fmt_options</span>:
      <span style="color:#66d9ef">write</span>: <span style="color:#66d9ef">false</span>
      <span style="color:#66d9ef">diff</span>: <span style="color:#66d9ef">true</span>
      <span style="color:#66d9ef">check</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">event</span>:
    - pull_request
</code></pre></div><p>This step is the same as running the <code>terraform fmt</code> command with the flags <code>-diff</code> and <code>-check</code>. While this isn&rsquo;t a necessary step, it does apply some consistent formatting rules to Terraform files.</p>
<p>In the <code>diff</code> and <code>check</code> mode, it doesn&rsquo;t make any changes, but will fail if <code>terraform fmt</code> mentions any required changes (as it will exit with a <strong>non-zero</strong> exit code).</p>
<p><img src="/img/homeinfra/drone-terraform-fmt.png" alt="Terraform Drone FMT step"></p>
<h3 id="steps---terraform-plan">Steps - Terraform Plan</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  - <span style="color:#66d9ef">name</span>: Terraform Plan
    <span style="color:#66d9ef">image</span>: jmccann/drone-terraform:latest
    <span style="color:#66d9ef">settings</span>:
      <span style="color:#66d9ef">actions</span>:
        - validate
        - plan
    <span style="color:#66d9ef">environment</span>:
      <span style="color:#66d9ef">DIGITALOCEAN_TOKEN</span>:
        <span style="color:#66d9ef">from_secret</span>: digitalocean_token 
      <span style="color:#66d9ef">HCLOUD_TOKEN</span>:
        <span style="color:#66d9ef">from_secret</span>: hcloud_token
      <span style="color:#66d9ef">CLOUDFLARE_API_TOKEN</span>:
        <span style="color:#66d9ef">from_secret</span>: cloudflare_api_token
    <span style="color:#66d9ef">when</span>:
      <span style="color:#66d9ef">event</span>:
      - pull_request
</code></pre></div><p>This step runs <code>terraform validate</code> and <code>terraform plan</code>. Validate ensures that the Terraform configuration is correct (i.e. brackets in the correct place, variables defined correctly etc), and the Plan stage shows the changes Terraform wants to make.</p>
<p>In this Drone expose a set of environment variables, namely my Digital Ocean API token, Hetzner HCloud API token and the Cloudflare API token. It uses the Digital Ocean and Hetzner tokens to create DNS records for the public IPs of my VPSs within Hetzner and Digital Ocean, and the Cloudflare API token is used to read/write updates to the Cloudflare API. An example of the relevant Terraform code is shown below: -</p>
<p><strong>data.tf</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;hcloud_server&#34; &#34;hcloud-vps-a&#34;</span> {
  name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vps-shme-hcloud-a&#34;</span>
}

<span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;hcloud_server&#34; &#34;hcloud-vps-b&#34;</span> {
  name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vps-shme-hcloud-b&#34;</span>
}

<span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;digitalocean_droplet&#34; &#34;vps-shme&#34;</span> {
  name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vps-shme&#34;</span>
}
</code></pre></div><p><strong>main.tf</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#75715e"># VPS Records - IPv4
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_record&#34; &#34;vps-a&#34;</span> {
  zone_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">this_zone_id</span>
  name    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vps-a&#34;</span>
  type    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;A&#34;</span>
  proxied <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;false&#34;</span>
  value   <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">hcloud_server</span>.<span style="color:#66d9ef">hcloud</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">vps</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">a</span>.<span style="color:#66d9ef">ipv4_address</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_record&#34; &#34;vps-b&#34;</span> {
  zone_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">this_zone_id</span>
  name    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vps-b&#34;</span>
  type    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;A&#34;</span>
  proxied <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;false&#34;</span>
  value   <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">hcloud_server</span>.<span style="color:#66d9ef">hcloud</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">vps</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">b</span>.<span style="color:#66d9ef">ipv4_address</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_record&#34; &#34;vps-shme&#34;</span> {
  zone_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">this_zone_id</span>
  name    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vps&#34;</span>
  type    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;A&#34;</span>
  proxied <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;false&#34;</span>
  value   <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">digitalocean_droplet</span>.<span style="color:#66d9ef">vps</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">shme</span>.<span style="color:#66d9ef">ipv4_address</span>
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e"># VPS Records - IPv6
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_record&#34; &#34;vps-a-v6&#34;</span> {
  zone_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">this_zone_id</span>
  name    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vps-a&#34;</span>
  type    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;AAAA&#34;</span>
  proxied <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;false&#34;</span>
  value   <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">hcloud_server</span>.<span style="color:#66d9ef">hcloud</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">vps</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">a</span>.<span style="color:#66d9ef">ipv6_address</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_record&#34; &#34;vps-b-v6&#34;</span> {
  zone_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">this_zone_id</span>
  name    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vps-b&#34;</span>
  type    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;AAAA&#34;</span>
  proxied <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;false&#34;</span>
  value   <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">hcloud_server</span>.<span style="color:#66d9ef">hcloud</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">vps</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">b</span>.<span style="color:#66d9ef">ipv6_address</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_record&#34; &#34;vps-shme-v6&#34;</span> {
  zone_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">this_zone_id</span>
  name    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vps&#34;</span>
  type    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;AAAA&#34;</span>
  proxied <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;false&#34;</span>
  value   <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">digitalocean_droplet</span>.<span style="color:#66d9ef">vps</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">shme</span>.<span style="color:#66d9ef">ipv6_address</span>
}
</code></pre></div><p>As you can see, this uses data sources to get information from Digital Ocean and Hetzner, and then applies the IPv4/IPv6 addresses as values in <code>cloudflare_record</code> resources.</p>
<p>I also create DNS records for instances within Google Cloud Platform and Oracle Cloud Infrastructure, but with these I use <a href="https://www.terraform.io/language/state/remote">Terraform Remote State</a>. The reason for this is that both of these require extra configuration and files (GPG keys for OCI, a JSON document for GCP) that add extra steps and complication to the pipeline. While this does mean that I am reliant on the Terraform Remote State being up to date (rather than a direct data source like for Digital Ocean or Hetzner), I accept this trade off to avoid the additional complexity.</p>
<p>The relevant Terraform for the GCP and OCI records are shown below: -</p>
<p><strong>data.tf</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;terraform_remote_state&#34; &#34;oracle-ork&#34;</span> {
  backend <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;consul&#34;</span>
  config <span style="color:#f92672">=</span> {
    address <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;consul.noisepalace.co.uk&#34;</span>
    scheme  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https&#34;</span>
    path    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;terraform/oraclecloud/oci&#34;</span>
  }
}

<span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;terraform_remote_state&#34; &#34;yetiops-goggle&#34;</span> {
  backend <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;consul&#34;</span>
  config <span style="color:#f92672">=</span> {
    address <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;consul.noisepalace.co.uk&#34;</span>
    scheme  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https&#34;</span>
    path    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;terraform/gcp/yetiops-goggle&#34;</span>
  }
}
</code></pre></div><p><strong>main.tf</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_record&#34; &#34;vps-ork-01&#34;</span> {
  zone_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">this_zone_id</span>
  name    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ork-01&#34;</span>
  type    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;A&#34;</span>
  proxied <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;false&#34;</span>
  value   <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">terraform_remote_state</span>.<span style="color:#66d9ef">oracle</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">ork</span>.<span style="color:#66d9ef">outputs</span>.<span style="color:#66d9ef">instance_public_ip</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_record&#34; &#34;vps-gog-01&#34;</span> {
  zone_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">local</span>.<span style="color:#66d9ef">this_zone_id</span>
  name    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;gog-01&#34;</span>
  type    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;A&#34;</span>
  proxied <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;false&#34;</span>
  value   <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">terraform_remote_state</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">goggle</span>.<span style="color:#66d9ef">outputs</span>.<span style="color:#66d9ef">gog</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">01</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">ipv4</span>
}
</code></pre></div><p>These use Consul as the source for Remote State. More details on this are in <a href="#remote-state-backend-with-consul">this section</a>.</p>
<p>The Drone step will run a plan against these resources (and all other defined resources), and show what changes need to be made (if any).</p>
<p><img src="/img/homeinfra/drone-terraform-plan.png" alt="Terraform Drone Plan"></p>
<h3 id="steps---terraform-apply">Steps - Terraform Apply</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  - <span style="color:#66d9ef">name</span>: Terraform Apply
    <span style="color:#66d9ef">image</span>: jmccann/drone-terraform:latest
    <span style="color:#66d9ef">settings</span>:
      <span style="color:#66d9ef">actions</span>:
        - validate
        - plan
        - apply
    <span style="color:#66d9ef">environment</span>:
      <span style="color:#66d9ef">DIGITALOCEAN_TOKEN</span>:
        <span style="color:#66d9ef">from_secret</span>: digitalocean_token 
      <span style="color:#66d9ef">HCLOUD_TOKEN</span>:
        <span style="color:#66d9ef">from_secret</span>: hcloud_token
      <span style="color:#66d9ef">CLOUDFLARE_API_TOKEN</span>:
        <span style="color:#66d9ef">from_secret</span>: cloudflare_api_token
    <span style="color:#66d9ef">when</span>:
      <span style="color:#66d9ef">branch</span>:
      - main
      <span style="color:#66d9ef">event</span>:
      - push
      - tag
</code></pre></div><p>This step is almost identical to the Plan stage. The only difference is that it uses the <code>apply</code> action as well. This runs a <code>validate</code> and a <code>plan</code> action (to ensure that the code is still valid when merged with the main branch) and then applies the changes.</p>
<p>This is all controlled via Gitea pull requests, meaning that any PRs raised will go through all the validation and planning steps. Changes are then applied without any human interaction with the Terraform CLI itself.</p>
<p><img src="/img/homeinfra/drone-terraform-apply.png" alt="Drone Terraform Apply"></p>
<p>For most Terraform code, these are all the steps you will need.</p>
<h2 id="additional-steps">Additional steps</h2>
<p>As noted above, most of my Terraform code doesn&rsquo;t need anything more than the steps covered already. The secrets to expose may differ (based upon the infrastructure used), otherwise everything else is the same.</p>
<p>However for some Terraform code, I need to make files available (e.g. SSH keys, GPG keys, configuration objects). The following step covers how to do this: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">steps</span>:
  - <span style="color:#66d9ef">name</span>: Place SSH keys - PR
    <span style="color:#66d9ef">image</span>: alpine
    <span style="color:#66d9ef">environment</span>: 
      <span style="color:#66d9ef">SSH_PRIV</span>:
        <span style="color:#66d9ef">from_secret</span>: drone_ssh_priv
      <span style="color:#66d9ef">SSH_PUB</span>:
        <span style="color:#66d9ef">from_secret</span>: drone_ssh_pub
      <span style="color:#66d9ef">OCI_PRIVATE_KEY</span>:
        <span style="color:#66d9ef">from_secret</span>: oci_private_key
    <span style="color:#66d9ef">volumes</span>:
      - <span style="color:#66d9ef">name</span>: cache
        <span style="color:#66d9ef">path</span>: /ssh
    <span style="color:#66d9ef">commands</span>:
      - echo -e <span style="color:#e6db74">&#34;$SSH_PRIV&#34;</span> | tee /ssh/id_rsa
      - echo -e <span style="color:#e6db74">&#34;$SSH_PUB&#34;</span> | tee /ssh/id_rsa.pub
      - echo -e <span style="color:#e6db74">&#34;$OCI_PRIVATE_KEY&#34;</span> | tee /ssh/oci_private_key.pem
      - chmod <span style="color:#ae81ff">644</span> /ssh/*
    <span style="color:#66d9ef">when</span>:
      <span style="color:#66d9ef">event</span>:
      - pull_request

[...]

<span style="color:#66d9ef">volumes</span>:
  - <span style="color:#66d9ef">name</span>: cache
    <span style="color:#66d9ef">temp</span>: {}
</code></pre></div><p>This step takes the contents of some secrets, and then places them into files. In the above, these are my Drone SSH keys, and also the OCI GPG private key. These are placed in a <a href="https://docs.drone.io/pipeline/docker/syntax/volumes/">volume</a>. A volume is a shared directory/path that can be made available to other steps within a pipeline.</p>
<p>For Oracle Cloud Infrastructure, the GPG key is required to authenticate against the API, whereas the SSH keys (specifically the public key) are used in the <code>cloud-init</code>/user data to bootstrap the instances. By default they do not allow password-based authentication to login via SSH, so they need some form of SSH key to allow you to login.</p>
<p>An example of using this is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">name</span>: Terraform Plan
  <span style="color:#66d9ef">image</span>: jmccann/drone-terraform:latest
  <span style="color:#66d9ef">settings</span>:
    <span style="color:#66d9ef">actions</span>:
      - validate
      - plan
    <span style="color:#66d9ef">vars</span>:
      <span style="color:#66d9ef">ssh_public_key</span>: <span style="color:#e6db74">&#34;/ssh/id_rsa.pub&#34;</span>
  <span style="color:#66d9ef">environment</span>:
      <span style="color:#66d9ef">TF_VAR_tenancy_ocid</span>: 
        <span style="color:#66d9ef">from_secret</span>: oci_tenancy_ocid
      <span style="color:#66d9ef">TF_VAR_user_ocid</span>: 
        <span style="color:#66d9ef">from_secret</span>: oci_user_ocid
      <span style="color:#66d9ef">TF_VAR_compartment_ocid</span>: 
        <span style="color:#66d9ef">from_secret</span>: oci_compartment_ocid
      <span style="color:#66d9ef">TF_VAR_fingerprint</span>: 
        <span style="color:#66d9ef">from_secret</span>: oci_fingerprint
      <span style="color:#66d9ef">TF_VAR_private_key_path</span>: <span style="color:#e6db74">&#34;/ssh/oci_private_key.pem&#34;</span>
  <span style="color:#66d9ef">volumes</span>:
    - <span style="color:#66d9ef">name</span>: cache
      <span style="color:#66d9ef">path</span>: /ssh
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">event</span>:
    - pull_request
</code></pre></div><p>As you can see, the SSH key path is supplied as a variable. We also supply the OCI private key path as a <code>TF_VAR</code> environment variable. The most important part here is the <code>volumes</code> section, as without this the keys created in the previous step would not be available.</p>
<p>Volumes could be used in many different ways (e.g. cached dependencies, creating build artifacts and pushing them). In this case it make files available between steps, allowing authentication to certain providers.</p>
<p>The same step is used in my Lab Terraform when creating Proxmox virtual machines. This is because <a href="https://cloudinit.readthedocs.io/en/latest/">cloud-init</a> in Proxmox requires adding files directly to the Proxmox host&rsquo;s file system (rather than being stored in an API). In my case, I use SSH/SCP within Terraform to transfer the files to the Proxmox host(s), using the Drone SSH keys for authentication.</p>
<h2 id="remote-state-backend-with-consul">Remote State Backend with Consul</h2>
<p>While not specific to Drone, using some form of Remote State Backend within Terraform allows the following: -</p>
<ul>
<li>A central place to source state between Drone jobs and local Terraform testing</li>
<li>Using Remote State as a data source in Terraform code</li>
<li>State Locking - avoids multiple jobs/people applying changes at the same time (and potentially breaking each others changes)
<ul>
<li>Not all backends support this, and some require a separate backend for locking (e.g. AWS S3 for state storage, and DynamoDB for state locking)</li>
</ul>
</li>
<li>Most importantly, it means Drone does not need to write to a <code>terraform.tfstate</code> file in the repository and push it back to the repository after changes are made</li>
</ul>
<p>The last point is especially pertinent, as without this the following steps would be required: -</p>
<ul>
<li>Create a cache/state volume in the pipeline</li>
<li>Make the Terraform Drone plugin place state files in the volume</li>
<li>Run a step after each Terraform Apply stage to add, commit and push changes back to the code repository
<ul>
<li>This also means giving Drone access to push (and not just pull/clone rights) access to the repository</li>
</ul>
</li>
</ul>
<p>Instead, using a Remote State Backend means none of these extra steps are required. Many different options for Backends are available (e.g. AWS S3, Azure Blob Storage, etcd, Postgres), but I chose Consul as I already run Consul for <a href="/posts/prometheus-consul-node_exporter/">Prometheus Service Discovery</a>.</p>
<p>Setting up this backend looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">terraform</span> {
  <span style="color:#66d9ef">backend</span> <span style="color:#e6db74">&#34;consul&#34;</span> {
    address <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;consul.noisepalace.co.uk&#34;</span>
    scheme  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https&#34;</span>
    path    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;terraform/oraclecloud/oci&#34;</span>
  }
}
</code></pre></div><p>The path structure is arbitrary. I decided to use a <code>$APPLICATION/$PROVIDER/$PURPOSE</code> structure, but you could name each one after characters in Star Wars or Transformers if you wished! This stores state in the Consul KV store, as shown below: -</p>
<p><img src="/img/homeinfra/drone-terraform-consul.png" alt="Drone Terraform Consul KV store"></p>
<h2 id="demonstration">Demonstration</h2>
<p>The below video is a demonstration of making a change to the Terraform repository and committing it. In this step I am going to create a DNS record in Cloudflare: -</p>

<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe src="https://www.youtube.com/embed/7MWhwdbKW5c" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allowfullscreen title="YouTube Video"></iframe>
</div>

<h2 id="summary">Summary</h2>
<p>In this, we have seen how to can make use of Drone to apply Terraform configuration.</p>
<p>This has also shown how we can make use of Consul to provide a consistent Backend for Terraform, reducing the need for additional steps to manage Terraform state files with Drone.</p>
<p>In the next post, I&rsquo;m going to cover using Drone to build Go releases.</p>
]]></content>
        </item>
        
        <item>
            <title>Home and Personal Infrastructure Overhaul: Part 6 - Using Drone with SaltStack</title>
            <link>https://yetiops.net/posts/home-infra-overhaul-part6-drone-salt/</link>
            <pubDate>Mon, 23 May 2022 19:25:54 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/home-infra-overhaul-part6-drone-salt/</guid>
            <description>This post is the next in the series on how I overhauled my personal infrastructure to make it easier to manage, make changes and integrate new applications.
Previous posts in the series are: -
 Introduction Ansible Improvements SaltStack Improvements Introduction to Drone CI Using Drone with Ansible  As we have already covered how to use set up Drone CI, as well as a working pipeline with Ansible, it&amp;rsquo;s time to show it working with Salt.</description>
            <content type="html"><![CDATA[<p>This post is the next in the series on how I overhauled my personal infrastructure to make it easier to manage, make changes and integrate new applications.</p>
<p>Previous posts in the series are: -</p>
<ul>
<li><a href="https://yetiops.net/posts/home-infra-overhaul-part1/">Introduction</a></li>
<li><a href="https://yetiops.net/posts/home-infra-overhaul-part2-ansible-improvements/">Ansible Improvements</a></li>
<li><a href="https://yetiops.net/posts/home-infra-overhaul-part3-salt-improvements/">SaltStack Improvements</a></li>
<li><a href="https://yetiops.net/posts/home-infra-overhaul-part4-drone-getting-started/">Introduction to Drone CI</a></li>
<li><a href="https://yetiops.net/posts/home-infra-overhaul-part5-drone-ansible/">Using Drone with Ansible</a></li>
</ul>
<p>As we have already covered how to use set up Drone CI, as well as a working pipeline with Ansible, it&rsquo;s time to show it working with <a href="https://saltproject.io/">Salt</a>.</p>
<h2 id="background">Background</h2>
<p>As mentioned before, I use Ansible to bootstrap nodes, set up monitoring of devices that can&rsquo;t be managed by Salt, build my DNS and DHCP configuration and some other minor.</p>
<p>For most other tasks though, I use Salt to manage my machines. This covers everything from setting up Prometheus and exporters, NFS, Promtail/Loki, Samba, Wireguard, Gitea and more.</p>
<h2 id="differences">Differences</h2>
<p>Because Salt (unlike Ansible) uses agents and a central Salt server to apply changes to nodes, Drone can&rsquo;t run Salt directly from within a container. I could configure Salt Masterless, but this requires a lot of extra work (including cloning the entire Salt configuration to each managed node), removing the speed benefits that Salt has over Ansible.</p>
<p>With this being the case, Drone needs some way of access the Salt central server (known as a &ldquo;master&rdquo;) to check, test and apply changes.</p>
<p>In the <a href="https://yetiops.net/posts/home-infra-overhaul-part4-drone-getting-started/">Introduction to Drone CI</a> post, I referred to different kinds of runners (Docker, Exec, SSH) that perform different functions. In this case, Drone will use the Exec Runner. The Exec Runner can run commands directly on a machine (rather than inside of a Docker container), which fits with how to manage Salt.</p>
<h2 id="full-drone-pipeline">Full Drone Pipeline</h2>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#66d9ef">kind</span>: pipeline
<span style="color:#66d9ef">name</span>: pr
<span style="color:#66d9ef">type</span>: exec

<span style="color:#66d9ef">clone</span>:
  <span style="color:#66d9ef">disable</span>: <span style="color:#66d9ef">true</span>

<span style="color:#66d9ef">node</span>:
  <span style="color:#66d9ef">salt</span>: server

<span style="color:#66d9ef">trigger</span>:
  <span style="color:#66d9ef">branch</span>:
  - main
  <span style="color:#66d9ef">event</span>:
  - pull_request

<span style="color:#66d9ef">steps</span>:
- <span style="color:#66d9ef">name</span>: Check all minions are up 
  <span style="color:#66d9ef">commands</span>:
    - <span style="color:#e6db74">&#34;sudo salt --force-color &#39;*&#39; test.ping&#34;</span>
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">event</span>:
    - pull_request

- <span style="color:#66d9ef">name</span>: Salt Lint
  <span style="color:#66d9ef">commands</span>:
    - <span style="color:#e6db74">&#34;cd /srv/salt&#34;</span>
    - <span style="color:#e6db74">&#34;git checkout main&#34;</span>
    - <span style="color:#e6db74">&#34;sudo -u stuh84 git pull&#34;</span>
    - <span style="color:#e6db74">&#34;git checkout ${DRONE_SOURCE_BRANCH}&#34;</span> 
    - <span style="color:#e6db74">&#34;find /srv/salt -type f -name &#39;*.sls&#39; -print0 | xargs -0 --no-run-if-empty salt-lint -x 204 -x 203&#34;</span>
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">event</span>:
    - pull_request

- <span style="color:#66d9ef">name</span>: Highstate test
  <span style="color:#66d9ef">commands</span>:
    - <span style="color:#e6db74">&#34;cd /srv/salt&#34;</span>
    - <span style="color:#e6db74">&#34;git checkout main&#34;</span>
    - <span style="color:#e6db74">&#34;sudo -u stuh84 git pull&#34;</span>
    - <span style="color:#e6db74">&#34;git checkout ${DRONE_SOURCE_BRANCH}&#34;</span> 
    - <span style="color:#e6db74">&#34;sudo salt --force-color &#39;*&#39; saltutil.pillar_refresh&#34;</span>
    - <span style="color:#e6db74">&#34;sudo salt --force-color &#39;*&#39; state.highstate --state-verbose=False test=True&#34;</span>
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">event</span>:
    - pull_request

---
<span style="color:#66d9ef">kind</span>: pipeline
<span style="color:#66d9ef">name</span>: slack-pr
<span style="color:#66d9ef">type</span>: docker

<span style="color:#66d9ef">clone</span>:
  <span style="color:#66d9ef">disable</span>: <span style="color:#66d9ef">true</span>

<span style="color:#66d9ef">depends_on</span>:
- pr

<span style="color:#66d9ef">trigger</span>:
  <span style="color:#66d9ef">branch</span>:
  - main
  <span style="color:#66d9ef">event</span>:
  - pull_request
  <span style="color:#66d9ef">status</span>:
  - success
  - failure

<span style="color:#66d9ef">steps</span>:
- <span style="color:#66d9ef">name</span>: slack-pr
  <span style="color:#66d9ef">image</span>: plugins/slack
  <span style="color:#66d9ef">settings</span>:
    <span style="color:#66d9ef">webhook</span>:
      <span style="color:#66d9ef">from_secret</span>: drone_builds_slack_webhook 
    <span style="color:#66d9ef">channel</span>: builds
    <span style="color:#66d9ef">template</span>: <span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">      {{#success build.status}}</span>
        {{repo.name}} PR build passed. 
        Merge in to apply.
        <span style="color:#66d9ef">PR</span>: https://git.noisepalace.co.uk/YetiOps/{{repo.name}}/pulls/{{build.pull}}
        <span style="color:#66d9ef">Build</span>: https://drone.noisepalace.co.uk/YetiOps/{{repo.name}}/{{build.number}}
      {{else}}
        {{repo.name}} PR build failed. 
        Please investigate. 
        <span style="color:#66d9ef">PR</span>: https://git.noisepalace.co.uk/YetiOps/{{repo.name}}/pulls/{{build.pull}}
        <span style="color:#66d9ef">Build</span>: https://drone.noisepalace.co.uk/YetiOps/{{repo.name}}/{{build.number}}
      {{/success}}      
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">status</span>:
    - failure
    - success
    <span style="color:#66d9ef">event</span>:
      - pull_request

---
<span style="color:#66d9ef">kind</span>: pipeline
<span style="color:#66d9ef">name</span>: slack-push-start
<span style="color:#66d9ef">type</span>: docker

<span style="color:#66d9ef">trigger</span>:
  <span style="color:#66d9ef">branch</span>:
  - main
  <span style="color:#66d9ef">event</span>:
  - push
  - tag

<span style="color:#66d9ef">clone</span>:
  <span style="color:#66d9ef">disable</span>: <span style="color:#66d9ef">true</span>

<span style="color:#66d9ef">steps</span>:
- <span style="color:#66d9ef">name</span>: slack-push-start
  <span style="color:#66d9ef">image</span>: plugins/slack
  <span style="color:#66d9ef">settings</span>:
    <span style="color:#66d9ef">webhook</span>:
      <span style="color:#66d9ef">from_secret</span>: drone_builds_slack_webhook 
    <span style="color:#66d9ef">channel</span>: builds
    <span style="color:#66d9ef">template</span>: <span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">      {{repo.name}} build is starting.</span>
      <span style="color:#66d9ef">Build</span>: https://drone.noisepalace.co.uk/YetiOps/{{repo.name}}/{{build.number}}
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">branch</span>:
    - main
    <span style="color:#66d9ef">event</span>:
    - push
    - tag

---
<span style="color:#66d9ef">kind</span>: pipeline
<span style="color:#66d9ef">name</span>: push
<span style="color:#66d9ef">type</span>: exec

<span style="color:#66d9ef">node</span>:
  <span style="color:#66d9ef">salt</span>: server

<span style="color:#66d9ef">trigger</span>:
  <span style="color:#66d9ef">branch</span>:
  - main
  <span style="color:#66d9ef">event</span>:
  - push
  - tag

<span style="color:#66d9ef">depends_on</span>:
  - slack-push-start

<span style="color:#66d9ef">clone</span>:
  <span style="color:#66d9ef">disable</span>: <span style="color:#66d9ef">true</span>

<span style="color:#66d9ef">steps</span>:
- <span style="color:#66d9ef">name</span>: Salt Lint
  <span style="color:#66d9ef">commands</span>:
    - <span style="color:#e6db74">&#34;cd /srv/salt&#34;</span>
    - <span style="color:#e6db74">&#34;git checkout main&#34;</span>
    - <span style="color:#e6db74">&#34;sudo -u stuh84 git pull&#34;</span>
    - <span style="color:#e6db74">&#34;find /srv/salt -type f -name &#39;*.sls&#39; -print0 | xargs -0 --no-run-if-empty salt-lint -x 204 -x 203&#34;</span>
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">branch</span>:
    - main
    <span style="color:#66d9ef">event</span>:
    - push
    - tag

- <span style="color:#66d9ef">name</span>: Salt Highstate 
  <span style="color:#66d9ef">commands</span>:
    - <span style="color:#e6db74">&#34;cd /srv/salt&#34;</span>
    - <span style="color:#e6db74">&#34;git checkout main&#34;</span>
    - <span style="color:#e6db74">&#34;sudo -u stuh84 git pull&#34;</span>
    - <span style="color:#e6db74">&#34;sudo salt --force-color &#39;*&#39; --state-verbose=False state.highstate&#34;</span>
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">branch</span>:
    - main
    <span style="color:#66d9ef">event</span>:
    - push
    - tag

---
<span style="color:#66d9ef">kind</span>: pipeline
<span style="color:#66d9ef">name</span>: slack-push
<span style="color:#66d9ef">type</span>: docker

<span style="color:#66d9ef">trigger</span>:
  <span style="color:#66d9ef">branch</span>:
  - main
  <span style="color:#66d9ef">event</span>:
  - push
  - tag
  <span style="color:#66d9ef">status</span>:
  - success
  - failure

<span style="color:#66d9ef">depends_on</span>:
- push

<span style="color:#66d9ef">clone</span>:
  <span style="color:#66d9ef">disable</span>: <span style="color:#66d9ef">true</span>

<span style="color:#66d9ef">steps</span>:
- <span style="color:#66d9ef">name</span>: slack-push
  <span style="color:#66d9ef">image</span>: plugins/slack
  <span style="color:#66d9ef">settings</span>:
    <span style="color:#66d9ef">webhook</span>:
      <span style="color:#66d9ef">from_secret</span>: drone_builds_slack_webhook 
    <span style="color:#66d9ef">channel</span>: builds
    <span style="color:#66d9ef">template</span>: <span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">      {{#success build.status}}</span>
        {{repo.name}} build passed.
        <span style="color:#66d9ef">Build</span>: https://drone.noisepalace.co.uk/YetiOps/{{repo.name}}/{{build.number}}
      {{else}}
        {{repo.name}} build {{build.number}} failed. Please investigate. 
        <span style="color:#66d9ef">Build</span>: https://drone.noisepalace.co.uk/YetiOps/{{repo.name}}/{{build.number}}
      {{/success}}      
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">status</span>:
    - failure
    - success
    <span style="color:#66d9ef">branch</span>:
    - main
    <span style="color:#66d9ef">event</span>:
    - push
    - tag
</code></pre></div><p>Compared to the <a href="/posts/home-infra-overhaul-part5-drone-ansible/#full-drone-pipeline">Ansible pipeline</a>, there are many similarities but also a few key differences.</p>
<p>The first is that the Drone file contains multiple pipelines. Each separator (the <code>---</code>) is the start of a new pipeline. The reason I need to do this is because each pipeline has a type. I already have a working Slack notification step, so it makes sense to continue using this. However the Slack notification step uses Docker. As noted already, I am using the Exec runner for commands on the Salt server, which requires a use of the <code>exec</code> type. To use them all in the same &ldquo;build&rdquo; (i.e. a run through of all the steps in the Drone file), each &ldquo;type&rdquo; of build needs to be separated.</p>
<p>Also, you&rsquo;ll notice the disabling of clones (i.e. performing a Git clone). Drone by default will run an implicit Clone step in every pipeline, to make code available within a container. However this doesn&rsquo;t work for the Exec runners, at which point this step will fail. I also don&rsquo;t need to clone any code for Slack notifications, so I disable it in every Pipeline defined in the Drone file.</p>
<p>Another difference is the node selector. Node selectors in Drone use key-value pairs, which are defined in the Exec Runner configuration as a label - <code>DRONE_RUNNER_LABELS=salt:server</code>. You can add more labels with commas, and these can also be set as environment variables rather than in a configuration file. What this means is that the Pipeline will run on nodes that match the key-value pair and nowhere else. This ensures that Drone doesn&rsquo;t run the Pipeline on a server other than that which is running the Salt server.</p>
<p>Finally, there is the <code>depends_on</code> field. By default, Drone will run pipelines defined in a file in parallel. This is really useful when deploying to multiple locations, or running concurrent (parallel) steps or pipelines. However in this Drone file, sending Slack notifications at the same time as running Salt would notify before the stage is finished. Using <code>depends_on</code> ensures each Pipeline runs sequentially, with the status of each Pipeline affecting any Pipelines that depend on it.</p>
<h2 id="pipeline-steps">Pipeline Steps</h2>
<p>As the Slack notification steps are identical to the steps in the Ansible pipeline, I&rsquo;ll cover the Salt specific steps.</p>
<h3 id="check-all-minions-are-up">Check all minions are up</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">name</span>: Check all minions are up 
  <span style="color:#66d9ef">commands</span>:
    - <span style="color:#e6db74">&#34;sudo salt --force-color &#39;*&#39; test.ping&#34;</span>
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">event</span>:
    - pull_request
</code></pre></div><p>As this step (and others in the Drone file) use the Exec runner, there is no need to specify a Docker Image to use as part of the process. Instead, I supply the commands to run on the machine.</p>
<p>The <code>test.ping</code> command is used to ensure all the managed minions are up and return a response. If this fails, it means an agent is not reachable, and therefore configuration cannot be applied to all nodes at once. This ensures Salt is working everywhere before trying to apply changes.</p>
<p>The <code>--force-color</code> argument is used because if a real terminal (i.e. SSH or someone physically logged in to a machine) is not present, Salt will not display colours. Red is quite an easy colour to spot for problems, rather than the output being same colour whether a command passed or failed!</p>
<p><img src="/img/homeinfra/drone-salt-ping.png" alt="Salt Ping"></p>
<h3 id="salt-lint">Salt Lint</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">name</span>: Salt Lint
  <span style="color:#66d9ef">commands</span>:
    - <span style="color:#e6db74">&#34;cd /srv/salt&#34;</span>
    - <span style="color:#e6db74">&#34;git checkout main&#34;</span>
    - <span style="color:#e6db74">&#34;sudo -u stuh84 git pull&#34;</span>
    - <span style="color:#e6db74">&#34;git checkout ${DRONE_SOURCE_BRANCH}&#34;</span> 
    - <span style="color:#e6db74">&#34;find /srv/salt -type f -name &#39;*.sls&#39; -print0 | xargs -0 --no-run-if-empty salt-lint -x 204 -x 203&#34;</span>
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">event</span>:
    - pull_request
</code></pre></div><p>As you can see in this, the commands used are the same as those that a user would need to run directly on the machine. This is where the Exec runner can become unwieldy compared to running in Docker, as you aren&rsquo;t guaranteed to be running in a clean state (i.e. no hanging commits/changes, the code is cloned fresh).</p>
<p>In this, the Exec runner will change directory to the Salt base path (<code>/srv/salt</code> in this case), checkout the main branch, run a Git pull (to bring in all previously merged changes and committed branches), and then checkout the branch opened for this Pull Request.</p>
<p>Finally, Drone runs <a href="https://github.com/warpnet/salt-lint">salt-lint</a> over the Salt files to ensure there are no glaring mistakes or bad practices. For example, it will catch if file permissions are not being set as part of a file creation task, or modal permissions are valid, and much more.</p>
<p>I ignore two rules, which are <code>203 - Most files should not contain tabs</code> and <code>204 - Lines should be no longer than 160 chars</code>. The first is because of setting some file contents in a file on OpenBSD that requires tabs, and the second is because I use a lot of URLs that would become really hard to read (and reuse for other states) if I started to split them down. Most other Salt Lint rules are valid for my codebase, but these two would require making changes that I don&rsquo;t feel are all that beneficial.</p>
<p><img src="/img/homeinfra/drone-salt-lint.png" alt="Drone Salt Lint"></p>
<h3 id="highstate-test">Highstate Test</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">name</span>: Highstate test
  <span style="color:#66d9ef">commands</span>:
    - <span style="color:#e6db74">&#34;cd /srv/salt&#34;</span>
    - <span style="color:#e6db74">&#34;git checkout main&#34;</span>
    - <span style="color:#e6db74">&#34;sudo -u stuh84 git pull&#34;</span>
    - <span style="color:#e6db74">&#34;git checkout ${DRONE_SOURCE_BRANCH}&#34;</span> 
    - <span style="color:#e6db74">&#34;sudo salt --force-color &#39;*&#39; saltutil.pillar_refresh&#34;</span>
    - <span style="color:#e6db74">&#34;sudo salt --force-color &#39;*&#39; state.highstate --state-verbose=False test=True&#34;</span>
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">event</span>:
    - pull_request
</code></pre></div><p>In this, the commands are similar to those in the previous steps. The main difference here is running a &ldquo;pillar refresh&rdquo;, and a &ldquo;highstate&rdquo; in test mode.</p>
<p>The pillar refresh is used to ensure that each host that Salt manages has the latest pillar data (analogous to host/group variables in Ansible, or custom facts in Puppet). I have had issues in the past where states haven&rsquo;t applied correctly because the pillar data is not up to date on each node being managed. This makes sure that states don&rsquo;t fail erroneously because the agents haven&rsquo;t pulled in the latest data yet.</p>
<p>The highstate all states for each host that they assigned to. The <code>test=True</code> flag makes sure that changes are not applied (i.e. a dry run). The <code>state-verbose=False</code> flag tells Salt to only output tasks that have changed, rather than outputting the status of every task that ran on a node.</p>
<p><img src="/img/homeinfra/drone-salt-highstate-test.png" alt="Drone Salt Highstate Test"></p>
<h3 id="highstate">Highstate</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">name</span>: Salt Highstate 
  <span style="color:#66d9ef">commands</span>:
    - <span style="color:#e6db74">&#34;cd /srv/salt&#34;</span>
    - <span style="color:#e6db74">&#34;git checkout main&#34;</span>
    - <span style="color:#e6db74">&#34;sudo -u stuh84 git pull&#34;</span>
    - <span style="color:#e6db74">&#34;sudo salt --force-color &#39;*&#39; --state-verbose=False state.highstate&#34;</span>
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">branch</span>:
    - main
    <span style="color:#66d9ef">event</span>:
    - push
    - tag
</code></pre></div><p>This state performs a very similar function to the previous step. The main differences are: -</p>
<ul>
<li>Checking out the main branch, pulling in the changes, and NOT checking out another branch (i.e. running from the primary/default branch)</li>
<li>No pillar refresh, as this happens in the previous step, at which point it is very unlikely to be out of date</li>
</ul>
<p>This is where changes are actually applied, based upon all the new code being merged in. Other than Slack notifications, this is the final step that Drone will run.</p>
<p><img src="/img/homeinfra/drone-salt-highstate.png" alt="Drone Salt Highstate"></p>
<h2 id="demonstration">Demonstration</h2>
<p>The below video is a demonstration of making a change to the Salt repository and committing it. In this case I had no actual changes I needed to make, so this pipeline is based upon removing a line from the README file. The same process is used when making actual changes within Salt: -</p>

<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe src="https://www.youtube.com/embed/QBte44RTlPM" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allowfullscreen title="YouTube Video"></iframe>
</div>

<h2 id="summary">Summary</h2>
<p>In this, we have now seen how we can make use of the Exec runners and how to use them in the same pipelines as Docker runners.</p>
<p>This has also shown why it is preferred to use Docker runners, as Docker runners clone the repository directly and can run commands as if the repository is the base path. Exec runners are effectively like running a script on a server, meaning that you need to think about paths, Git flow (i.e. checking out code, making sure you have the most recent branch etc) whereas this isn&rsquo;t a consideration for the Docker runners.</p>
<p>In the next post, I&rsquo;m going to cover using Drone with Terraform. This will also involve using Hashicorp&rsquo;s Consul for a remote state backend.</p>
]]></content>
        </item>
        
        <item>
            <title>Home and Personal Infrastructure Overhaul: Part 5 - Using Drone with Ansible</title>
            <link>https://yetiops.net/posts/home-infra-overhaul-part5-drone-ansible/</link>
            <pubDate>Sun, 15 May 2022 16:04:50 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/home-infra-overhaul-part5-drone-ansible/</guid>
            <description>This post is the next in the series on how I overhauled my personal infrastructure to make it easier to manage, make changes and integrate new applications.
Previous posts in the series are: -
 Introduction Ansible Improvements SaltStack Improvements Introduction to Drone CI  Now that we have a better Ansible and Salt codebase, and have Drone working, we can start to look at some Drone pipelines! In this post, I will cover using Ansible with Drone.</description>
            <content type="html"><![CDATA[<p>This post is the next in the series on how I overhauled my personal infrastructure to make it easier to manage, make changes and integrate new applications.</p>
<p>Previous posts in the series are: -</p>
<ul>
<li><a href="https://yetiops.net/posts/home-infra-overhaul-part1/">Introduction</a></li>
<li><a href="https://yetiops.net/posts/home-infra-overhaul-part2-ansible-improvements/">Ansible Improvements</a></li>
<li><a href="https://yetiops.net/posts/home-infra-overhaul-part3-salt-improvements/">SaltStack Improvements</a></li>
<li><a href="https://yetiops.net/posts/home-infra-overhaul-part4-drone-getting-started/">Introduction to Drone CI</a></li>
</ul>
<p>Now that we have a better Ansible and Salt codebase, and have Drone working, we can start to look at some Drone pipelines! In this post, I will cover using Ansible with Drone.</p>
<h2 id="background">Background</h2>
<p>As mentioned in previous posts, I currently use Ansible to manage bootstrapping machines in my home infrastructure, as well as managing DNS, DHCP, IP address management, and adding monitoring/Prometheus checks for hosts not managed by Salt.</p>
<p>In addition, I have also recently started managing my lab environments (usually for Proof-of-Concepts, or even building infrastructure to use in posts on this site) using Ansible too.</p>
<p>To manage all of this before, it required manual updates, running multiple playbooks from the command line, all while logged in to a single server.</p>
<p>By creating pipelines in Drone, I can now commit changes directly to Git (from any machine, or even a phone/tablet) and the changes will roll out automatically.</p>
<h2 id="steps">Steps</h2>
<p>The flow I want to use for making changes is as follows: -</p>
<ol>
<li>Create a branch in Git</li>
<li>Make the relevant changes</li>
<li>Commit the branch to Git</li>
<li>Raise a Pull Request</li>
<li>Before merging, have the Ansible playbooks go through a syntax check and linting, as well as a dry run</li>
<li>Send a notification of success or failure</li>
<li>Show the results of the dry run</li>
<li>Merge the Pull Request</li>
<li>Apply the changes</li>
<li>Send a notification of success or failure</li>
</ol>
<p>This seems like a lot of steps, but this flow is how I tend to work anyway (personally and professionally). Drone adds the automated testing and applying the changes.</p>
<h2 id="full-drone-pipeline">Full Drone Pipeline</h2>
<p>The below is the full pipeline: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">kind</span>: pipeline
<span style="color:#66d9ef">name</span>: default
<span style="color:#66d9ef">type</span>: docker

<span style="color:#66d9ef">trigger</span>:
  <span style="color:#66d9ef">branch</span>:
    - main

<span style="color:#66d9ef">steps</span>:
- <span style="color:#66d9ef">name</span>: Syntax Check 
  <span style="color:#66d9ef">image</span>: plugins/ansible
  <span style="color:#66d9ef">settings</span>:
    <span style="color:#66d9ef">become</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">playbook</span>: playbook.yml
    <span style="color:#66d9ef">inventory</span>: inventory
    <span style="color:#66d9ef">private_key</span>:
      <span style="color:#66d9ef">from_secret</span>: drone_ssh_priv
    <span style="color:#66d9ef">ssh_extra_args</span>: <span style="color:#e6db74">&#34;-o StrictHostKeyChecking=no&#34;</span>
    <span style="color:#66d9ef">requirements</span>: requirements.txt
    <span style="color:#66d9ef">galaxy</span>: requirements.yml
    <span style="color:#66d9ef">syntax_check</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">event</span>:
    - pull_request

- <span style="color:#66d9ef">name</span>: Lint
  <span style="color:#66d9ef">image</span>: cytopia/ansible-lint
  <span style="color:#66d9ef">commands</span>:
    - ansible-lint playbook.yml --force-color
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">event</span>:
    - pull_request

- <span style="color:#66d9ef">name</span>: Show Diff and Check
  <span style="color:#66d9ef">image</span>: plugins/ansible
  <span style="color:#66d9ef">settings</span>:
    <span style="color:#66d9ef">become</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">playbook</span>: playbook.yml
    <span style="color:#66d9ef">inventory</span>: inventory
    <span style="color:#66d9ef">private_key</span>:
      <span style="color:#66d9ef">from_secret</span>: drone_ssh_priv
    <span style="color:#66d9ef">diff</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">check</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ssh_extra_args</span>: <span style="color:#e6db74">&#34;-o StrictHostKeyChecking=no&#34;</span>
    <span style="color:#66d9ef">requirements</span>: requirements.txt
    <span style="color:#66d9ef">galaxy</span>: requirements.yml
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">event</span>:
    - pull_request

- <span style="color:#66d9ef">name</span>: slack-pr
  <span style="color:#66d9ef">image</span>: plugins/slack
  <span style="color:#66d9ef">settings</span>:
    <span style="color:#66d9ef">webhook</span>:
      <span style="color:#66d9ef">from_secret</span>: drone_builds_slack_webhook 
    <span style="color:#66d9ef">channel</span>: builds
    <span style="color:#66d9ef">template</span>: <span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">      {{#success build.status}}</span>
        {{repo.name}} PR build passed. 
        Merge in to apply.
        <span style="color:#66d9ef">PR</span>: https://git.noisepalace.co.uk/YetiOps/{{repo.name}}/pulls/{{build.pull}}
        <span style="color:#66d9ef">Build</span>: https://drone.noisepalace.co.uk/YetiOps/{{repo.name}}/{{build.number}}
      {{else}}
        {{repo.name}} PR build failed. 
        Please investigate. 
        <span style="color:#66d9ef">PR</span>: https://git.noisepalace.co.uk/YetiOps/{{repo.name}}/pulls/{{build.pull}}
        <span style="color:#66d9ef">Build</span>: https://drone.noisepalace.co.uk/YetiOps/{{repo.name}}/{{build.number}}
      {{/success}}      
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">status</span>:
    - failure
    - success
    <span style="color:#66d9ef">event</span>:
      - pull_request

- <span style="color:#66d9ef">name</span>: slack-push-start
  <span style="color:#66d9ef">image</span>: plugins/slack
  <span style="color:#66d9ef">settings</span>:
    <span style="color:#66d9ef">webhook</span>:
      <span style="color:#66d9ef">from_secret</span>: drone_builds_slack_webhook 
    <span style="color:#66d9ef">channel</span>: builds
    <span style="color:#66d9ef">template</span>: <span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">      {{repo.name}} build is starting.</span>
      <span style="color:#66d9ef">Build</span>: https://drone.noisepalace.co.uk/YetiOps/{{repo.name}}/{{build.number}}
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">branch</span>:
    - main
    <span style="color:#66d9ef">event</span>:
    - push
    - tag

- <span style="color:#66d9ef">name</span>: Apply Playbook 
  <span style="color:#66d9ef">image</span>: plugins/ansible
  <span style="color:#66d9ef">settings</span>:
    <span style="color:#66d9ef">become</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">playbook</span>: playbook.yml
    <span style="color:#66d9ef">inventory</span>: inventory
    <span style="color:#66d9ef">private_key</span>:
      <span style="color:#66d9ef">from_secret</span>: drone_ssh_priv
    <span style="color:#66d9ef">ssh_extra_args</span>: <span style="color:#e6db74">&#34;-o StrictHostKeyChecking=no&#34;</span>
    <span style="color:#66d9ef">requirements</span>: requirements.txt
    <span style="color:#66d9ef">galaxy</span>: requirements.yml
    <span style="color:#66d9ef">diff</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">branch</span>:
    - main
    <span style="color:#66d9ef">event</span>:
    - push
    - tag

- <span style="color:#66d9ef">name</span>: slack-push
  <span style="color:#66d9ef">image</span>: plugins/slack
  <span style="color:#66d9ef">settings</span>:
    <span style="color:#66d9ef">webhook</span>:
      <span style="color:#66d9ef">from_secret</span>: drone_builds_slack_webhook 
    <span style="color:#66d9ef">channel</span>: builds
    <span style="color:#66d9ef">template</span>: <span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">      {{#success build.status}}</span>
        {{repo.name}} build passed.
        <span style="color:#66d9ef">Build</span>: https://drone.noisepalace.co.uk/YetiOps/{{repo.name}}/{{build.number}}
      {{else}}
        {{repo.name}} build {{build.number}} failed. Please investigate. 
        <span style="color:#66d9ef">Build</span>: https://drone.noisepalace.co.uk/YetiOps/{{repo.name}}/{{build.number}}
      {{/success}}      
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">status</span>:
    - failure
    - success
    <span style="color:#66d9ef">branch</span>:
    - main
    <span style="color:#66d9ef">event</span>:
    - push
    - tag
</code></pre></div><p>To cover a few of the terms: -</p>
<ul>
<li><code>kind</code> - This defines the kind of Drone execution. This is almost always going to be <code>pipeline</code> (although <code>template</code> is another option)</li>
<li><code>type</code> - This is the kind of execution. This could be <code>docker</code>, <code>exec</code>, <code>ssh</code>, <code>kubernetes</code> and many more</li>
<li><code>trigger</code> - This defines what conditions need to be met for a pipeline to start</li>
<li><code>steps</code> - These are the steps that the pipeline will take</li>
</ul>
<p>The <code>trigger</code> step in this pipeline specifies that it will only run if the destination branch of the Pull Request is <code>main</code>, or commits directly to <code>main</code>. Pull Requests between other branches (e.g. <code>dev</code> to <code>prod</code>) would not be covered by this.</p>
<h2 id="pipeline-steps">Pipeline Steps</h2>
<p>I&rsquo;ll now go through each step to show what they do.</p>
<h3 id="syntax-check">Syntax Check</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">steps</span>:
- <span style="color:#66d9ef">name</span>: Syntax Check 
  <span style="color:#66d9ef">image</span>: plugins/ansible
  <span style="color:#66d9ef">settings</span>:
    <span style="color:#66d9ef">become</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">playbook</span>: playbook.yml
    <span style="color:#66d9ef">inventory</span>: inventory
    <span style="color:#66d9ef">private_key</span>:
      <span style="color:#66d9ef">from_secret</span>: drone_ssh_priv
    <span style="color:#66d9ef">ssh_extra_args</span>: <span style="color:#e6db74">&#34;-o StrictHostKeyChecking=no&#34;</span>
    <span style="color:#66d9ef">requirements</span>: requirements.txt
    <span style="color:#66d9ef">galaxy</span>: requirements.yml
    <span style="color:#66d9ef">syntax_check</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">event</span>:
    - pull_request
</code></pre></div><p>This step details the following: -</p>
<ul>
<li>The name of the step</li>
<li>The Plugin (a.k.a. Docker Image) used to execute the step</li>
<li>Any settings specific to the plugin</li>
<li>When to execute the step</li>
</ul>
<p>All available <strong>Plugins</strong> for Drone can be found <a href="https://plugins.drone.io/">here</a>. In our case, we use a plugin (Docker Image) that will run Ansible. We also specify a few settings: -</p>
<ul>
<li><code>become: true</code> - This states that we will use privilege escalation (e.g. <code>sudo</code>, <code>doas</code>) for tasks
<ul>
<li>This is often a requirement when installing packages and/or updating configuration of system daemons</li>
</ul>
</li>
<li><code>playbook: playbook.yml</code> - This specifies the path for the playbook. In this repository, it is in the base path</li>
<li><code>inventory: inventory</code> - This specifies the inventory file for Ansible, which in this case is a file in the base path called <code>inventory</code></li>
<li><code>ssh_extra_args</code> - This specifies any extra arguments to the Ansible SSH client process</li>
<li><code>requirements: requirements.txt</code> - This specifies any Python dependencies required for the pipeline to run</li>
<li><code>galaxy: requirements.yml</code> - This specifies any Ansible Galaxy roles (and now <a href="https://docs.ansible.com/ansible/latest/user_guide/collections_using.html">Collections</a>) to install before running the Playbooks</li>
<li><code>private_key</code> - This section specifies the SSH private key used by Ansible/Drone to connect to machines in the inventory</li>
<li><code>syntax_check: true</code> - This tells Ansible to only check that the syntax of the roles and playbooks are correct, not actual running any steps</li>
</ul>
<p><img src="/img/homeinfra/drone-ansible-syntaxcheck.png" alt="Drone Ansible Syntax Check"></p>
<h4 id="ssh-extra-arguments">SSH Extra Arguments</h4>
<p>The extra argument supplied here is <code>-o StrictHostKeyChecking=no</code>. This tells Ansible to ignore if the host SSH key is unknown.</p>
<p>In most cases, you don&rsquo;t want to enable this as a host key changing may indicate a machine has been compromised. However because the Docker containers that apply the changes are brand new on every run, they would have no previous knowledge of any hosts. For Ansible to run from a container, we must ignore strict host key checking.</p>
<h4 id="private-key">Private Key</h4>
<p>The private key is created on another machine using <code>ssh-keygen -t rsa</code>. We add the public key as an authorized keys on all nodes we want to manage. In my case, this is done using an Ansible role (that Drone also controls the execution of).</p>
<p>The contents of the private key are then added to a secret within Drone.</p>
<p>Drone Secrets can be repository specific, or common across a Git organisation. The below shows this: -</p>
<p><img src="/img/homeinfra/drone-secrets.png" alt="Drone Secrets"></p>
<p>We can then source the contents of this secret within a Drone pipeline.</p>
<h4 id="when-clause">When clause</h4>
<p>The <code>when</code> field says when this action will take place. In this task, it says that it will take action on Pull Requests. Combined with the trigger on the <code>main</code> branch set across all steps in the Pipeline, this means that this will only take effect on Pull Requests into the <code>main</code> branch.</p>
<h3 id="lint">Lint</h3>
<p>The <code>lint</code> step is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">name</span>: Lint
  <span style="color:#66d9ef">image</span>: cytopia/ansible-lint
  <span style="color:#66d9ef">commands</span>:
    - ansible-lint playbook.yml --force-color
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">event</span>:
    - pull_request
</code></pre></div><p>This uses the <a href="https://github.com/ansible/ansible-lint">ansible-lint</a> package to check that we have used the correct syntax, loops are defined correctly, variables are defined correctly and many other rules too.</p>
<p>Rather than supplying configuration settings (like in the previous step), this just runs a command in a Docker image against our Playbook.</p>
<p><img src="/img/homeinfra/drone-ansible-lint.png" alt="Drone Ansible Lint"></p>
<h3 id="show-diff-and-check">Show Diff and Check</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">name</span>: Show Diff and Check
  <span style="color:#66d9ef">image</span>: plugins/ansible
  <span style="color:#66d9ef">settings</span>:
    <span style="color:#66d9ef">become</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">playbook</span>: playbook.yml
    <span style="color:#66d9ef">inventory</span>: inventory
    <span style="color:#66d9ef">private_key</span>:
      <span style="color:#66d9ef">from_secret</span>: drone_ssh_priv
    <span style="color:#66d9ef">diff</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">check</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ssh_extra_args</span>: <span style="color:#e6db74">&#34;-o StrictHostKeyChecking=no&#34;</span>
    <span style="color:#66d9ef">requirements</span>: requirements.txt
    <span style="color:#66d9ef">galaxy</span>: requirements.yml
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">event</span>:
    - pull_request
</code></pre></div><p>This step is almost identical to the Syntax Check stage, but with some minor differences: -</p>
<ul>
<li>We do not set <code>syntax_check</code> to <code>true</code></li>
<li>We add the <code>diff: true</code> and <code>check: true</code> flags</li>
</ul>
<p>This will do a Dry Run of the playbook (i.e. shows what actions would take place) and will also show a <code>diff</code> (i.e. the changes) between what already exists and what would change.</p>
<p>This allows us to see what actions would take place, without them actually being applied. We can then make a judgement call on whether the changes are correct before merging and applying the code.</p>
<p><img src="/img/homeinfra/drone-ansible-diff.png" alt="Drone Ansible Diff and Check"></p>
<h3 id="slack-pr">Slack PR</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">name</span>: slack-pr
  <span style="color:#66d9ef">image</span>: plugins/slack
  <span style="color:#66d9ef">settings</span>:
    <span style="color:#66d9ef">webhook</span>:
      <span style="color:#66d9ef">from_secret</span>: drone_builds_slack_webhook 
    <span style="color:#66d9ef">channel</span>: builds
    <span style="color:#66d9ef">template</span>: <span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">      {{#success build.status}}</span>
        {{repo.name}} PR build passed. 
        Merge in to apply.
        <span style="color:#66d9ef">PR</span>: https://git.noisepalace.co.uk/YetiOps/{{repo.name}}/pulls/{{build.pull}}
        <span style="color:#66d9ef">Build</span>: https://drone.noisepalace.co.uk/YetiOps/{{repo.name}}/{{build.number}}
      {{else}}
        {{repo.name}} PR build failed. 
        Please investigate. 
        <span style="color:#66d9ef">PR</span>: https://git.noisepalace.co.uk/YetiOps/{{repo.name}}/pulls/{{build.pull}}
        <span style="color:#66d9ef">Build</span>: https://drone.noisepalace.co.uk/YetiOps/{{repo.name}}/{{build.number}}
      {{/success}}            
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">status</span>:
    - failure
    - success
    <span style="color:#66d9ef">event</span>:
      - pull_request
</code></pre></div><p>This task is used to send notifications to a Slack instance. We have a template which includes relevant details (i.e. links to the build job and the pull request), and also shows a slightly different message based upon whether the job was successful or not.</p>
<p>One point to note here is that in the <code>when</code> section, we match on a <code>status</code> of <code>success</code> or <code>failure</code>. Most pipeline steps will not execute if a previous step has failed, which would mean we wouldn&rsquo;t receive notifications on failure (only success). Adding this condition means it will execute even if a previous step failed.</p>
<h3 id="slack-push-start">Slack Push Start</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">name</span>: slack-push-start
  <span style="color:#66d9ef">image</span>: plugins/slack
  <span style="color:#66d9ef">settings</span>:
    <span style="color:#66d9ef">webhook</span>:
      <span style="color:#66d9ef">from_secret</span>: drone_builds_slack_webhook 
    <span style="color:#66d9ef">channel</span>: builds
    <span style="color:#66d9ef">template</span>: <span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">      {{repo.name}} build is starting.</span>
      <span style="color:#66d9ef">Build</span>: https://drone.noisepalace.co.uk/YetiOps/{{repo.name}}/{{build.number}}      
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">branch</span>:
    - main
    <span style="color:#66d9ef">event</span>:
    - push
    - tag
</code></pre></div><p>This step is almost identical to the previous step, except that it matches upon a push to the <code>main</code> branch, or a <code>tag</code> being applied to a commit in the <code>main</code> branch.</p>
<p>This allows us to notify that a build has started.</p>
<p><img src="/img/homeinfra/drone-ansible-slack.png" alt="Drone Ansible Slack Notification"></p>
<h3 id="apply-playbook">Apply Playbook</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">name</span>: Apply Playbook 
  <span style="color:#66d9ef">image</span>: plugins/ansible
  <span style="color:#66d9ef">settings</span>:
    <span style="color:#66d9ef">become</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">playbook</span>: playbook.yml
    <span style="color:#66d9ef">inventory</span>: inventory
    <span style="color:#66d9ef">private_key</span>:
      <span style="color:#66d9ef">from_secret</span>: drone_ssh_priv
    <span style="color:#66d9ef">ssh_extra_args</span>: <span style="color:#e6db74">&#34;-o StrictHostKeyChecking=no&#34;</span>
    <span style="color:#66d9ef">requirements</span>: requirements.txt
    <span style="color:#66d9ef">galaxy</span>: requirements.yml
    <span style="color:#66d9ef">diff</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">branch</span>:
    - main
    <span style="color:#66d9ef">event</span>:
    - push
    - tag
</code></pre></div><p>This step is identical to the <strong>Show Diff and Check</strong> step, except that we do not use the <code>check</code> field. This will apply all the changes, as well as showing a <code>diff</code> of all changes (rather than only showing that tasks were executed).</p>
<p>We have also changed when this will run, which again is based upon a push to the <code>main</code> branch, or a tagged commit.</p>
<h3 id="slack-push">Slack Push</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">name</span>: slack-push
  <span style="color:#66d9ef">image</span>: plugins/slack
  <span style="color:#66d9ef">settings</span>:
    <span style="color:#66d9ef">webhook</span>:
      <span style="color:#66d9ef">from_secret</span>: drone_builds_slack_webhook 
    <span style="color:#66d9ef">channel</span>: builds
    <span style="color:#66d9ef">template</span>: <span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">      {{#success build.status}}</span>
        {{repo.name}} build passed.
        <span style="color:#66d9ef">Build</span>: https://drone.noisepalace.co.uk/YetiOps/{{repo.name}}/{{build.number}}
      {{else}}
        {{repo.name}} build {{build.number}} failed. Please investigate. 
        <span style="color:#66d9ef">Build</span>: https://drone.noisepalace.co.uk/YetiOps/{{repo.name}}/{{build.number}}
      {{/success}}            
  <span style="color:#66d9ef">when</span>:
    <span style="color:#66d9ef">status</span>:
    - failure
    - success
    <span style="color:#66d9ef">branch</span>:
    - main
    <span style="color:#66d9ef">event</span>:
    - push
    - tag
</code></pre></div><p>This final step sends a notification that the Playbook apply succeeded or failed. Again, this is based upon a push to the <code>main</code> branch (i.e. a merged pull request, or a direct push to the branch) or a tagged commit.</p>
<h2 id="demonstration">Demonstration</h2>
<p>The below video is a demonstration of making a simple change (adding a CNAME to DNS), and seeing Gitea and Drone work together to apply the changes: -</p>

<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe src="https://www.youtube.com/embed/ctAmsYxxxc8" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allowfullscreen title="YouTube Video"></iframe>
</div>

<h2 id="summary">Summary</h2>
<p>This has demonstrated how to use Ansible with Drone. I currently use this for multiple repositories, and will probably add more to it in future as well.</p>
<p>The next post will cover how I use Drone CI with SaltStack, which involves the Docker runner and the Exec runner.</p>
]]></content>
        </item>
        
        <item>
            <title>Home and Personal Infrastructure Overhaul: Part 4 - Getting Started with Drone CI</title>
            <link>https://yetiops.net/posts/home-infra-overhaul-part4-drone-getting-started/</link>
            <pubDate>Sun, 13 Feb 2022 08:10:30 +0000</pubDate>
            
            <guid>https://yetiops.net/posts/home-infra-overhaul-part4-drone-getting-started/</guid>
            <description>This post is the next in the series on how I overhauled my personal infrastructure to make it easier to manage, make changes and integrate new applications.
In the previous posts I covered the improvements to my Ansible Playbooks (i.e. a complete overhaul of them) and the improvements to my Saltstack environment. Both of these were pre-requisites to being able to implement a Continuous Integration environment on my infrastructure.
This post is going to cover the choices of CI environments I could use, and why I chose Drone.</description>
            <content type="html"><![CDATA[<p>This post is the next in the series on how I overhauled my personal infrastructure to make it easier to manage, make changes and integrate new applications.</p>
<p>In the previous posts I covered the improvements to my <a href="/posts/home-infra-overhaul-part2-ansible-improvements">Ansible Playbooks</a> (i.e. a complete overhaul of them) and the improvements to my <a href="/posts/home-infra-overhaul-part3-salt-improvements">Saltstack environment</a>. Both of these were pre-requisites to being able to implement a Continuous Integration environment on my infrastructure.</p>
<p>This post is going to cover the choices of CI environments I could use, and why I chose Drone.</p>
<h2 id="the-choices">The choices</h2>
<p>In terms of continuous integration, there are a number of different solutions to fit every kind of infrastructure, budget and need. For example, if all of your code is on GitHub, an obvious choice would be GitHub Actions. The same can be said of the Gitlab CI offering as well.</p>
<p>If you are more geared towards Windows deployment, you may choose something like TeamCity. If you are in AWS you may choose CodePipelines and CodeDeploy. If you are in Azure, you may choose Azure DevOps. The options are endless.</p>
<p>With this being the case, I needed to set out what I wanted in a CI tool: -</p>
<ul>
<li>Self-hosted
<ul>
<li>I have a homelab, so something I can run on it is preferable</li>
</ul>
</li>
<li>Resource usage isn&rsquo;t huge
<ul>
<li>My homelab is a 2012 Mac Mini and a 6th generation NUC, so resources are enough but not unlimited</li>
</ul>
</li>
<li>It integrates with my chosen Git repository provider</li>
<li>The pipelines are written in a simple language/format</li>
</ul>
<p>Before I found Drone, the two main contenders were Gitlab CI and Jenkins.</p>
<h3 id="jenkins">Jenkins?</h3>
<p>I only briefly considered Jenkins. Jenkins has it&rsquo;s place, but it isn&rsquo;t what I was after.</p>
<p>I&rsquo;m not a huge fan of the Groovy language for writing pipelines. Also the resource usage can be quite significant, especially if one of the plugins is poorly written.</p>
<p>I have supported Jenkins at multiple businesses, and always run into issues with the plugin system. Upgrading one can break others, or upgrading one core plugin means every other plugin needs updating too.</p>
<p>Due to this, it was easy for me to rule out Jenkins.</p>
<h3 id="gitlab-ci">Gitlab CI?</h3>
<p>Gitlab CI seemed like the obvious choice at this point. It also helps that I have integrated Gitlab CI before at one of my previous employers, so I already had a lot of experience with it.</p>
<p>However, after running just self-hosted Gitlab on a virtual machine on my infrastructure for a while, I found the resource usage to be a little too much. Even at idle, the machine used a lot of memory and CPU (at least compared to the other machines on my infrastructure).</p>
<p>I subsequently removed Gitlab from my personal infrastructure. With this being the case, I needed to move all my repositories from my self-hosted Gitlab instance into another Git system. Thankfully,</p>
<p>I already have a Gitea instance running on my infrastructure, so after migrating all the repositories to it, I began the search for a CI system that works with Gitea</p>
<h3 id="what-works-with-gitea">What works with Gitea?</h3>
<p>Gitea is a self-hosted Git service, providing a web interface, API and standard Git forge utilities (i.e. issues, pull requests, webhooks to external systems etc). However Gitea does not (at the time of writing) have an integrated CI system (unlike GitHub or Gitlab).</p>
<p>After a little research, I found the <a href="https://gitea.com/gitea/awesome-gitea">Awesome-Gitea</a> repository. This contains a list of useful utilities that can integrate with Gitea, or augment the functionality. In this list were a few CI systems that integrate natively with Gitea.</p>
<p>The one that caught my eye was <a href="https://www.drone.io/">Drone</a>. It is built in Go (so had a good chance of low resource usage), the pipelines are written in YAML, it is self-hosted, and integrates natively with Gitea.</p>
<p>I decided to give Drone a try.</p>
<h2 id="setting-up-the-drone-server">Setting up the Drone Server</h2>
<p>Drone has two versions, the Community edition and the Enterprise edition. I chose the Community edition as I didn&rsquo;t need any of the extra features that the Enterprise edition provides (like clustering, ephemeral agents/runners, autoscaling).</p>
<p>To install the server, Drone provide <a href="https://docs.drone.io/server/overview/">documentation</a> for integration with GitHub, Gitlab, Gogs (what Gitea is forked from), Gitea, Bitbucket and Gitee.</p>
<p>The installation uses Docker, so you need to have a system that supports containers (Podman will probably work, although I haven&rsquo;t tried it). You also to generate some secrets and an OAuth Application settings up in Gitea.</p>
<h3 id="creating-the-oauth-application-in-gitea">Creating the OAuth Application in Gitea</h3>
<p>To create an OAuth application in Gitea, click on your user icon in the top right hand corner, and click settings: -</p>
<p><img src="/img/homeinfra/gitea-menu.png" alt="Gitea Menu"></p>
<p>From here, choose Applications: -</p>
<p><img src="/img/homeinfra/gitea-tabs.png" alt="Gitea Tabs"></p>
<p>You should now see the option to <strong>Manage OAuth2 Applications</strong>. In the fields provided, add a name (e.g. <code>drone-ci</code>), and the URL Drone will run on followed by <code>/login</code>. In my home infrastructure, this is <code>https://drone.noisepalace.co.uk</code>, but in my demo lab, this is <code>http://192.168.203.10/login</code>: -</p>
<p><img src="/img/homeinfra/gitea-drone-app.png" alt="Gitea Drone App"></p>
<p>Click create application, save the Client ID and Client Secret provided, and we can now install Drone!</p>
<h3 id="installing-drone">Installing Drone</h3>
<p>To install Drone, we need a machine that is running Docker. I am running this on a Debian Bullseye machine (<a href="https://docs.docker.com/engine/install/debian/">Docker Installation</a> docs here), but any machine that can run Docker, Podman, or other OCI-compatible runtimes will probably be fine.</p>
<p>As per the Drone documentation, you need to create a shared secret that will be used for communication between the Drone server and the runners. The documentation suggests to use OpenSSL like so: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ openssl rand -hex <span style="color:#ae81ff">16</span>
bea26a2221fd8090ea38720fc445eca6
</code></pre></div><p>This isn&rsquo;t a requirement though. If you want to use <code>pwgen</code>, a password store, or making up your own secret, all are fine.</p>
<p>The documentation then says to pull down the Drone image and then run a Docker command directly to install it.</p>
<p>Personally, I prefer using something like <a href="https://docs.docker.com/compose/">Docker Compose</a> for this so that I always have the container declarations ready to use.</p>
<p>If you want to run this on Kubernetes you can, although I won&rsquo;t detail that in this post (maybe in the future if and when I move my infrastructure to Kubernetes). My <code>docker-compose.yml</code> file looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">version</span>: <span style="color:#e6db74">&#39;3.4&#39;</span>
<span style="color:#66d9ef">services</span>:
    <span style="color:#66d9ef">drone</span>:
        <span style="color:#66d9ef">ports</span>:
            - <span style="color:#e6db74">&#39;80:80&#39;</span>
            - <span style="color:#e6db74">&#39;443:443&#39;</span>
        <span style="color:#66d9ef">restart</span>: always
        <span style="color:#66d9ef">container_name</span>: drone
        <span style="color:#66d9ef">volumes</span>:
            - drone-storage:/data
        <span style="color:#66d9ef">environment</span>:
          - DRONE_GITEA_CLIENT_ID=<span style="color:#75715e">###CLIENT_ID###</span>
          - DRONE_GITEA_CLIENT_SECRET=<span style="color:#75715e">###CLIENT_SECRET###</span>
          - DRONE_GITEA_SERVER=http://<span style="color:#ae81ff">192.168.203.10</span>:<span style="color:#ae81ff">3000</span>
          - DRONE_RPC_SECRET=<span style="color:#75715e">###RPC_SECRET###</span>
          - DRONE_SERVER_HOST=<span style="color:#ae81ff">192.168.203.10</span>
          - DRONE_SERVER_PROTO=http
          - DRONE_USER_CREATE=username:stuh84,admin:<span style="color:#66d9ef">true</span>
        <span style="color:#66d9ef">image</span>: drone/drone:<span style="color:#ae81ff">2</span>
        <span style="color:#66d9ef">networks</span>:
          - drone
<span style="color:#66d9ef">networks</span>:
  <span style="color:#66d9ef">drone</span>:
    <span style="color:#66d9ef">external</span>: <span style="color:#66d9ef">false</span>

<span style="color:#66d9ef">volumes</span>:
  <span style="color:#66d9ef">drone-storage</span>:
    <span style="color:#66d9ef">external</span>: <span style="color:#66d9ef">false</span>
</code></pre></div><p>In a production scenario, you would want to replace the <code>DRONE_GITEA_SERVER</code> variable with a URL (e.g. <code>https://git.noisepalace.co.uk</code>), and the same for the <code>DRONE_SERVER_HOST</code> (e.g. <code>https://drone.noisepalace.co.uk</code>).</p>
<p>Place this <code>docker-compose.yaml</code> file in a directory (I have it in a directory called <code>drone</code>), run <code>docker-compose up -d</code>, and you should then have a functioning Drone server: -</p>
<p><img src="/img/homeinfra/drone-server.png" alt="Drone Server"></p>
<p>The nice part about setting up a Gitea OAuth2 application is that you can then login with your Gitea credentials.</p>
<p><img src="/img/homeinfra/drone-dashboard.png" alt="Drone Dashboard"></p>
<p>Now that we have the Drone server setup, we need some runners to run our CI jobs.</p>
<h2 id="setting-up-drone-runners">Setting up Drone Runners</h2>
<p>There are a few different <a href="https://docs.drone.io/runner/overview/">options</a> for runners with Drone. Some are infrastructure specific (e.g. Digital Ocean, AWS, MacStadium), whereas others are more generic. I currently use three different kinds of runners: -</p>
<ul>
<li><a href="https://docs.drone.io/runner/docker/overview/">Docker Runner</a> - This runner interacts with the Docker daemon on a host to run containers that will execute the actions specified in the CI jobs
<ul>
<li>This is my most used runner, with most tasks being suited to running in a container</li>
</ul>
</li>
<li><a href="https://docs.drone.io/runner/exec/overview/">Exec Runner</a> - This runner will run commands directly on a host rather than via containers
<ul>
<li>I use this for my Salt workflow, for reasons I will cover later</li>
</ul>
</li>
<li><a href="https://docs.drone.io/runner/ssh/overview/">SSH Runner</a> - This runner will use SSH to login to a host and run commands on it
<ul>
<li>I use this for some OpenBSD-specific tasks, as I haven&rsquo;t been able to get the Exec Runner working on OpenBSD</li>
</ul>
</li>
</ul>
<h3 id="docker-runner">Docker Runner</h3>
<p>As before, the Drone documentation uses Docker commands directly to create a runner. I again use Docker Compose to achieve this, so that everything is declared and stored in a Git repository for when I need it again in future.</p>
<p>The <code>docker-compose.yaml</code> looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">version</span>: <span style="color:#e6db74">&#39;3.4&#39;</span>
<span style="color:#66d9ef">services</span>:
    <span style="color:#66d9ef">drone-runner</span>:
        <span style="color:#66d9ef">ports</span>:
            - <span style="color:#e6db74">&#39;3000:3000&#39;</span>
        <span style="color:#66d9ef">restart</span>: always
        <span style="color:#66d9ef">container_name</span>: drone-runner
        <span style="color:#66d9ef">volumes</span>:
            - /var/run/docker.sock:/var/run/docker.sock
        <span style="color:#66d9ef">environment</span>:
          - DRONE_RPC_SECRET=<span style="color:#75715e">###RPC_SECRET###</span>
          - DRONE_RPC_HOST=<span style="color:#ae81ff">192.168.203.10</span>
          - DRONE_RPC_PROTO=http
          - DRONE_RUNNER_CAPACITY=<span style="color:#ae81ff">2</span>
          - DRONE_RUNNER_NAME=docker-runner
        <span style="color:#66d9ef">image</span>: drone/drone-runner-docker:<span style="color:#ae81ff">1</span>
        <span style="color:#66d9ef">networks</span>:
          - drone

<span style="color:#66d9ef">networks</span>:
  <span style="color:#66d9ef">drone</span>:
    <span style="color:#66d9ef">external</span>: <span style="color:#66d9ef">false</span>
</code></pre></div><p>Drone recommend that the runner runs on a different host from the Drone server itself.</p>
<p>At this point, we can run <code>docker-compose up -d</code> and see if it registers with the Drone server: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ docker-compose up -d
Creating network <span style="color:#e6db74">&#34;drone-runner_drone&#34;</span> with the default driver
Pulling drone-runner <span style="color:#f92672">(</span>drone/drone-runner-docker:1<span style="color:#f92672">)</span>...
1: Pulling from drone/drone-runner-docker
97518928ae5f: Pull complete
4af047b71fe6: Pull complete
de9b7e52f3f6: Pull complete
679d1ec5770c: Pull complete
Digest: sha256:70da970bb76a62567edbea1ac8002d9484664267f4cbb49fbd7c87a753d02260
Status: Downloaded newer image <span style="color:#66d9ef">for</span> drone/drone-runner-docker:1
Creating drone-runner ... <span style="color:#66d9ef">done</span>

$ docker logs drone-runner
time<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;2022-02-04T16:19:46Z&#34;</span> level<span style="color:#f92672">=</span>info msg<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;starting the server&#34;</span> addr<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;:3000&#34;</span>
time<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;2022-02-04T16:19:46Z&#34;</span> level<span style="color:#f92672">=</span>info msg<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;successfully pinged the remote server&#34;</span>
time<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;2022-02-04T16:19:46Z&#34;</span> level<span style="color:#f92672">=</span>info msg<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;polling the remote server&#34;</span> arch<span style="color:#f92672">=</span>amd64 capacity<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span> endpoint<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;http://192.168.203.10&#34;</span> kind<span style="color:#f92672">=</span>pipeline os<span style="color:#f92672">=</span>linux type<span style="color:#f92672">=</span>docker
</code></pre></div><p>Looks good!</p>
<h3 id="exec-runner">Exec Runner</h3>
<p>The Exec Runner runs directly on the host, so first we need to get the Exec Runner binary: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ curl -L https://github.com/drone-runners/drone-runner-exec/releases/latest/download/drone_runner_exec_linux_amd64.tar.gz | tar zx
$ sudo mv drone_runner_exec /usr/local/bin
</code></pre></div><p>Other architectures and operating systems are available from <a href="https://github.com/drone-runners/drone-runner-exec/releases">here</a>.</p>
<p>Next, we need to create a configuration file for Drone. I place this in <code>/etc/drone-runner-exec/config</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">DRONE_RPC_SECRET=###RPC_SECRET###
DRONE_RPC_HOST=192.168.203.10
DRONE_RPC_PROTO=http
DRONE_LOG_FILE=/var/log/drone-runner-exec/log.txt
DRONE_RUNNER_LABELS=exec:test
</code></pre></div><p>The configuration is similar to what we used in the Docker runner section. The only difference here is that we also specify <code>DRONE_RUNNER_LABELS</code>. These aren&rsquo;t strictly necessary, but you can use these labels to restrict jobs to execute on certain Runners.</p>
<p>For example, if I wanted to target my Salt Master server, I could use a label of <code>salt:server</code>, and then refer to that in the Drone CI jobs.</p>
<p>Once this is created, we can then create a service file. You can do this yourself, or the Exec Runner can do this for you by running: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ drone-runner-exec service install
$ drone-runner-exec service start
</code></pre></div><p>We can then check whether this is communicating with the Drone server like so: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ sudo systemctl status drone-runner-exec
● drone-runner-exec.service - Drone Exec Runner
     Loaded: loaded <span style="color:#f92672">(</span>/etc/systemd/system/drone-runner-exec.service; enabled; vendor preset: enabled<span style="color:#f92672">)</span>
     Active: active <span style="color:#f92672">(</span>running<span style="color:#f92672">)</span> since Fri 2022-02-04 11:33:55 EST; 6s ago
   Main PID: <span style="color:#ae81ff">5181</span> <span style="color:#f92672">(</span>drone-runner-ex<span style="color:#f92672">)</span>
      Tasks: <span style="color:#ae81ff">7</span> <span style="color:#f92672">(</span>limit: 2303<span style="color:#f92672">)</span>
     Memory: 2.5M
        CPU: 11ms
     CGroup: /system.slice/drone-runner-exec.service
             └─5181 /usr/local/bin/drone-runner-exec service run --config /etc/drone-runner-exec/config

Feb <span style="color:#ae81ff">04</span> 11:33:55 drone-runner systemd<span style="color:#f92672">[</span>1<span style="color:#f92672">]</span>: Started Drone Exec Runner.
Feb <span style="color:#ae81ff">04</span> 11:33:55 drone-runner drone-runner-exec<span style="color:#f92672">[</span>5181<span style="color:#f92672">]</span>: time<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;2022-02-04T11:33:55-05:00&#34;</span> level<span style="color:#f92672">=</span>info msg<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;starting the server&#34;</span> addr<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;:3000&#34;</span>
Feb <span style="color:#ae81ff">04</span> 11:33:55 drone-runner drone-runner-exec<span style="color:#f92672">[</span>5181<span style="color:#f92672">]</span>: time<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;2022-02-04T11:33:55-05:00&#34;</span> level<span style="color:#f92672">=</span>info msg<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;successfully pinged the remote server&#34;</span>
Feb <span style="color:#ae81ff">04</span> 11:33:55 drone-runner drone-runner-exec<span style="color:#f92672">[</span>5181<span style="color:#f92672">]</span>: time<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;2022-02-04T11:33:55-05:00&#34;</span> level<span style="color:#f92672">=</span>info msg<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;polling the remote server&#34;</span> capacity<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span> endpoint<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;http://192.168.203.10&#34;</span> kind<span style="color:#f92672">=</span>pipeline type<span style="color:#f92672">=</span>exec
</code></pre></div><p>Perfect.</p>
<h3 id="ssh-runner">SSH Runner</h3>
<p>The SSH runner is for hosts that can be reached via SSH, but can&rsquo;t necessarily run the Exec Runners themselves. Unfortunately I could not get the Exec Runner to run reliably on an OpenBSD machine, so I use the SSH runner for OpenBSD tasks instead.</p>
<p>Jobs that use the SSH runner require either supplying a user and password, or an SSH key pair to access the machine in question. I created a key pair for this with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ ssh-keygen -t ed25519 -f drone_ssh_key
</code></pre></div><p>This creates the <code>drone_ssh_key.pub</code> and <code>drone_ssh_key</code> files which can then be used with the SSH runner. You just need to add the public key as an authorized key for whatever machines you want to access, at which point jobs can specify to use these keys to access the machines in question.</p>
<p>To setup the SSH runner, you again use Docker. The instructions refer to using Docker directly, and again I use Docker Compose for this, like so: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">version</span>: <span style="color:#e6db74">&#39;3.4&#39;</span>
<span style="color:#66d9ef">services</span>:
    <span style="color:#66d9ef">drone-runner-ssh</span>:
        <span style="color:#66d9ef">ports</span>:
            - <span style="color:#e6db74">&#39;3100:3000&#39;</span>
        <span style="color:#66d9ef">restart</span>: always
        <span style="color:#66d9ef">container_name</span>: ssh-runner
        <span style="color:#66d9ef">environment</span>:
          - DRONE_RPC_SECRET=<span style="color:#75715e">###RPC_SECRET###</span>
          - DRONE_RPC_HOST=<span style="color:#ae81ff">192.168.203.10</span>
          - DRONE_RPC_PROTO=http
        <span style="color:#66d9ef">image</span>: drone/drone-runner-ssh
</code></pre></div><p>We run this as before, placing a <code>docker-compose.yaml</code> file in a directory of your choice (I chose <code>drone-ssh-runner</code>) and then run <code>docker-compose up -d</code>. You should see the runner connect to the Drone server: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ docker-compose up -d
Creating network <span style="color:#e6db74">&#34;drone-ssh-runner_default&#34;</span> with the default driver
Pulling drone-runner-ssh <span style="color:#f92672">(</span>drone/drone-runner-ssh:<span style="color:#f92672">)</span>...
latest: Pulling from drone/drone-runner-ssh
5a3ea8efae5d: Pull complete
db93b31631ef: Pull complete
5dfa4717c03d: Pull complete
Digest: sha256:cfab2ecc93abb9132f37ef01edc29ac80143e5217423095bb979c310804d3513
Status: Downloaded newer image <span style="color:#66d9ef">for</span> drone/drone-runner-ssh:latest
Creating ssh-runner ... <span style="color:#66d9ef">done</span>

$ docker logs ssh-runner
time<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;2022-02-06T15:20:29Z&#34;</span> level<span style="color:#f92672">=</span>info msg<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;starting the server&#34;</span> addr<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;:3000&#34;</span>
time<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;2022-02-06T15:20:29Z&#34;</span> level<span style="color:#f92672">=</span>info msg<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;successfully pinged the remote server&#34;</span>
time<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;2022-02-06T15:20:29Z&#34;</span> level<span style="color:#f92672">=</span>info msg<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;polling the remote server&#34;</span> capacity<span style="color:#f92672">=</span><span style="color:#ae81ff">10</span> endpoint<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;http://192.168.203.10&#34;</span> kind<span style="color:#f92672">=</span>pipeline type<span style="color:#f92672">=</span>ssh
</code></pre></div><p>Now that we have our runners, we can run some jobs!</p>
<h2 id="running-drone-pipelines">Running Drone Pipelines</h2>
<p>Drone pipelines are configured using YAML, in a file called <code>.drone.yml</code> that sits in the base path of a repository. The format of this file is like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">kind</span>: pipeline
<span style="color:#66d9ef">type</span>: docker
<span style="color:#66d9ef">name</span>: default

<span style="color:#66d9ef">steps</span>:
- <span style="color:#66d9ef">name</span>: testing
  <span style="color:#66d9ef">image</span>: alpine 
  <span style="color:#66d9ef">commands</span>:
  - echo <span style="color:#e6db74">&#34;Hello from Drone&#34;</span> 
</code></pre></div><p>The format is quite similar to Gitlab CI, GitHub Actions, and other CI systems that use YAML to define the steps. We need to take a couple of prerequisite steps before we can run a pipeline in Drone though.</p>
<h3 id="create-a-repository-in-gitea">Create a repository in Gitea</h3>
<p>If you currently have no repositories in Gitea, you will need to create one: -</p>
<p><img src="/img/homeinfra/gitea-new-repo.png" alt="Gitea New Repository"></p>
<p>Click on the plus sign, and follow the steps to create your first repository.</p>
<h3 id="sync-drone">Sync Drone</h3>
<p>If Drone is not synchronized yet, you won&rsquo;t see any repositories (or at least no new repositories if you already had some in your Gitea instance). Click on the Sync button in the top right hand corner, and Drone will get a list of all repositories that your user has access to in Gitea: -</p>
<p><img src="/img/homeinfra/gitea-drone-sync.png" alt="Gitea Drone Sync"></p>
<p>After this, you should now see your repository appear: -</p>
<p><img src="/img/homeinfra/drone-new-repo.png" alt="Drone New Repository"></p>
<h3 id="configure-repository">Configure repository</h3>
<p>Click on the repository in Drone, and then click <strong>Activate Repository</strong>: -</p>
<p><img src="/img/homeinfra/drone-activate-repo.png" alt="Drone Activate Repository"></p>
<p>You can now change settings like timeouts on builds (in case a build goes on too long), disabling builds that come from Pull Requests (may be useful in public repositories to stop your resources being used by drive-by commits!) and more. However to just start builds, you don&rsquo;t need to do anything else.</p>
<h3 id="add-the-droneyml-file-to-the-repo">Add the <code>.drone.yml</code> file to the repo</h3>
<p>You can now add the Drone pipeline file to the repository. We&rsquo;ll use our example repository here: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ git clone ssh://git@192.168.203.10:222/stuh84/drone-docker-runner
Cloning into <span style="color:#e6db74">&#39;drone-docker-runner&#39;</span>...
warning: You appear to have cloned an empty repository.

$ cd drone-docker-runner

$ cat &gt; .drone.yml <span style="color:#e6db74">&lt;&lt;- EOF
</span><span style="color:#e6db74">kind: pipeline
</span><span style="color:#e6db74">type: docker
</span><span style="color:#e6db74">name: default
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">steps:
</span><span style="color:#e6db74">- name: testing
</span><span style="color:#e6db74">  image: alpine
</span><span style="color:#e6db74">  commands:
</span><span style="color:#e6db74">  - echo &#34;Hello from Drone&#34;
</span><span style="color:#e6db74">EOF</span>

$ git add .drone.yml

$ git commit -m <span style="color:#e6db74">&#34;Added .drone.yml&#34;</span>
<span style="color:#f92672">[</span>master <span style="color:#f92672">(</span>root-commit<span style="color:#f92672">)</span> 65509e6<span style="color:#f92672">]</span> Added .drone.yml
 <span style="color:#ae81ff">1</span> file changed, <span style="color:#ae81ff">9</span> insertions<span style="color:#f92672">(</span>+<span style="color:#f92672">)</span>
 create mode <span style="color:#ae81ff">100644</span> .drone.yml

$ git push
Enumerating objects: 3, <span style="color:#66d9ef">done</span>.
Counting objects: 100% <span style="color:#f92672">(</span>3/3<span style="color:#f92672">)</span>, <span style="color:#66d9ef">done</span>.
Compressing objects: 100% <span style="color:#f92672">(</span>2/2<span style="color:#f92672">)</span>, <span style="color:#66d9ef">done</span>.
Writing objects: 100% <span style="color:#f92672">(</span>3/3<span style="color:#f92672">)</span>, <span style="color:#ae81ff">322</span> bytes | 322.00 KiB/s, <span style="color:#66d9ef">done</span>.
Total <span style="color:#ae81ff">3</span> <span style="color:#f92672">(</span>delta 0<span style="color:#f92672">)</span>, reused <span style="color:#ae81ff">0</span> <span style="color:#f92672">(</span>delta 0<span style="color:#f92672">)</span>, pack-reused <span style="color:#ae81ff">0</span>
remote: . Processing <span style="color:#ae81ff">1</span> references
remote: Processed <span style="color:#ae81ff">1</span> references in total
To ssh://192.168.203.10:222/stuh84/drone-docker-runner.git
 * <span style="color:#f92672">[</span>new branch<span style="color:#f92672">]</span>      main -&gt; main
</code></pre></div><p>Now every file you add to the repository should now trigger a pipeline in Drone.</p>
<h3 id="testing-the-pipeline">Testing the pipeline</h3>
<p>Lets commit a random file and see what happens: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">$ echo <span style="color:#e6db74">&#34;Test&#34;</span> &gt; test

$ git add .

$ git commit -m <span style="color:#e6db74">&#34;Test&#34;</span>
[main ab343da] Test build
 <span style="color:#ae81ff">1</span> file changed, <span style="color:#ae81ff">1</span> insertion(+)
 create mode <span style="color:#ae81ff">100644</span> test

$ git push
<span style="color:#66d9ef">Enumerating objects</span>: <span style="color:#ae81ff">4</span>, done.
<span style="color:#66d9ef">Counting objects</span>: <span style="color:#ae81ff">100</span>% (<span style="color:#ae81ff">4</span>/<span style="color:#ae81ff">4</span>), done.
<span style="color:#66d9ef">Compressing objects</span>: <span style="color:#ae81ff">100</span>% (<span style="color:#ae81ff">2</span>/<span style="color:#ae81ff">2</span>), done.
<span style="color:#66d9ef">Writing objects</span>: <span style="color:#ae81ff">100</span>% (<span style="color:#ae81ff">3</span>/<span style="color:#ae81ff">3</span>), <span style="color:#ae81ff">279</span> bytes | <span style="color:#ae81ff">279.00</span> KiB/s, done.
Total <span style="color:#ae81ff">3</span> (delta <span style="color:#ae81ff">0</span>), reused <span style="color:#ae81ff">0</span> (delta <span style="color:#ae81ff">0</span>), pack-reused <span style="color:#ae81ff">0</span>
<span style="color:#66d9ef">remote</span>: . Processing <span style="color:#ae81ff">1</span> references
<span style="color:#66d9ef">remote</span>: Processed <span style="color:#ae81ff">1</span> references in total
To ssh://<span style="color:#ae81ff">192.168.203.10</span>:<span style="color:#ae81ff">222</span>/stuh84/drone-docker-runner.git
   <span style="color:#ae81ff">65509e6</span>..ab343da  main -&gt; main
</code></pre></div><p>We should now see a build kick off in Drone: -</p>
<p><img src="/img/homeinfra/drone-build-test.png" alt="Drone Test Build"></p>
<p>We can see the steps of the build too: -</p>
<p><img src="/img/homeinfra/drone-test-build-clone.png" alt="Drone Test Build Clone"></p>
<p><img src="/img/homeinfra/drone-test-build-testing.png" alt="Drone Test Build Testing"></p>
<h2 id="summary">Summary</h2>
<p>At this point, we now have a running Drone server, as well as different kinds of runners that will be able to execute pipelines. The Drone <a href="https://docs.drone.io/">documentation</a> does a good job of showing how to create pipelines, different conditions to run pipelines based upon, as well as how to run pipelines across multiple different kinds of runners (whether different runners, different architectures or different operating systems).</p>
<p>Rather than going into too much detail in this post, I&rsquo;m going to cover each kind of runner and pipeline that uses it in future posts.</p>
<h2 id="future-posts">Future posts</h2>
<p>The next four posts are going to cover how to create Drone pipelines for: -</p>
<ul>
<li>Ansible - Using the Docker Runner for all tasks</li>
<li>Saltstack - Using the Exec Runner for most tasks, and the Docker Runner for notifications</li>
<li>Go building and releasing - Using the Docker Runner on <code>x86_64</code> and <code>arm64</code> Linux, as well as using the SSH runner for OpenBSD builds and releases</li>
<li>Hugo - Using the Docker Runner</li>
</ul>
<p>As we go through of these posts, we&rsquo;ll see how create pipelines, how to trigger them, how to take different steps based upon Pull Requests or merges to the main branch, and how through all of this Git now becomes the main method of defining and triggering changes on my infrastructure rather than manual intervention!</p>
]]></content>
        </item>
        
        <item>
            <title>Home and Personal Infrastructure Overhaul: Part 3 - Improving my Salt environment</title>
            <link>https://yetiops.net/posts/home-infra-overhaul-part3-salt-improvements/</link>
            <pubDate>Sun, 30 Jan 2022 09:04:25 +0000</pubDate>
            
            <guid>https://yetiops.net/posts/home-infra-overhaul-part3-salt-improvements/</guid>
            <description>The next part in this series of posts on how I overhauled the management of my Home and Personal infrastructure is on the changes to my Salt environment.
As mentioned in the last post, I decided to split my use of Ansible and Salt into: -
Ansible
 Bootstrapping nodes with the Saltstack agent and my SSH keys Taking a nodes SSH key and adding it to Gitea/Gitlab/GitHub Managing DNS and DHCP for hosts in my network (pools, static reservations, DNS entries etc) Adding certain hosts to Cloudflare DNS so that I can use Lets Encrypt on my applications Adding IPs/descriptions to Netbox for IPAM  Salt</description>
            <content type="html"><![CDATA[<p>The next part in this series of posts on how I overhauled the management of my Home and Personal infrastructure is on the changes to my Salt environment.</p>
<p>As mentioned in the <a href="/posts/home-infra-overhaul-part2-ansible-improvements/">last post</a>, I decided to split my use of Ansible and Salt into: -</p>
<p><strong>Ansible</strong></p>
<ul>
<li>Bootstrapping nodes with the Saltstack agent and my SSH keys</li>
<li>Taking a nodes SSH key and adding it to Gitea/Gitlab/GitHub</li>
<li>Managing DNS and DHCP for hosts in my network (pools, static reservations, DNS entries etc)</li>
<li>Adding certain hosts to Cloudflare DNS so that I can use Lets Encrypt on my applications</li>
<li>Adding IPs/descriptions to Netbox for IPAM</li>
</ul>
<p><strong>Salt</strong></p>
<ul>
<li>Pretty much everything else!</li>
</ul>
<p>Ansible is great as an early-stage/node preparation tool, as it doesn&rsquo;t rely on any Agents being applied. Once this is all done, Saltstack can takes over and manage what runs on nodes that have agents installed, what Prometheus exporters (i.e. monitoring agents) run alongside the applications, and more.</p>
<p>This series goes through what has changed in my home infrastructure, and more importantly why it needed to change. While my Ansible setup was rewritten from scratch mostly (new repository, new roles, different variable structures etc), my Salt environment didn&rsquo;t need quite as much work.</p>
<p>If you already use Salt, or want to, hopefully some of this will help you in improving your Salt environment.</p>
<h2 id="what-needed-to-change">What needed to change?</h2>
<p>When I started using Saltstack on my home infrastructure, I was already using it in a professional capacity too. While this doesn&rsquo;t guarantee that everything will be perfect, I had a solid grounding to build upon.</p>
<p>However I wasn&rsquo;t entirely happy with what I had put together. The main issues were: -</p>
<ul>
<li>Very little was managed with Salt
<ul>
<li>I only had Consul installation and configuration, some base packages and installing/enabling LLDP</li>
</ul>
</li>
<li>Consul pillar data (i.e. variables that applied to the hosts) was required to be defined up front for every host</li>
<li>Services would restart whenever Salt ran, even if nothing changed</li>
<li>The Pillars that defined what exporters Consul would add to it&rsquo;s service list was manually defined</li>
<li>Consul binaries were stored in the Git repository directly, rather than sourced from Hashicorp&rsquo;s repositories or releases</li>
<li>States across different operating systems (e.g. Ubuntu/Debian Linux, Alpine and OpenBSD) differed slightly, and yet were repeated in their entirety</li>
</ul>
<p>Why did I want to change this? I wanted to move to continuous integration for my infrastructure. Also, I wanted Git to be a source of truth for how my applications were defined. If every run restarts services, and every new host needs additional configuration, it makes the process less declarative and more reactive (and impactful).</p>
<p>Also, as I was migrating services from Ansible to Salt, I didn&rsquo;t want to inherit some of the same issues I had with the Ansible playbooks (non-repeatable tasks, locally-stored binaries, dependency issues). If I could get Salt into a good state, I would have a solid blueprint for adding more services to it.</p>
<h2 id="the-issues">The issues</h2>
<p>First I&rsquo;ll outline all the issues, and then I&rsquo;ll go through how I approached them.</p>
<h3 id="host-specific-pillars">Host-specific pillars</h3>
<p>My pillar <code>top.sls</code> file (the file which defines what pillars are tied to what host) had the following definition: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">base:
  '*':
    - consul.{{ grains['nodename'] }}
</code></pre><p>The asterisk (as you might be able to guess) means that this applies to every host. The <code>{{ grains['nodename'] }}</code> placeholder that refers to the name of the host. The below shows what the <code>nodename</code> is for some of my hosts: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#75715e"># salt &#39;*&#39; grains.item nodename</span>
archspire.noisepalace.home:
    ----------
    nodename:
        archspire
git-01.noisepalace.home:
    ----------
    nodename:
        git-01
vpn-01.noisepalace.home:
    ----------
    nodename:
        vpn-01
ns-03:
    ----------
    nodename:
        ns-03
pinkfloyd.noisepalace.home:
    ----------
    nodename:
        pinkfloyd
dev-02.noisepalace.home:
    ----------
    nodename:
        dev-02
</code></pre></div><p>What this means is that Salt will look for a file in the directory <code>consul</code> called <code>$NODENAME.sls</code> (e.g. <code>vpn-01.sls</code>, <code>dev-02.sls</code>). If a file does not exist for every node, Salt will throw an error, because the Pillar <code>top.sls</code> says that every host requires one.</p>
<p>This is an issue, as it means that when a brand new host is added, a file must be created that contains all the variables it needs. The point in configuration management is so that many hosts can be configured and prepared en masse. Defining a file for every single host goes against the principles of automation!</p>
<p>Why did I set up the pillars like this in the first place? The reason is because I run Consul on my VPSs (for Prometheus exporter service discovery). As I don&rsquo;t want to make my Consul servers publicly available, I needed to ensure the interface it would listen on (in my case, Wireguard tunnel interfaces). There are many ways to do this, but I chose the &ldquo;easiest&rdquo; rather than the most scalable. Hindsight is a wonderful thing!</p>
<h3 id="hard-coding-data-in-pillars">Hard-coding data in Pillars</h3>
<p>If we take a look at the old Pillar data, we can see the data Consul would use for binding to a specific interface: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e"># vps-shme.sls</span>
<span style="color:#66d9ef">consul</span>:
  <span style="color:#66d9ef">bind_int</span>: wg0
  <span style="color:#66d9ef">prometheus_services</span>:
    - node_exporter
    - wireguard
    - iptables
    - apache
    - mysql
    - icmp

<span style="color:#75715e"># ns-03.sls</span>
<span style="color:#66d9ef">consul</span>:
  <span style="color:#66d9ef">bind_int</span>: enp6s0
  <span style="color:#66d9ef">prometheus_services</span>:
    - node_exporter
    - named
    - pihole
    - dhcp
    - icmp
</code></pre></div><p>The variable <code>bind_int</code> was used in the Consul configuration template when deploying Consul to a node. It ensured Consul would bind to the correct interface. This required defining the pillar before we can run Salt, otherwise Salt would fail to run.</p>
<p>If we take a look at the Consul configuration template itself, we can see where this Pillar data was used: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">datacenter = &quot;noisepalace&quot;
data_dir = &quot;/opt/consul&quot;
encrypt = &quot;$TOKEN&quot;
retry_join = [&quot;192.168.0.6&quot;]
{%- if pillar['consul'] is defined %}
{%- if pillar['consul']['bind_int'] is defined %}
{%- set interface = pillar['consul']['bind_int'] %}
bind_addr = &quot;{{ grains['ip4_interfaces'][interface][0] }}&quot;
{%- endif %}
{%- endif %}

node_meta {
  env = &quot;noisepalace&quot; 
}

performance {
  raft_multiplier = 1
}
</code></pre><p>This takes the interface we supply in our Pillar data, and then derives the IP address of this interface from the host&rsquo;s grains (the host facts).</p>
<p>We also have a hard-coded list of what <code>prometheus_services</code> run on this machine. If one of the exporters is removed, it also needs to be removed from here. If I forgot to add the exporter to the list, it would never be monitored. This variable was used in the Consul State file (<code>states/consul/init.sls</code>) like so: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% if 'prometheus_services' in pillar['consul'] %}
{% for service in pillar['consul']['prometheus_services'] %}
/etc/consul.d/{{ service }}.json:
  file.managed:
    - source: salt://consul/files/{{ service }}.json
    - user: consul
    - group: consul
    - mode: 0640
    - template: jinja
{% endfor %}
{% endif %}
{% endif %}

consul_reload:
  cmd.run:
    - name: consul reload
</code></pre><p>For every service in the list, we would add the JSON file into the correct Consul directory, and then at the end we would reload Consul. While this task statement is okay, it is reliant on hard-coded data.</p>
<h3 id="needless-restartingreloading-of-services">Needless restarting/reloading of services</h3>
<p>As I didn&rsquo;t use Salt for many things, or run it often, the odd service restart wasn&rsquo;t an issue. However the more I moved to it, the more applications would restart without good reason. If every minor configuration change required restarting almost every application on a machine, the machines would have huge CPU/memory/disk usage spikes, as well as taking the services down.</p>
<p>As an example, the below reloads Consul, whether anything has changed or not: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">consul_reload:
  cmd.run:
    - name: consul reload
</code></pre><p>Also a lot of the Ansible Playbooks I wanted to migrate (particularly exporter installation) would restart services, Prometheus, exporters and more every time they ran, e.g.: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">name</span>: Daemon Reload and run prometheus
  <span style="color:#66d9ef">systemd</span>:
    <span style="color:#66d9ef">state</span>: restarted
    <span style="color:#66d9ef">enabled</span>: yes
    <span style="color:#66d9ef">daemon_reload</span>: yes
    <span style="color:#66d9ef">name</span>: prometheus
</code></pre></div><h3 id="binaries-stored-in-the-repository">Binaries stored in the repository</h3>
<p>Storing binaries in Git (unless you are using something like <a href="https://git-lfs.github.com/">Git LFS</a> is not recommended. The problem with having binaries stored in Git (or other version control repository) are: -</p>
<ul>
<li>Git is geared towards showing changes, which is difficult and not very useful on binary files</li>
<li>The repository size is significantly larger compared to storing text/configuration/code</li>
<li>The version of the binary is static until you download a new version and commit it to the repository</li>
</ul>
<p>Bear in mind that Git stores all versions of objects committed. If over time you have multiple versions of a binary (even if they have the same name) in the history, a clone of the repository in future would include all versions of that binary too.</p>
<p>The following is how Consul was installed previously: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">/usr/local/bin/consul:
  file.managed:
    {% if grains['os'] != 'Alpine' and grains['os'] != 'OpenBSD' and grains['cpuarch'] == 'x86_64' %}
    - source: salt://consul/files/consul_amd64
    {% elif grains['os'] != 'Alpine' and grains['os'] != 'OpenBSD' and 'arm' in grains['cpuarch'] %}
    - source: salt://consul/files/consul_arm
    {% endif %}
    - user: root
    - group: root
    - mode: 755
</code></pre><p>Here there is a version of Consul for <code>x86-64</code> machines, and one for <code>arm</code>, both stored in the repository.</p>
<h3 id="similar-tasks-repeated-for-different-systems">Similar tasks repeated for different systems</h3>
<p>In my network I run a mixture of Debian, Ubuntu, Alpine, OpenBSD and a single Arch Linux machine (mainly for development/testing).</p>
<p>Debian and Ubuntu are similar enough that most states for Debian work with Ubuntu. However Arch, Alpine, and OpenBSD especially, differ significantly. Arch uses a different package manager (and some different locations for some binaries), Alpine uses a different <a href="https://docs.alpinelinux.org/user-handbook/0.1a/Working/openrc.html">init</a> system and service manager, and OpenBSD is an entirely different operating system altogether.</p>
<p>To cater to these, many states were repeated but with minor differences. For example: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% if grains['os'] != 'OpenBSD' %}
/opt/consul:
  file.directory:
    - user: consul
    - group: consul
    - mode: 755
    - makedirs: True

/etc/consul.d:
  file.directory:
    - user: consul
    - group: consul
    - mode: 755
    - makedirs: True

[...]
{% if grains['os'] == 'OpenBSD' %}
/opt/consul:
  file.directory:
    - user: _consul
    - group: _consul
    - mode: 755
    - makedirs: True

/etc/consul.d:
  file.directory:
    - user: _consul
    - group: _consul
    - mode: 755
    - makedirs: True
[...]
</code></pre><p>The only difference in the above is that the user and group are prefixed by an underscore (<code>_</code>) on OpenBSD, and yet we have entirely separate tasks to create directories and add files.</p>
<p>If I need to change the directory that Consul stores data in or it&rsquo;s configuration, I would have had to change it in multiple places. This often leads to forgetting to change it in all places and breaking at least one of the systems used.</p>
<h2 id="the-changes">The Changes</h2>
<p>While the above issues were significant, they didn&rsquo;t require a complete refactor (unlike my Ansible repository).</p>
<h3 id="host-specific-pillars-1">Host-specific pillars</h3>
<p>I wanted to move away from host-specific pillars for every machine. This doesn&rsquo;t mean that hosts cannot have pillars that are specific to them, just that by default they are only required if they differ from the defaults. Having sensible defaults rather than no defaults can dramatically reduce complexity and manual configuration work.</p>
<p>I approached this by making better use of <a href="https://docs.saltproject.io/en/latest/topics/grains/#grains-in-the-minion-config">custom grains</a>.</p>
<p>Grains are like facts in Ansible. Salt derives the facts from the host, rather than defining them for the host. For example, the CPU architecture of a machine is derived from the host, as is what OS it is running.</p>
<p>As mentioned already, I use Ansible to bootstrap hosts and to deploy the Salt Minion application. A few minor changes in my Ansible code would help with solving this issue.</p>
<p>Previously, the Salt Minion configuration template looked like the below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">master: salt-master.noisepalace.home
id: {{ ansible_fqdn }}
nodename: {{ ansible_hostname }}
grains:
  nodename: {{ ansible_hostname }}
</code></pre><p>We had a single custom grain here (which overrides the discovered <code>nodename</code> grain), but nothing more. The below shows the changes I made to the template: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">master: salt-master.noisepalace.home
id: {{ ansible_fqdn }}
nodename: {{ ansible_hostname }}
grains:
  nodename: {{ ansible_hostname }}
{% if 'vps' in ansible_hostname %}
  bind_int: wg0
{% else %}
  bind_int: {{ ansible_default_ipv4['interface'] }}
{% endif %}
  groups:
{% for group in group_names %}
    - {{ group }}
{% endfor -%}
</code></pre><p>I added the <code>bind_int</code> grain, and also the <code>groups</code> grain. The <code>bind_int</code> grain will contain the primary interface on a machine (i.e. the one which has a default route) unless it is one of my VPSs. If it is a VPS, the <code>bind_int</code> grain will always be the Wireguard tunnel interface (<code>wg0</code>).</p>
<p>I then updated the Consul configuration template to remove the reliance on Pillar data: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">datacenter = &quot;noisepalace&quot;
data_dir = &quot;/opt/consul&quot;
encrypt = &quot;$ENCRYPTION_TOKEN&quot;
node_name = &quot;{{ grains['nodename'] }}&quot;
retry_join = [&quot;192.168.0.6&quot;]
{%- set interface = grains['bind_int'] %}
bind_addr = &quot;{{ grains['ip4_interfaces'][interface][0] }}&quot;

node_meta {
  env = &quot;noisepalace&quot;
}

performance {
  raft_multiplier = 1
}
</code></pre><p>The Consul pillar data for each host is no longer required for it to be installed and configured correctly.</p>
<p>We still need to do something about the Prometheus services that are registered with Consul, but I will cover that in a later section.</p>
<p>In addition, I added the Ansible groups that each host is in as grains. This means that I can define a host as having a <strong>role</strong>, and then install packages/states/configuration based upon the <strong>role</strong>. A example of this is below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">base:
[...]
  'groups:vmh':
    - match: grain
    - vmh
    - exporters.libvirt_exporter
    - exporters.syncthing_exporter
    - docker
    - syncthing
</code></pre><p>The above says that if a host is in the <code>vmh</code> group, we will install the <code>vmh</code> states (which installed KVM/QEMU and related services), Docker, Syncthing (for syncing ISOs between hosts) and related exporters for monitoring.</p>
<p>I also use this to say whether the <code>qemu-guest-agent</code> package should be installed on a machine, based upon it being in the <code>vmh-guest</code> group: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">base_packages:
  pkg.installed:
  - pkgs:
    - lldpd
    - jq
    - rsync
    - wget
[...]
{% if 'vmh-guests' in grains['groups'] %}
    - qemu-guest-agent
{% endif %}
[...]
</code></pre><p>This means the <code>qemu-guest-agent</code> package is not installed on the hypervisors and VPSs, only those that need it (i.e. my virtual machines).</p>
<h3 id="hard-coding-data-in-pillars-1">Hard-coding data in pillars</h3>
<p>I am no longer hard-coding which interface Consul will bind to (using custom grains). The next improvement is to remove the hard-coded list of exporters to register with Consul (for Prometheus to discover and monitor).</p>
<p>Part of the issue previously is that because all of the exporters were installed by Ansible, Salt did not know what was already installed on a machine. However now the exporters are deployed with Salt, we can also include the relevant file to register them with Consul: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">## Wireguard exporter 
[...]
/etc/consul.d/wireguard.json:
  file.managed:
    - source: salt://consul/files/wireguard.json
    - user: consul
    - group: consul
    - mode: 0640
    - template: jinja

consul_reload_wireguard:
  cmd.run:
    - name: consul reload
    - onchanges:
      - file: /etc/consul.d/wireguard.json

## Pihole exporter 
[...]
/etc/consul.d/pihole.json:
  file.managed:
    - source: salt://consul/files/pihole.json
    - user: consul
    - group: consul
    - mode: 0640
    - template: jinja

consul_reload_pihole:
  cmd.run:
    - name: consul reload
    - onchanges:
      - file: /etc/consul.d/pihole.json
</code></pre><p>These types of tasks are included in the State file for each exporter. If an exporter is installed, it gets registered with Consul automatically. This removes the reliance Pillar data for services Prometheus needs to monitor, meaning no Consul pillar needs to be defined for every host.</p>
<p>This isn&rsquo;t to say that no Consul pillar data exists at all though. Some applications already have a metrics/monitoring endpoint (i.e. no exporters are required) that exposes metrics in the correct Prometheus format. In this case, we still supply a Consul pillar like so: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">## git-01.sls
consul:
  prometheus_services:
    - gitea

## pinkfloyd.sls
consul:
  prometheus_services:
    - traefik
    - alertmanager
</code></pre><p>However unlike before, this doesn&rsquo;t cover every possible Prometheus service, just additional services. If I show what services are running on <strong>pinkfloyd</strong> for example: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ consul catalog services -node pinkfloyd
alertmanager
cadvisor
consul
icmp
libvirt
mikrotik_exporter
nextcloud
nginx
node_exporter
pihole
redis
speedtest
syncthing
traefik
wireguard
</code></pre></div><p>All but <code>traefik</code> and <code>alertmanager</code> are not dependent upon Pillar data.</p>
<h3 id="restartingreloading-of-services">Restarting/reloading of services</h3>
<p>To avoid restarting/reloading services when nothing has changed, I used three different directives in Salt: -</p>
<ul>
<li><code>onchanges</code> - This will run a task only if a prerequisite task has changed</li>
<li><code>watch</code> - This is very similar to <code>onchanges</code>, except that for certain kinds of tasks additional actions may be taken</li>
<li><code>require</code> - This only requires that a task completed successfully, as opposed to a previous task completing and making changes</li>
</ul>
<p>A good example of <code>require</code> is ensuring a package has been installed before trying to configure it.</p>
<p>If a package fails to installed, then the files it would install (e.g. service files, configuration, directories) will not exist. If a subsequent task tries to put additional files into the directory that the package installation would have created, it will also fail. With the <code>require</code> keyword, we ensure that tasks with hard dependencies do not run without their prerequisites completing first.</p>
<p>The <code>onchanges</code> directive means that the task will only run if a prerequisite task made any changes. This is useful for things like reloading Consul if a file was added/changed in it&rsquo;s configuration directory, or performing a <code>systemctl daemon-reload</code> if the SystemD unit file changes.</p>
<p>A good example with both <code>require</code> and <code>onchanges</code> is below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">test_prometheus_config:
  cmd.run:
    - name: &quot;/usr/local/bin/promtool check config /etc/prometheus/prometheus.yml&quot;
    - onchanges:
      - file: /etc/prometheus/prometheus.yml
      - file: add_prometheus_alerts

prometheus_reload:
  cmd.run:
    - name: curl -X POST localhost:9090/-/reload
    - onchanges:
      - file: /etc/prometheus/prometheus.yml
      - file: add_prometheus_alerts
    - require:
      - test_prometheus_config
</code></pre><p>In the above, we use <code>promtool</code> (a Prometheus utility CLI tool) to check that our Prometheus configuration is correct. The <code>promtool</code> command will also check Alertmanager rules by default. This uses <code>onchanges</code> to make sure it only runs if there have been updates to the <code>/etc/prometheus/prometheus.yml</code> task or the <code>add_prometheus_alerts</code> task.</p>
<p>If the <code>test_prometheus_config</code> task runs without errors, then we trigger a reload of Prometheus  via the API. If the <code>test_prometheus_config</code> task fails, Prometheus does not reload. If neither the Prometheus configuration or Alertmanager rules have changed, no reload takes place.</p>
<p>This not only ensures that Prometheus only reloads when it&rsquo;s configuration files changes, but has added benefit of making sure they are syntactically correct.</p>
<p>A quick point to the note is that Salt tasks can have custom names (e.g. <code>add_prometheus_alerts</code>) or the name can be the file/service being changed (e.g. <code>/etc/prometheus/prometheus.yml</code>). The <code>onchanges</code> directive must refer to the name of the task, whether that is a custom name, or matches the file/service being acted upon.</p>
<h4 id="watch-versus-onchanges"><code>watch</code> versus <code>onchanges</code></h4>
<p>The <code>onchanges</code> directive instructs Salt to run the task if a preceding task changed. In the above we can see that this runs commands on the machines in question to test/reload Prometheus.</p>
<p>Take the below as an example: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">prometheus_service:
  service.running:
    - name: prometheus
    - enable: True
    - onchanges:
      - file: /etc/systemd/system/prometheus.service
</code></pre><p>When Prometheus is installed, this will ensure the service is running and is enabled. If the service file changes though, the task doesn&rsquo;t do anything because the service is still running (unless another process/user stopped it) and is still enabled.</p>
<p>What <code>watch</code> does is for certain types of tasks, it will perform additional actions. If we change the above to this: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">prometheus_service:
  service.running:
    - name: prometheus
    - enable: True
    - watch:
      - file: /etc/systemd/system/prometheus.service
</code></pre><p>Salt will now perform the additional action of restarting the service if the file it is watching changes. Not every type of task has an additional <code>watch</code> behaviour, but in this case it helps to cut down on the amount of tasks we need to define.</p>
<h3 id="binaries-stored-in-the-repository-1">Binaries stored in the repository</h3>
<p>This is a pretty straightforward change to make, especially for Consul. <a href="https://hashicorp.com">Hashicorp</a> now make a lot of their products available via Apt and RPM repositories. This means all my Debian and Ubuntu machines can now use the repositories, and get updates when they are available (not when I remember to update the binary in my Git repository).</p>
<p>This doesn&rsquo;t help with my OpenBSD, Alpine and Arch machines. For them, I use what is in their package repositories. All three do keep relatively up-to-date packages for Consul, whereas the versions in the Debian/Ubuntu archives are usually months/years old.</p>
<p>To move to this approach, we use: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% if grains['os_family'] == 'Debian' %}
consul_repo:
  pkgrepo.managed:
    - humanname: Hashicorp
    - name: &quot;deb https://apt.releases.hashicorp.com {{ grains['oscodename'] }} main&quot;
    - dist: {{ grains['oscodename'] }}
    - file: /etc/apt/sources.list.d/hashicorp.list
    - gpgcheck: 1
    - key_url: https://apt.releases.hashicorp.com/gpg
{% endif %}

{% if grains['os_family'] == 'RedHat'  and grains['os'] != 'Fedora' %}
consul_repo:
  pkgrepo.managed:
    - humanname: Hashicorp
    - file: /etc/yum.repos.d/hashicorp.repo
    - baseurl: https://rpm.releases.hashicorp.com/RHEL/$releasever/$basearch/stable
    - gpgcheck: 1
    - gpgkey: https://rpm.releases.hashicorp.com/gpg
{% endif %}

{% if grains['os_family'] == 'RedHat' and grains['os'] == 'Fedora' %}
consul_repo:
  pkgrepo.managed:
    - humanname: Hashicorp
    - file: /etc/yum.repos.d/hashicorp.repo
    - baseurl: https://rpm.releases.hashicorp.com/fedora/$releasever/$basearch/stable
    - gpgcheck: 1
    - gpgkey: https://rpm.releases.hashicorp.com/gpg
{% endif %}

{% if grains['os_family'] == 'Suse' %}
Virtualization_containers:
  pkgrepo.managed:
    - humanname: &quot;Virtualization:containers (openSUSE_Tumbleweed_and_d_l_g)&quot;
    - baseurl: https://download.opensuse.org/repositories/Virtualization:/containers/openSUSE_Tumbleweed_and_d_l_g/
    - gpgcheck: 1
    - gpgkey: https://download.opensuse.org/repositories/Virtualization:/containers/openSUSE_Tumbleweed_and_d_l_g/repodata/repomd.xml.key
    - autorefresh: 0
    - gpgautoimport: True
{% endif %}

consul_package:
  pkg.installed:
  - pkgs:
    - consul
  - refresh: True

[...]
</code></pre><p>As you can see, I also have declarations for RHEL-based machines and SuSE. While I don&rsquo;t currently run them on my infrastructure, it is worth having these for if and when I do need them. I wanted to be agnostic as I can be to what systems I need in my infrastructure, allowing me to choose the best systems for each role.</p>
<p>The last task referenced above installs Consul, and refreshes the package repositories so that it will use the latest package available. This ensures that we install from the Hashicorp repository after it is added (to the systems that repositories exist for). This will also use the native package installer on each system, whether that is Apt, DNF, Pacman, Zypper, APK, pkg_add or anything else.</p>
<p>In the cases where official package repositories are not available (e.g. most exporters, Prometheus and more), we can use this approach instead: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% if 'x86_64' in grains['cpuarch'] %}
retrieve_bind_exporter:
  cmd.run:
    - name: wget -O /tmp/bind_exporter.tar.gz https://github.com/prometheus-community/bind_exporter/releases/download/v{{ pillar['bind_exporter']['version'] }}/bind_exporter-{{ pillar['bind_exporter']['version'] }}.linux-amd64.tar.gz
{% endif %}
{% if 'aarch64' in grains['cpuarch'] %}
retrieve_bind_exporter:
  cmd.run:
    - name: wget -O /tmp/bind_exporter.tar.gz https://github.com/prometheus-community/bind_exporter/releases/download/v{{ pillar['bind_exporter']['version'] }}/bind_exporter-{{ pillar['bind_exporter']['version'] }}.linux-arm64.tar.gz
{% endif %}

extract_bind_exporter:
  archive.extracted:
    - name: /tmp
    - enforce_toplevel: false
    - source: /tmp/bind_exporter.tar.gz
    - archive_format: tar
    - user: root
    - group: root

{% if 'x86_64' in grains['cpuarch'] %}
/usr/local/bin/bind_exporter:
  file.rename:
    - name: /usr/local/bin/bind_exporter
    - source: /tmp/bind_exporter-{{ pillar['bind_exporter']['version'] }}.linux-amd64/bind_exporter

delete_bind_exporter_dir:
  file.absent:
    - name: /tmp/bind_exporter-{{ pillar['bind_exporter']['version'] }}.linux-amd64
{% endif %}

{% if 'aarch64' in grains['cpuarch'] %}
/usr/local/bin/bind_exporter:
  file.rename:
    - name: /usr/local/bin/bind_exporter
    - source: /tmp/bind_exporter-{{ pillar['bind_exporter']['version'] }}.linux-arm64/bind_exporter

delete_bind_exporter_dir:
  file.absent:
    - name: /tmp/bind_exporter-{{ pillar['bind_exporter']['version'] }}.linux-arm64
{% endif %}

delete_bind_exporter_files:
  file.absent:
    - name: /tmp/bind_exporter.tar.gz
{% endif %}
</code></pre><p>This does pin the exporters to a version rather than having them on the latest, but it does at least ensure that I am not storing an old version in a repository and using that.</p>
<h3 id="similar-tasks-repeated-for-different-systems-1">Similar tasks repeated for different systems</h3>
<p>One of the issues I found with having tasks that are almost identical but with one field/option different (e.g. a different username/group) is that I would often make updates to one and not the other, and then wonder why my changes were not taking place.</p>
<p>Unlike Ansible, Salt generates the state files on the fly, allowing you to use Jinja2 templating syntax inside of them. How does this help us? Lets see: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% if grains['os'] == 'FreeBSD' %}
/usr/local/etc/consul.d/consul.hcl:
{% else %}
/etc/consul.d/consul.hcl:
{% endif %}
  file.managed:
    - source: salt://consul/files/consul.hcl
{% if grains['os'] == 'OpenBSD' %}
    - user: _consul
    - group: _consul
{% else %}
    - user: consul
    - group: consul
{% endif %}
    - mode: 0640
    - template: jinja
</code></pre><p>The first conditional evaluates based upon whether the machine running this task is a FreeBSD machine or not. FreeBSD uses a different location for user-installed binaries and configuration files (usually prefixed with <code>/usr/local</code>). With the conditional in place, we can use a different filename based upon the system.</p>
<p>After this, we also have a conditional to say that if we are on OpenBSD, the user and group for Consul is <code>_consul</code>. For any other system, the user and group would be <code>consul</code>.</p>
<p>Rather than potentially 3 different tasks (a standard task, FreeBSD with a differing filename, OpenBSD with different user/groups), we have a single task with some conditionals in place. Updates to this task will now apply to all machines, not just the ones I remember to update!</p>
<h2 id="what-else">What else?</h2>
<p>So after mitigating all these issue, what else needed to be done? Move as much to Salt as possible!</p>
<p>As mentioned, previously I only installed and managed Consul with Salt. Now, I manage the following: -</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td>alertmanager</td>
<td>Receive alerts from Prometheus and send them to Slack</td>
</tr>
<tr>
<td>apache_exporter</td>
<td>For monitoring Apache2</td>
</tr>
<tr>
<td>base</td>
<td>Install base utilities</td>
</tr>
<tr>
<td>bind_exporter</td>
<td>For monitoring Bind9</td>
</tr>
<tr>
<td>blackbox_exporter</td>
<td>For monitoring ICMP/HTTP(S)/TCP connections</td>
</tr>
<tr>
<td>cadvisor</td>
<td>For container monitoring</td>
</tr>
<tr>
<td>consul</td>
<td>Install Consul and any relevant services defined in Pillars</td>
</tr>
<tr>
<td>dhcp_exporter</td>
<td>For monitoring isc-dhcp-server</td>
</tr>
<tr>
<td>docker</td>
<td>Install Docker, docker-compose, and customise the Docker daemon configuration</td>
</tr>
<tr>
<td>gitea</td>
<td>Gitea version control installation and configuration</td>
</tr>
<tr>
<td>gitea-release</td>
<td>For deploying my Gitea Release Golang application to manage releases)</td>
</tr>
<tr>
<td>iptables_exporter</td>
<td>For monitoring traffic usage on machines with iptables installed</td>
</tr>
<tr>
<td>libvirt_exporter</td>
<td>For monitoring KVM machines</td>
</tr>
<tr>
<td>lldp</td>
<td>For adding LLDP (Link Layer Discovery Protocol) to all my machines</td>
</tr>
<tr>
<td>mikrotik_exporter</td>
<td>For monitoring my home router</td>
</tr>
<tr>
<td>mysqld_exporter</td>
<td>For monitoring MySQL</td>
</tr>
<tr>
<td>nextcloud_exporter</td>
<td>For monitoring my Nextcloud instances</td>
</tr>
<tr>
<td>nfs</td>
<td>For enabling NFS on servers that need it</td>
</tr>
<tr>
<td>nginx_exporter</td>
<td>For monitoring Nginx</td>
</tr>
<tr>
<td>node_exporter</td>
<td>For monitoring host metrics and custom script output</td>
</tr>
<tr>
<td>oxidized</td>
<td>Adds Oxidized for backing up my network configs</td>
</tr>
<tr>
<td>pihole_exporter</td>
<td>For monitoring my PiHole servers</td>
</tr>
<tr>
<td>plex_exporter</td>
<td>For monitoring my Plex setup</td>
</tr>
<tr>
<td>postgres_exporter</td>
<td>For monitoring PostgreSQL</td>
</tr>
<tr>
<td>print</td>
<td>Manages CUPs on my Raspberry Pi Print Server</td>
</tr>
<tr>
<td>prometheus</td>
<td>Installs and manages Prometheus and Alert rules</td>
</tr>
<tr>
<td>promtail</td>
<td>For sending logs to Loki</td>
</tr>
<tr>
<td>rclone</td>
<td>For backing up various data to Backblaze</td>
</tr>
<tr>
<td>redis_exporter</td>
<td>For monitoring my Redis instances</td>
</tr>
<tr>
<td>salt</td>
<td>For managing the Salt Master configuration</td>
</tr>
<tr>
<td>samba</td>
<td>For enabling Samba on machines that require it</td>
</tr>
<tr>
<td>snmp_exporter</td>
<td>For monitoring my other network equipment (Unifi, ZyXEL and my FS.com switch)</td>
</tr>
<tr>
<td>snmp</td>
<td>For enabling the SNMP daemon on some servers</td>
</tr>
<tr>
<td>speedtest_exporter</td>
<td>For monitoring my Internet bandwidth</td>
</tr>
<tr>
<td>sshkeys</td>
<td>For deploying my sshkeys Golang application to all my machines</td>
</tr>
<tr>
<td>syncthing</td>
<td>For deploying Syncthing to machines that require it</td>
</tr>
<tr>
<td>syncthing_exporter</td>
<td>For monitoring my Syncthing instances</td>
</tr>
<tr>
<td>traefik</td>
<td>For deploying a Traefik container to machines running Docker services</td>
</tr>
<tr>
<td>unpoller</td>
<td>For more in-depth monitoring of my Unifi APs</td>
</tr>
<tr>
<td>vaultsql</td>
<td>For deploying my VaultSQL Golang application that manages my FreeRADIUS users in MySQL (for use with 802.1x on my wireless networks)</td>
</tr>
<tr>
<td>vmh</td>
<td>For deploying KVM to machines that require it</td>
</tr>
<tr>
<td>wireguard</td>
<td>For deploying Wireguard to a number of machines that require tunnels between them</td>
</tr>
<tr>
<td>wireguard_exporter</td>
<td>For monitoring my Wireguard tunnels</td>
</tr>
</tbody>
</table>
<p>I have also integrated Hashicorp&rsquo;s Vault for secret/token management. This means I can avoid committing sensitive values to Git repositories. I will go into more information on this in a future post, including how to use it with Ansible, Salt, Drone and using the Go SDK.</p>
<h2 id="what-still-needs-work">What still needs work?</h2>
<p>There are still a few things I want to tackle in my Salt setup, some minor, some quite major.</p>
<p>Some of my state files could be improved. For example, the states I use to deploy the Prometheus <code>node_exporter</code> need work. These separate files (with very similar tasks) for Linux systems using SystemD, Alpine and OpenBSD. As shown already, these can be consolidated quite easily. However I essentially used the same code as laid out in my <a href="/posts/prometheus-consul-saltstack-part-1-linux/">series on deploying Prometheus and Consul with Saltstack</a>.</p>
<p>Not all applications I use are managed by Salt yet. Most are, but I haven&rsquo;t tackled FreeRADIUS, monitoring OpenBSD package upgrades, and a few other small pieces. These aren&rsquo;t huge, but they do need to be managed.</p>
<p>Finally, I want to make more use of Custom Grains. The grains I have now are fine, but the more I have, the less static host definitions/wildcard matches are required in my <code>top.sls</code> files. Examples could include deploying the Salt master, deploying Print servers, what requires Wireguard tunnels and more. This would allow me to treat all my machines as having <strong>roles</strong> instead. This simplifies configuration, and makes it easy to add new hosts with similar/the same <strong>roles</strong>.</p>
<h2 id="bonus-section-why-not-salt-masterless-instead-of-ansible">Bonus Section: Why not Salt Masterless instead of Ansible?</h2>
<p>When I released the last post, I got a question on the <a href="https://www.adminadminpodcast.co.uk/telegram">Admin Admin Podcast Telegram Channel</a> as to why I don&rsquo;t use <a href="https://docs.saltproject.io/en/latest/topics/tutorials/quickstart.html">Salt Masterless</a> instead of Ansible. This a valid question, especially as it would mean only managing one type of codebase. I have a few reasons for this.</p>
<p>Firstly, the process to use Salt Masterless still requires bootstrapping Salt onto a machine.</p>
<p>We also need to add additional configuration to Salt on a machine to tell it to use Masterless mode (see the referenced documentation for what needs to be done)</p>
<p>Additionally, the Salt configuration must be available to the machine (by cloning it locally, or defining a Git source in the Salt configuration).</p>
<p>Finally, Salt Masterless works the opposite way to Ansible. Rather than Ansible telling multiple machines what to do, Salt Masterless requires running Salt commands directly on each host to update their configuration. If you are familiar with Puppet (and running <code>puppet agent -t</code> in Cron/service files periodically), this is the same sort of model.</p>
<p>To make this even less desirable, the machines would then need their Salt configuration replaced at the end of the bootstrap process to now use Salt in non-Masterless mode.</p>
<p>If anything, the work to use Masterless mode requires more steps than using non-Masterless!</p>
<p>Masterless mode makes a huge amount of sense in a Cloud-like environment, using something like Cloud-Init/Cloud-Config to install Salt on the machines, and then point Salt to Git as a source of it&rsquo;s state files. I wouldn&rsquo;t hesitate to use it in that sort of scenario.</p>
<p>With my infrastructure though, Ansible makes more sense. Short of adding a user with the correct privileges to the machine so that Ansible can run the tasks it needs, I don&rsquo;t need to do anything else. It also fits much better into using continuous integration, as Salt Masterless would require logging to every host and running commands. Ansible manages all the host interaction, it just needs a valid user and credentials/SSH key available.</p>
<h2 id="summary">Summary</h2>
<p>The updates made to Salt, and turning it into (mostly) the source of truth of what runs on my infrastructure (that isn&rsquo;t in a container) has made managing my personal infrastructure a lot nicer.</p>
<p>My applications are up to date, the configuration is version controlled for the applications, and now I can trust what is in Git is also what is running.</p>
<p>It is also now in a state that if no changes are made in my Salt code, no changes are seen when running Salt. Services aren&rsquo;t restarted every time, configuration is repeatable, everything runs clean! This is exactly what I was aiming for, paving the way to using a Continuous Integration system to manage and deploy future changes.</p>
<h2 id="next-post">Next post</h2>
<p>The next post in this series will cover getting started with <a href="https://www.drone.io/">Drone</a>, the different kinds of &ldquo;runners&rdquo;, and the Drone files that contain the actions to take. Once this is done, there will be subsequent posts on how I integrated Ansible and Salt with Drone, how I build Go binaries, and how I deploy this website that you&rsquo;re reading this post on using Drone too!</p>
]]></content>
        </item>
        
        <item>
            <title>Home and Personal Infrastructure Overhaul: Part 2 - Moving to Ansible Roles</title>
            <link>https://yetiops.net/posts/home-infra-overhaul-part2-ansible-improvements/</link>
            <pubDate>Sun, 16 Jan 2022 10:57:01 +0000</pubDate>
            
            <guid>https://yetiops.net/posts/home-infra-overhaul-part2-ansible-improvements/</guid>
            <description>In my last post I covered how my home and personal infrastructure management was due an overhaul. When I had first put together all of the configuration management for my home infrastructure, I hadn&amp;rsquo;t used Ansible in about 3 or 4 years, and was almost starting from zero (especially with the amount of changes that happened to Ansible in that time!).
I had a collection (pun not intended) of single-use playbooks that performed tasks like: -</description>
            <content type="html"><![CDATA[<p>In my last <a href="/posts/home-infra-overhaul-part1">post</a> I covered how my home and personal infrastructure management was due an overhaul. When I had first put together all of the configuration management for my home infrastructure, I hadn&rsquo;t used Ansible in about 3 or 4 years, and was almost starting from zero (especially with the amount of changes that happened to Ansible in that time!).</p>
<p>I had a collection (pun not intended) of single-use playbooks that performed tasks like: -</p>
<ul>
<li>Updating DHCP (<a href="https://www.isc.org/dhcp/">ISC DHCP</a>) server configuration</li>
<li>Updating DNS (<a href="https://www.isc.org/bind/">BIND9</a>) server configuration</li>
<li>Updating Prometheus configuration (including Alertmanager and associated alert rules)</li>
<li>Installing Prometheus exporters on new machines</li>
<li>Installing the <code>salt-minion</code> (for Saltstack configuration management) package on new machines</li>
</ul>
<p>To set up a new machine, I had to update a file that contained the machine&rsquo;s intended IP address, MAC address (for static DHCP allocations) and then run each playbook against it.</p>
<p>This isn&rsquo;t particularly scalable, and it made moving to a continuous integration system really difficult.</p>
<h2 id="what-is-a-role">What is a role?</h2>
<p>An Ansible Role is a set of tasks that are related to each other, e.g. they configure/install a certain application. More often than not you would create a role to install and configure a single service, e.g. <strong>bind</strong> for DNS or <strong>isc-dhcp-server</strong>.</p>
<p>Roles can also be shared (either in your organisation, or publicly). Rather than duplicating code, you can use your own (or other peoples) roles again when the need arises. The Ansible Role <a href="https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_roles.html">Documentation</a> gives a more in-depth overview of what a role is and how to use it.</p>
<p>A Role has a certain directory structure, like so: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">-- roles
    |-- base
    |   |-- defaults
    |   |-- files
    |   |-- handlers
    |   |-- meta
    |   |-- tasks
    |   |-- templates
    |   |-- tests
    |   |-- vars
    |-- dns
    |   |-- defaults
    |   |-- files
    |   |-- handlers
    |   |-- meta
    |   |-- tasks
    |   |-- templates
    |   |-- tests
    |   |-- vars
</code></pre></div><p>You can create the directory structure yourself, or you can use <code>ansible-galaxy init $ROLENAME</code>. The latter will instantiate a directory with the correct structure, as well as some basic README files, tests and base YAML files to add your tasks/handlers/tests to. <a href="https://galaxy.ansible.com/">Ansible Galaxy</a> is the central repository of Ansible Roles that are shared publicly, but you can use the CLI tool to create local roles without a requirement to share them.</p>
<p>Typically in an Ansible playbook if you want to use a template, a file, handler or anything else you need to give the full path (or at least the relative path) to find them. However within a role, Ansible will look for files in one of the role directories first. For example, if you had a task like the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">name</span>: Copy file to server
  <span style="color:#66d9ef">copy</span>:
    <span style="color:#66d9ef">src</span>: myfile.txt
    <span style="color:#66d9ef">dest</span>: /tmp/myfile.txt
    <span style="color:#66d9ef">owner</span>: myuser
    <span style="color:#66d9ef">group</span>: mygroup
    <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0644</span>
</code></pre></div><p>If you place the file <code>myfile.txt</code> in the <code>files</code> directory of your role, Ansible will look there first, meaning you do not need to specify the full/relative path. This allows you to keep all related files, templates and tasks in the same role, making it much easier to share with others. It also helps when you looking at the role again a few months down the line.</p>
<h3 id="how-can-we-use-a-role">How can we use a role?</h3>
<p>To use a role, create a directory called <code>roles</code>, add the role in there, and then create a playbook in the top-level directory that references it. The below is a short example of using a role: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">hosts</span>: all
  <span style="color:#66d9ef">become</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">roles</span>:
    - <span style="color:#66d9ef">role</span>: base
      <span style="color:#66d9ef">tags</span>:
        - base
</code></pre></div><p>In this, we have included a role called <code>base</code>, as well as adding a <strong>tag</strong> to it. Tags can be used to run only certain roles/tasks. If you run <code>ansible-playbook --tags base playbook.yml</code>, the only tasks/roles that will run are those tagged with <code>base</code>. This is especially helpful when creating your roles and testing them.</p>
<p>You can also use public roles (from Ansible Galaxy) like so: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">hosts</span>: all
  <span style="color:#66d9ef">become</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">roles</span>:
    - geerlingguy.docker
</code></pre></div><p>If you then add the following to a <code>requirements.yaml</code> file at the base of your repository: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">roles</span>:
  - <span style="color:#66d9ef">name</span>: geerlingguy.docker
</code></pre></div><p>By default this will look for the role in Ansible Galaxy before trying to run against your hosts. You can also refer to roles directly from Git repositories using: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">roles</span>:
  - <span style="color:#66d9ef">name</span>: nginx_role
    <span style="color:#66d9ef">src</span>: https://github.com/bennojoy/nginx
    <span style="color:#66d9ef">version</span>: main
</code></pre></div><p>This is how you can share roles in your organisation without necessarily making them available for public consumption.</p>
<p>To specify multiple roles for different hosts in a single playbook, you can do something like the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">hosts</span>: archspire.noisepalace.home
  <span style="color:#66d9ef">become</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">roles</span>:
    - <span style="color:#66d9ef">role</span>: blackbox_checks
      <span style="color:#66d9ef">tags</span>:
        - blackbox
        - prometheus

- <span style="color:#66d9ef">hosts</span>: vmh
  <span style="color:#66d9ef">become</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">roles</span>:
    - <span style="color:#66d9ef">role</span>: cockpit
      <span style="color:#66d9ef">tags</span>:
        - cockpit

- <span style="color:#66d9ef">hosts</span>: ns<span style="color:#ae81ff">-04.</span>noisepalace.home,ns<span style="color:#ae81ff">-03.</span>noisepalace.home
  <span style="color:#66d9ef">become</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">roles</span>:
    - <span style="color:#66d9ef">role</span>: dns
      <span style="color:#66d9ef">tags</span>:
        - dns
        - bind9
    - <span style="color:#66d9ef">role</span>: dhcp
      <span style="color:#66d9ef">tags</span>:
        - dhcp
</code></pre></div><p>What we can see here is that I am running different roles against different hosts, all from the same place. This means that when you run the single playbook, all machines will get the changes they need for every role applicable to them.</p>
<h2 id="working-example-dns">Working example: DNS</h2>
<p>The best way to show what has changed is to go through a previous example of how I used to update DNS and compare it with what I use now. DNS is a good example because it uses some tasks, installs packages and uses files and templates.</p>
<h3 id="old-playbook">Old Playbook</h3>
<p>The previous playbook looked like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
- <span style="color:#66d9ef">hosts</span>: <span style="color:#ae81ff">192.168.10.1</span>,<span style="color:#ae81ff">192.168.10.2</span>
  <span style="color:#66d9ef">become</span>: yes
  <span style="color:#66d9ef">tasks</span>:
  - <span style="color:#66d9ef">name</span>: Install bind9
    <span style="color:#66d9ef">package</span>:
      <span style="color:#66d9ef">state</span>: present
      <span style="color:#66d9ef">name</span>: bind9
      <span style="color:#66d9ef">update_cache</span>: yes

- <span style="color:#66d9ef">hosts</span>: <span style="color:#ae81ff">192.168.10.1</span>
  <span style="color:#66d9ef">become</span>: yes
  <span style="color:#66d9ef">tasks</span>:
  - <span style="color:#66d9ef">name</span>: Pick up vars file
    <span style="color:#66d9ef">include_vars</span>:
      <span style="color:#66d9ef">file</span>: ../dhcp/ip-hosts.yaml
      <span style="color:#66d9ef">name</span>: home_hosts
    <span style="color:#66d9ef">tags</span>:
    - configuration

  - <span style="color:#66d9ef">name</span>: Pick up net vars file
    <span style="color:#66d9ef">include_vars</span>:
      <span style="color:#66d9ef">file</span>: ../netvars.yaml
    <span style="color:#66d9ef">tags</span>:
    - configuration

  - <span style="color:#66d9ef">name</span>: Generate reverse lookup file
    <span style="color:#66d9ef">template</span>:
      <span style="color:#66d9ef">src</span>: named/db<span style="color:#ae81ff">.168.192</span>.in-addr.arpa.j2
      <span style="color:#66d9ef">dest</span>: /etc/bind/db<span style="color:#ae81ff">.168.192</span>.in-addr.arpa
    <span style="color:#66d9ef">tags</span>:
    - configuration

  - <span style="color:#66d9ef">name</span>: Generate forward lookup file
    <span style="color:#66d9ef">template</span>:
      <span style="color:#66d9ef">src</span>: named/db.noisepalace.home.j2
      <span style="color:#66d9ef">dest</span>: /etc/bind/db.noisepalace.home
    <span style="color:#66d9ef">tags</span>:
    - configuration

  - <span style="color:#66d9ef">name</span>: Named Local
    <span style="color:#66d9ef">copy</span>:
      <span style="color:#66d9ef">src</span>: named/named.conf.local.primary
      <span style="color:#66d9ef">dest</span>: /etc/bind/named.conf.local
    <span style="color:#66d9ef">tags</span>:
    - configuration

  - <span style="color:#66d9ef">name</span>: Named Options
    <span style="color:#66d9ef">template</span>:
      <span style="color:#66d9ef">src</span>: named/named.conf.options.primary
      <span style="color:#66d9ef">dest</span>: /etc/bind/named.conf.options
    <span style="color:#66d9ef">tags</span>:
    - configuration

- <span style="color:#66d9ef">hosts</span>: <span style="color:#ae81ff">192.168.10.2</span>
  <span style="color:#66d9ef">become</span>: yes
  <span style="color:#66d9ef">tasks</span>:
  - <span style="color:#66d9ef">name</span>: Pick up net vars file
    <span style="color:#66d9ef">include_vars</span>:
      <span style="color:#66d9ef">file</span>: ../netvars.yaml
    <span style="color:#66d9ef">tags</span>:
    - configuration

  - <span style="color:#66d9ef">name</span>: Named Local
    <span style="color:#66d9ef">copy</span>:
      <span style="color:#66d9ef">src</span>: named/named.conf.local.secondary
      <span style="color:#66d9ef">dest</span>: /etc/bind/named.conf.local
    <span style="color:#66d9ef">tags</span>:
    - configuration

  - <span style="color:#66d9ef">name</span>: Named Options
    <span style="color:#66d9ef">template</span>:
      <span style="color:#66d9ef">src</span>: named/named.conf.options.secondary
      <span style="color:#66d9ef">dest</span>: /etc/bind/named.conf.options
    <span style="color:#66d9ef">tags</span>:
    - configuration


- <span style="color:#66d9ef">hosts</span>: <span style="color:#ae81ff">192.168.10.1</span>,<span style="color:#ae81ff">192.168.10.2</span>
  <span style="color:#66d9ef">become</span>: yes
  <span style="color:#66d9ef">tasks</span>:

  - <span style="color:#66d9ef">name</span>: Stats directory
    <span style="color:#66d9ef">file</span>:
      <span style="color:#66d9ef">path</span>: /var/bind
      <span style="color:#66d9ef">owner</span>: bind
      <span style="color:#66d9ef">group</span>: bind
      <span style="color:#66d9ef">mode</span>: <span style="color:#e6db74">&#39;0755&#39;</span>
      <span style="color:#66d9ef">state</span>: directory
    <span style="color:#66d9ef">tags</span>:
    - configuration

  - <span style="color:#66d9ef">name</span>: Reload bind9
    <span style="color:#66d9ef">systemd</span>:
      <span style="color:#66d9ef">name</span>: bind9
      <span style="color:#66d9ef">state</span>: reloaded
      <span style="color:#66d9ef">enabled</span>: yes
    <span style="color:#66d9ef">tags</span>:
    - configuration
</code></pre></div><p>There are a few issues with this playbook that I wanted to address: -</p>
<ul>
<li>Statically defined hosts
<ul>
<li>This means that if I wanted to re-use this with other hosts, I have to update this playbook (and all the host declarations inside of it)</li>
</ul>
</li>
<li>The same/similar tasks repeated</li>
<li>BIND9 will be reloaded at the end of this playbook, even if nothing changed</li>
<li>We refer to two different sets of variables files, including one that is in a different &ldquo;roles&rdquo; directory
<ul>
<li>As this didn&rsquo;t use true Ansible Roles before, I refer to a role here as a different directory of Playbooks</li>
</ul>
</li>
<li>One of the sets of variables is imported with a prefix of <code>home_hosts</code> on every variable</li>
</ul>
<p>Some of this is down to my understanding when creating the Playbooks, but some of it was down to just wanting to get it to work and then change later (which I expected to happen much sooner than 2 years later!). For those who know how often a temporary fix becomes semi-permanent, this shouldn&rsquo;t come as too much of a surprise 😄.</p>
<p>An example of the variables are below: -</p>
<h4 id="ip-hostsyaml"><code>ip-hosts.yaml</code></h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">home_hosts</span>:
   <span style="color:#66d9ef">router</span>:
      <span style="color:#66d9ef">mac</span>: c4:ad:<span style="color:#ae81ff">34</span>:<span style="color:#ae81ff">55</span>:cb:<span style="color:#ae81ff">03</span>
      <span style="color:#66d9ef">ip_addr</span>: <span style="color:#ae81ff">192.168.0.1</span>
   <span style="color:#66d9ef">VanHalen</span>:
      <span style="color:#66d9ef">mac</span>: <span style="color:#ae81ff">02</span>:<span style="color:#ae81ff">95</span>:<span style="color:#ae81ff">09</span>:c0:d1:9a
      <span style="color:#66d9ef">ip_addr</span>: <span style="color:#ae81ff">192.168.0.3</span>
   <span style="color:#66d9ef">archspire</span>:
      <span style="color:#66d9ef">mac</span>: f4:4d:<span style="color:#ae81ff">30</span>:<span style="color:#ae81ff">64</span>:4d:bf
      <span style="color:#66d9ef">ip_addr</span>: <span style="color:#ae81ff">192.168.0.5</span>
   <span style="color:#66d9ef">pinkfloyd</span>:
      <span style="color:#66d9ef">mac</span>: a8:<span style="color:#ae81ff">20</span>:<span style="color:#ae81ff">66</span>:<span style="color:#ae81ff">34</span>:f0:2b
      <span style="color:#66d9ef">ip_addr</span>: <span style="color:#ae81ff">192.168.0.6</span>
[...]
   <span style="color:#66d9ef">behemoth</span>:
      <span style="color:#66d9ef">cname</span>: behemoth-10g
   <span style="color:#66d9ef">pihole</span>:
      <span style="color:#66d9ef">cname</span>: netutil<span style="color:#ae81ff">-01</span>
   <span style="color:#66d9ef">grafana</span>:
      <span style="color:#66d9ef">cname</span>: pinkfloyd
   <span style="color:#66d9ef">prometheus</span>:
      <span style="color:#66d9ef">cname</span>: pinkfloyd
</code></pre></div><h4 id="netvarsyaml"><code>netvars.yaml</code></h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">netvars</span>:
  <span style="color:#66d9ef">ip_ranges</span>:
    - <span style="color:#ae81ff">192.168.0.0</span>/<span style="color:#ae81ff">24</span>
    - <span style="color:#ae81ff">192.168.2.0</span>/<span style="color:#ae81ff">24</span>
    - <span style="color:#ae81ff">192.168.3.0</span>/<span style="color:#ae81ff">24</span>
    - <span style="color:#ae81ff">192.168.4.0</span>/<span style="color:#ae81ff">24</span>
    - <span style="color:#ae81ff">192.168.5.0</span>/<span style="color:#ae81ff">24</span>
    - <span style="color:#ae81ff">192.168.10.0</span>/<span style="color:#ae81ff">24</span>
    - <span style="color:#ae81ff">192.168.20.0</span>/<span style="color:#ae81ff">24</span>
    - <span style="color:#ae81ff">192.168.99.0</span>/<span style="color:#ae81ff">24</span>
    - <span style="color:#ae81ff">192.168.100.0</span>/<span style="color:#ae81ff">24</span>
    - <span style="color:#ae81ff">192.168.255.0</span>/<span style="color:#ae81ff">24</span>
</code></pre></div><h3 id="old-templates">Old templates</h3>
<p>The previous templates used looked like the below: -</p>
<h4 id="dbnoisepalacehomej2"><code>db.noisepalace.home.j2</code></h4>
<pre><code class="language-jinja2" data-lang="jinja2">;
; BIND data file for local loopback interface
;
$TTL    604800
@       IN      SOA     noisepalace.home. root.noisepalace.home. (
                              2         ; Serial
                         604800         ; Refresh
                          86400         ; Retry
                        2419200         ; Expire
                         604800 )       ; Negative Cache TTL
;
@               	IN      NS      ns4.noisepalace.home.
@               	IN      A       192.168.10.1
ns3             	IN      A       192.168.10.2
ns4             	IN      A       192.168.10.1
{% for name in home_hosts.home_hosts %}
{% if &quot;cname&quot; in home_hosts.home_hosts[name] %}
{{ name|lower }} IN      CNAME  {{ home_hosts.home_hosts[name]['cname'] }}
{% else %}
{{ name|lower }} IN      A      {{ home_hosts.home_hosts[name]['ip_addr'] }}
{% endif %}
{% endfor %}
</code></pre><p>While this is mostly okay, we could clean up the conditional check on CNAMEs and A records. Also having every variable prefixed with <code>home_hosts</code> is redundant.</p>
<h4 id="db168192in-addrarpaj2"><code>db.168.192.in-addr.arpa.j2</code></h4>
<pre><code class="language-jinja2" data-lang="jinja2">;
; BIND reverse data file for the Noise Palace
;
$TTL    604800
@       IN      SOA     noisepalace.home. root.noisepalace.home. (
                              1         ; Serial
                         604800         ; Refresh
                          86400         ; Retry
                        2419200         ; Expire
                         604800 )       ; Negative Cache TTL
;
@		IN      NS      noisepalace.home.
{% for name in home_hosts.home_hosts %}
{% if home_hosts.home_hosts[name]['ip_addr'] is defined %}
{% set octet = home_hosts.home_hosts[name]['ip_addr'].split('.') %}
{{ octet[3] }}.{{ octet[2] }}   		IN      PTR     {{ name | lower }}.noisepalace.home.
{% endif %}
{% endfor %}
</code></pre><p>Other than the <code>home_hosts</code> prefix on this, this is okay. It does hard-code the domain name, but I only use one domain in my home infrastructure anyway, so I&rsquo;m okay with this trade-off for now.</p>
<h4 id="other-templates">Other templates</h4>
<p>The other templates are standard BIND9 configuration defining my zones, what I forward to for recursive DNS and other standard features. The only interesting part I have is adding my subnets in as &ldquo;goodclients&rdquo; (i.e. those that are allowed to query DNS from BIND9): -</p>
<pre><code class="language-jinja2" data-lang="jinja2">acl goodclients {
  {% for network in netvars['ip_ranges'] %}{{ network }};
  {% endfor -%}
  localhost;
};
</code></pre><h3 id="new-role-outline">New Role: Outline</h3>
<p>As noted, the previous approach had a few limitations that I wanted to address. The below is a quick summary of what I want to fix in the tasks: -</p>
<ul>
<li>Statically defined hosts</li>
<li>Repetition of tasks</li>
<li>Only reload/restart <code>bind9</code> if a change has been made</li>
<li>Consolidate the variables into a single file, available globally across all roles and hosts</li>
<li>Remove redundant prefixing/namespacing of variables</li>
</ul>
<p>For the templates, I wanted them to be simpler with less conditional logic.</p>
<p>Finally, I wanted to remove the primary/secondary roles from the servers, and just have each server answer with what it has.</p>
<p>I have found a few issues with records not replicating correctly between my two instances of <code>bind9</code>. Given both are managed with Ansible anyway, I don&rsquo;t need to have the primary inform the secondary of what records exist.</p>
<h3 id="new-role-initialisation">New Role: Initialisation</h3>
<p>To initialise the role, I ran <code>ansible-galaxy init dns</code> in the <code>roles</code> directory of my Ansible repository. I also added it to my overall Playbook like so: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">hosts</span>: ns<span style="color:#ae81ff">-04.</span>noisepalace.home,ns<span style="color:#ae81ff">-03.</span>noisepalace.home
  <span style="color:#66d9ef">become</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">roles</span>:
    - <span style="color:#66d9ef">role</span>: dns
      <span style="color:#66d9ef">tags</span>:
        - dns
        - bind9
</code></pre></div><p>In the above, I have applied the role to my <code>ns-04</code> and <code>ns-03</code> hosts (two virtual machines on my network).</p>
<p>This is because the role is solving the <strong>Statically defined hosts</strong> issue. If I wanted to add additional servers, I can add them to the hosts list in the overall Playbook, rather than updating the role itself.</p>
<p>As an aside, why do I have <code>ns-04</code> and <code>ns-03</code> rather than <code>ns-01</code> and <code>ns-02</code>? The simple reason is that when I moved from running everything on two Raspberry Pis and onto my two home servers, I didn&rsquo;t want to end up breaking DNS in my house.</p>
<p>I had records for <code>ns-01</code> and <code>ns-02</code> pointing to the Raspberry Pis, so I could add the new servers, move all other machines/DHCP pools/anything else reliant on DNS to use the new machines, and then decommission the Raspberry Pis. If I had simply tried to replace them and it turned out I&rsquo;d made a configuration error, I would have broken DNS (including access to the internet to help me fix it!).</p>
<h3 id="new-role-tasks">New Role: Tasks</h3>
<p>The tasks Playbook in <code>roles/dns/tasks/main.yml</code> looks like the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e"># tasks file for dns</span>
- <span style="color:#66d9ef">name</span>: Generate reverse lookup file
  <span style="color:#66d9ef">template</span>:
    <span style="color:#66d9ef">src</span>: db<span style="color:#ae81ff">.168.192</span>.in-addr.arpa.j2
    <span style="color:#66d9ef">dest</span>: /etc/bind/db<span style="color:#ae81ff">.168.192</span>.in-addr.arpa
    <span style="color:#66d9ef">owner</span>: root
    <span style="color:#66d9ef">group</span>: bind
    <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0644</span>
  <span style="color:#66d9ef">notify</span>:
    - Reload bind9
  <span style="color:#66d9ef">tags</span>:
  - configuration

- <span style="color:#66d9ef">name</span>: Generate forward lookup file
  <span style="color:#66d9ef">template</span>:
    <span style="color:#66d9ef">src</span>: db.noisepalace.home.j2
    <span style="color:#66d9ef">dest</span>: /etc/bind/db.noisepalace.home
    <span style="color:#66d9ef">owner</span>: root
    <span style="color:#66d9ef">group</span>: bind
    <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0644</span>
  <span style="color:#66d9ef">notify</span>:
    - Reload bind9
  <span style="color:#66d9ef">tags</span>:
  - configuration

- <span style="color:#66d9ef">name</span>: Named Local
  <span style="color:#66d9ef">template</span>:
    <span style="color:#66d9ef">src</span>: named.conf.local.j2
    <span style="color:#66d9ef">dest</span>: /etc/bind/named.conf.local
    <span style="color:#66d9ef">owner</span>: root
    <span style="color:#66d9ef">group</span>: bind
    <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0644</span>
  <span style="color:#66d9ef">notify</span>:
    - Reload bind9
  <span style="color:#66d9ef">tags</span>:
  - configuration

- <span style="color:#66d9ef">name</span>: Named Options
  <span style="color:#66d9ef">template</span>:
    <span style="color:#66d9ef">src</span>: named.conf.options.j2
    <span style="color:#66d9ef">dest</span>: /etc/bind/named.conf.options
    <span style="color:#66d9ef">owner</span>: root
    <span style="color:#66d9ef">group</span>: bind
    <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0644</span>
  <span style="color:#66d9ef">notify</span>:
    - Reload bind9
  <span style="color:#66d9ef">tags</span>:
  - configuration

- <span style="color:#66d9ef">name</span>: Stats directory
  <span style="color:#66d9ef">file</span>:
    <span style="color:#66d9ef">path</span>: /var/bind
    <span style="color:#66d9ef">owner</span>: bind
    <span style="color:#66d9ef">group</span>: bind
    <span style="color:#66d9ef">mode</span>: <span style="color:#e6db74">&#39;0755&#39;</span>
    <span style="color:#66d9ef">state</span>: directory
  <span style="color:#66d9ef">tags</span>:
  - configuration
</code></pre></div><p>We have fewer tasks compared to the original Playbook This is partly due to reducing repetition and partly due to moving away from primary/secondary replication.</p>
<p>We also use something called <code>notify</code>. The purpose of <code>notify</code> is to trigger a <a href="https://docs.ansible.com/ansible/latest/user_guide/playbooks_handlers.html">Handler</a>. When using templates and files, if a change has occurred, the handler is triggered. If no changes were required to the files on the servers, the handler is not triggered.</p>
<p>One interesting advantage of using handlers is that if multiple tasks refer to the same <strong>Handler</strong>, Ansible will wait until the last time it is called before triggering. This means that we don&rsquo;t restart the service multiple times during the Playbook.</p>
<p>The handler is defined in <code>roles/dns/handlers/main.yml</code> as such: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e"># handlers file for dns</span>
- <span style="color:#66d9ef">name</span>: Reload bind9
  <span style="color:#66d9ef">systemd</span>:
    <span style="color:#66d9ef">name</span>: bind9
    <span style="color:#66d9ef">state</span>: reloaded
    <span style="color:#66d9ef">enabled</span>: yes
  <span style="color:#66d9ef">tags</span>:
  - configuration
</code></pre></div><p>This achieves the aims of reducing repetition of tasks, and only restarting/reloading <code>bind9</code> when needed.</p>
<h3 id="new-role-templates">New Role: Templates</h3>
<p>In terms of templates, I was able to remove a few (due to getting rid of Primary/Secondary roles). For the ones left, I made a couple of changes to make them easier to work with, and to provide more comments/help when reading the output files at a later date.</p>
<h4 id="rolesdnstemplatesdbnoisepalacehomej2"><code>roles/dns/templates/db.noisepalace.home.j2</code></h4>
<pre><code class="language-jinja2" data-lang="jinja2">;
; BIND data file for local loopback interface
;
$TTL    604800
@       IN      SOA     noisepalace.home. root.noisepalace.home. (
                              2         ; Serial
                         604800         ; Refresh
                          86400         ; Retry
                        2419200         ; Expire
                         604800 )       ; Negative Cache TTL
;
@               	IN      NS      ns4.noisepalace.home.
@               	IN      A       192.168.10.1
ns3             	IN      A       192.168.10.2
ns4             	IN      A       192.168.10.1
{% for network in networks %}
; Subnet: {{ network['subnet'] }}
; Purpose: {{ network['purpose'] }}
{% for host in network['hosts'] %}
{{ host['name']|lower }} IN      A      {{ host['ip_addr'] }} ; {{ host['description'] }}
{% endfor %}
{% endfor %}
{% for domain in cnames %}
{% for record in domain['records'] %}
{{ record['name']|lower }} IN      CNAME  {{ record['cname'] }}
{% endfor %}
{% endfor %} 
</code></pre><p>The redundant <code>home_hosts</code> prefix when looping through variables has been removed. The conditional check on whether this is a CNAME or an A record has also been removed (as CNAMEs are now in their own section of the variables). We also add some comments in to make the resulting file easier to navigate.</p>
<p>I will go through what has changed in the variables file soon so that you can see where the information is sourced from.</p>
<h4 id="rolesdnstemplatesdb168192in-addrarpaj2"><code>roles/dns/templates/db.168.192.in-addr.arpa.j2</code></h4>
<pre><code class="language-jinja2" data-lang="jinja2">;
; BIND reverse data file for the Noise Palace
;
$TTL    604800
@       IN      SOA     noisepalace.home. root.noisepalace.home. (
                              1         ; Serial
                         604800         ; Refresh
                          86400         ; Retry
                        2419200         ; Expire
                         604800 )       ; Negative Cache TTL
;
@		IN      NS      noisepalace.home.
{% for network in networks %}
; Subnet: {{ network['subnet'] }}
; Purpose: {{ network['purpose'] }}
{% for host in network['hosts'] %}
{% set octet = host['ip_addr'].split('.') %}
{{ octet[3] }}.{{ octet[2] }}   		IN      PTR     {{ host['name']|lower }}.noisepalace.home.
{% endfor %}
{% endfor %}
</code></pre><p>This doesn&rsquo;t differ much from the original template, except for some additional comments for each subnet. Still, we have removed the redundant <code>home_hosts</code> prefix on all the variables, so that is a win!</p>
<h3 id="new-role-variables">New Role: Variables</h3>
<p>This section isn&rsquo;t specific to this role. However it will help to show the difference between using local/directory-specific variables and using global variables usable by all hosts and roles.</p>
<p>Previously the variables for this role were found in other directories. We had to have specific tasks to include them in the Playbook (and any others that required it). The same would be true for any other application/Playbook I had created, as there was no global definition of variables.</p>
<p>Now in the overall Playbook, we have the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">hosts</span>: all,localhost
  <span style="color:#66d9ef">become</span>: <span style="color:#66d9ef">false</span>
  <span style="color:#66d9ef">pre_tasks</span>:
  - <span style="color:#66d9ef">name</span>: Include Global vars
    <span style="color:#66d9ef">include_vars</span>: vars.yml
    <span style="color:#66d9ef">tags</span>:
      - vars
</code></pre></div><p>This includes a global variables file that is available to all hosts and for all roles. Any changes in the global variables file are then available to all roles that require them.</p>
<p>I also changed the format of the variables to look like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">[...]
<span style="color:#66d9ef">networks</span>:
  - <span style="color:#66d9ef">subnet</span>: <span style="color:#ae81ff">192.168.0.0</span>/<span style="color:#ae81ff">24</span>
    <span style="color:#66d9ef">purpose</span>: Management
    <span style="color:#66d9ef">domain</span>: noisepalace.home
    <span style="color:#66d9ef">hosts</span>:
    - <span style="color:#66d9ef">name</span>: router
      <span style="color:#66d9ef">mac</span>: c4:ad:<span style="color:#ae81ff">34</span>:<span style="color:#ae81ff">55</span>:cb:<span style="color:#ae81ff">03</span>
      <span style="color:#66d9ef">ip_addr</span>: <span style="color:#ae81ff">192.168.0.1</span>
      <span style="color:#66d9ef">description</span>: <span style="color:#e6db74">&#34;Core Router - MikroTik 4011 Core&#34;</span>
      <span style="color:#66d9ef">monitor</span>:
        <span style="color:#66d9ef">icmp</span>: yes
        <span style="color:#66d9ef">mikrotik_snmp</span>: <span style="color:#66d9ef">true</span>
    - <span style="color:#66d9ef">name</span>: archspire
      <span style="color:#66d9ef">mac</span>: f4:4d:<span style="color:#ae81ff">30</span>:<span style="color:#ae81ff">64</span>:4d:bf
      <span style="color:#66d9ef">ip_addr</span>: <span style="color:#ae81ff">192.168.0.5</span>
      <span style="color:#66d9ef">description</span>: <span style="color:#e6db74">&#34;Archspire - Intel NUC VMH&#34;</span>
    - <span style="color:#66d9ef">name</span>: pinkfloyd
      <span style="color:#66d9ef">mac</span>: a8:<span style="color:#ae81ff">20</span>:<span style="color:#ae81ff">66</span>:<span style="color:#ae81ff">34</span>:f0:2b
      <span style="color:#66d9ef">ip_addr</span>: <span style="color:#ae81ff">192.168.0.6</span>
      <span style="color:#66d9ef">description</span>: <span style="color:#e6db74">&#34;PinkFloyd - Mac Mini VMH&#34;</span>
[...]
<span style="color:#66d9ef">cnames</span>:
  - <span style="color:#66d9ef">domain</span>: noisepalace.home
    <span style="color:#66d9ef">records</span>:
    - <span style="color:#66d9ef">name</span>: behemoth
      <span style="color:#66d9ef">cname</span>: behemoth-10g
    - <span style="color:#66d9ef">name</span>: pihole
      <span style="color:#66d9ef">cname</span>: netutil<span style="color:#ae81ff">-01</span>
    - <span style="color:#66d9ef">name</span>: grafana
      <span style="color:#66d9ef">cname</span>: pinkfloyd
    - <span style="color:#66d9ef">name</span>: prometheus
      <span style="color:#66d9ef">cname</span>: pinkfloyd
[...]
</code></pre></div><p>Before everything was a dictionary (i.e. keys and values). Now I have moved to using lists for the variables I want to iterate over. I find lists slightly easier to use in Jinja2 templates, and I also find it helps at first glance to see where one host starts and one host ends.</p>
<p>You&rsquo;ll also notice that each subnet has a <strong>purpose</strong> (i.e. the kind of hosts that exist in it) and also each host has a <strong>description</strong>.</p>
<p>We saw in the templates that each DNS record has a description next to it (as the hostname alone may not always show enough information). Also all the records in a subnet will be prefixed by a comment that has the CIDR range (e.g. <code>192.168.0.0/24</code>, <code>192.168.10.0/24</code>) as well as the purpose of the subnet.</p>
<h3 id="new-role-summary">New Role: Summary</h3>
<p>At this point, the role can now be used against multiple hosts (i.e. not hardcoded to two hosts), we&rsquo;ve reduced repetition, made the variables available to all roles and we only perform a reload/restart on <code>bind9</code> when required. The templates have been simplified (while also giving more information in the output files) and the primary/secondary DNS roles are gone.</p>
<p>I went through a similar exercise against other Playbooks, combining all variables that would be useful to other hosts into the global variables files, reducing repetition, and using handlers in all places I could.</p>
<h2 id="what-other-roles-do-i-use">What other roles do I use?</h2>
<p>As well as the aforementioned DNS role, I have a number of other roles defined in my overall Playbook: -</p>
<h3 id="base"><code>base</code></h3>
<p>This is the base role for anything I might want against all hosts.</p>
<p>Currently, this just installs <code>gnupg2</code> on any Debian machines. Without it, adding Debian repositories fails because of a lack of GPG-key management software.</p>
<h3 id="sshkeys"><code>sshkeys</code></h3>
<p>This takes my public SSH keys from my workstations/laptops/desktops/terminal apps on phones/tablets and adds them to the <code>authorized_keys</code> files for my primary user on each provisioned machine.</p>
<h3 id="authkeys"><code>authkeys</code></h3>
<p>This takes the same keys as above, but adds them to my Gitea, Gitlab and GitHub user so that I can clone my repositories using SSH.</p>
<p>Rather than using a native Ansible module for this,  it uses a small Go tool I built to interact with the Gitea/Gitlab/GitHub APIs. I created the tool in part help learning Go, and it also makes adding other Git/VCS providers quite straightforward in future.</p>
<h3 id="saltstack"><code>saltstack</code></h3>
<p>This role adds the Saltstack repositories to any Debian or RHEL-based machine. These are the only official repositories available for distributions I use (i.e. Debian and Ubuntu primarily, with Alma/Rocky for testing sometimes).</p>
<p>It will then install the <code>salt-minion</code> package (from the official repository if available, from the distribution packages if not), update the configuration file to point to my Salt Master server, add a few useful <a href="https://docs.saltproject.io/en/latest/topics/grains/index.html">grains</a> and then restart the <code>salt-minion</code> daemon (to force it to register with the Salt Master).</p>
<h3 id="blackbox_checks"><code>blackbox_checks</code></h3>
<p>For any hosts not managed by Salt (i.e. routers, switches, access points, IoT devices), this role will add them to my Prometheus checks so that I can monitor and alert on them. This uses the Prometheus <a href="/posts/prometheus-consul-blackbox/">blackbox_exporter</a> that I have detailed in other posts.</p>
<h3 id="cockpit"><code>cockpit</code></h3>
<p>This install <a href="https://cockpit-project.org/">Cockpit</a> on my hypervisors. This is only used for Virtual Machine consoles when I am not on a machine with <strong>virt-manager</strong> available, but it is useful to have nonetheless.</p>
<h3 id="dhcp"><code>dhcp</code></h3>
<p>This installs <code>isc-dhcp-server</code>, configures the pools for each of my subnets (i.e. one per VLAN in my network), any static IP reservations, and then reloads <code>isc-dhcp-server</code> when required.</p>
<h3 id="drone"><code>drone</code></h3>
<p>This installs the <code>drone-exec-runner</code> on two of my machines (one <code>arm64</code> based, one that is running the Salt Master). I will go into much more detail on this in a later post.</p>
<h3 id="cloudflaredns"><code>cloudflaredns</code></h3>
<p>For a number of DNS records, I add them to Cloudflare (who I use for DNS). The reason for this is so that I can use one of my purchased domains with Lets Encrypt to get valid certificates for the hosts on my network.</p>
<p>Quite a few applications now either warn when HTTPS isn&rsquo;t used or uses a self-signed certificate. Some browsers now make you type <code>thisisunsafe</code> to accept a self-signed certificate. Other applications won&rsquo;t even work without HTTPS.</p>
<p>I have been running HTTP only on any application in my network for years now, but I decided it was time to start using HTTPS properly.</p>
<p>As Cloudflare is my public DNS provider, I will toggle what hosts I want in my variables file using <code>cf_add: true</code>, and then they will be added to the zone for my purchased domain.</p>
<h3 id="ipam"><code>ipam</code></h3>
<p>I run an instance of Netbox on my network, but I have been updating it manually. This role updates IP entries automatically, meaning I can see how much space I have left in each subnet, as well as just a general overview of what I have running.</p>
<p>All of the above roles make use of the same global variables files. This allows me to feed the DNS, DHCP, IPAM and Cloudflare roles with the same information.</p>
<p>The final overall Playbook that runs all of these roles looks like the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">hosts</span>: all,localhost
  <span style="color:#66d9ef">become</span>: <span style="color:#66d9ef">false</span>
  <span style="color:#66d9ef">strategy</span>: mitogen_free
  <span style="color:#66d9ef">pre_tasks</span>:
  - <span style="color:#66d9ef">name</span>: Include Global vars
    <span style="color:#66d9ef">include_vars</span>: vars.yml
    <span style="color:#66d9ef">tags</span>:
      - vars

- <span style="color:#66d9ef">hosts</span>: config<span style="color:#ae81ff">-01.</span>noisepalace.home
  <span style="color:#66d9ef">strategy</span>: mitogen_free
  <span style="color:#66d9ef">roles</span>:
    - <span style="color:#66d9ef">role</span>: sshkeys
      <span style="color:#66d9ef">tags</span>:
        - sshkeys
        - authkeys

- <span style="color:#66d9ef">hosts</span>: all
  <span style="color:#66d9ef">strategy</span>: mitogen_free
  <span style="color:#66d9ef">become</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">roles</span>:
    - <span style="color:#66d9ef">role</span>: base
      <span style="color:#66d9ef">tags</span>:
        - base
    - <span style="color:#66d9ef">role</span>: authkeys
      <span style="color:#66d9ef">tags</span>:
        - authkeys
    - <span style="color:#66d9ef">role</span>: saltstack
      <span style="color:#66d9ef">tags</span>:
        - saltstack

- <span style="color:#66d9ef">hosts</span>: archspire.noisepalace.home
  <span style="color:#66d9ef">strategy</span>: mitogen_free
  <span style="color:#66d9ef">become</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">roles</span>:
    - <span style="color:#66d9ef">role</span>: blackbox_checks
      <span style="color:#66d9ef">tags</span>:
        - blackbox
        - prometheus

- <span style="color:#66d9ef">hosts</span>: vmh
  <span style="color:#66d9ef">strategy</span>: mitogen_free
  <span style="color:#66d9ef">become</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">roles</span>:
    - <span style="color:#66d9ef">role</span>: cockpit
      <span style="color:#66d9ef">tags</span>:
        - cockpit

- <span style="color:#66d9ef">hosts</span>: ns<span style="color:#ae81ff">-04.</span>noisepalace.home,ns<span style="color:#ae81ff">-03.</span>noisepalace.home
  <span style="color:#66d9ef">strategy</span>: mitogen_free
  <span style="color:#66d9ef">become</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">roles</span>:
    - <span style="color:#66d9ef">role</span>: dns
      <span style="color:#66d9ef">tags</span>:
        - dns
        - bind9
    - <span style="color:#66d9ef">role</span>: dhcp
      <span style="color:#66d9ef">tags</span>:
        - dhcp

- <span style="color:#66d9ef">hosts</span>: piutil<span style="color:#ae81ff">-01.</span>noisepalace.home,config<span style="color:#ae81ff">-01.</span>noisepalace.home
  <span style="color:#66d9ef">strategy</span>: mitogen_free
  <span style="color:#66d9ef">become</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">roles</span>:
    - <span style="color:#66d9ef">role</span>: drone
      <span style="color:#66d9ef">tags</span>:
        - drone

- <span style="color:#66d9ef">connection</span>: local
  <span style="color:#66d9ef">strategy</span>: mitogen_free
  <span style="color:#66d9ef">hosts</span>: localhost
  <span style="color:#66d9ef">become</span>: <span style="color:#66d9ef">false</span>
  <span style="color:#66d9ef">roles</span>:
    - <span style="color:#66d9ef">role</span>: cloudflaredns
      <span style="color:#66d9ef">tags</span>:
        - cloudflaredns
        - dns
    - <span style="color:#66d9ef">role</span>: ipam
      <span style="color:#66d9ef">tags</span>:
        - ipam
</code></pre></div><h2 id="mitogen">Mitogen?</h2>
<p>One of the benefits of moving to Roles is making a change to a single variables file, and all the roles that use the added/changed variable (e.g. adding a new host/IP address) will roll out at the same time (so long as you don&rsquo;t use <code>ansible-playbook --tag $TAG</code> or <code>ansible-playbook --skip-tags $TAG</code>).</p>
<p>The new Playbook and roles now runs against all of the hosts in my home and personal infrastructure. A change to add a new host will still run all the other roles too, even though not all may be relevant to it.</p>
<p>This means that a quick addition/fix takes much longer than it used to. There are a couple of tweaks you can do to speed up Ansible compared to the default behaviour.</p>
<h3 id="general-ansible-performance-improvements">General Ansible performance improvements</h3>
<p>The first tweak is changing the &ldquo;<a href="https://docs.ansible.com/ansible/latest/user_guide/playbooks_strategies.html">strategy</a>&rdquo;. The strategy defines the way Ansible runs tasks.</p>
<p>By default, Ansible uses the <strong>linear</strong> strategy. For each task in a playbook, Ansible waits for a response from each server that the task runs against before moving onto the next task.</p>
<p>Changing this to <strong>free</strong> means that each host will try to reach the end of the Playbook in the quickest time it can without waiting for other hosts.</p>
<p>Changing your default strategy to <strong>free</strong> can help with getting some hosts to complete as quickly as they can. This can help with overall Playbook execution time, and means that not all hosts are penalised by the slowest one.</p>
<p>The second tweak is to increase the amount of <a href="https://docs.ansible.com/ansible/latest/user_guide/playbooks_strategies.html#setting-the-number-of-forks">forks</a> the Ansible process will make.</p>
<p>A fork is a separate process of Ansible that will run concurrently with others. If you have multiple cores on your machine(s) available, this will speed up time to process the playbooks by allowing Ansible to execute tasks concurrently. The more threads available, the less the instructions contend with each other for CPU time.</p>
<h3 id="is-it-enough">Is it enough?</h3>
<p>Even with the above tweaks, the execution time for a Playbook with a few roles can still be significant.</p>
<p>Previously the single Playbooks I used would take around 1-3 minutes to execute. This depended on the amount of tasks, amount of files to manage and any loops. I may have been running multiple Playbooks, but if I only wanted to update DNS, it would only take or a minute or two.</p>
<p>With the move to using a number of roles against multiple hosts, the overall execution time reached around 15-20 minutes. If I had to run every single one of my old Playbooks, this may have taken a similar amount of time. However other than adding a brand new host, it was rare that I needed to run lots of Playbooks at once.</p>
<p>At this point, I was considering separating out the single Playbook with multiple roles into a few separate playbooks with some of the roles. I could then add in conditional checks (or even run separate repositories) so that each role didn&rsquo;t need to run every time.</p>
<p>However this goes against the idea of moving to this approach, in that a single variable change may affect multiple Roles, so having some Playbooks run and not others could leave drift in configuration. Also, if I did need to run every role (i.e. new host being added), it would still take the full execution time anyway.</p>
<p>After a little research on improving Ansible performance and execution time, I came across <a href="https://mitogen.networkgenomics.com/ansible_detailed.html">Mitogen</a>.</p>
<h3 id="what-is-mitogen">What is Mitogen?</h3>
<p>Mitogen is a Python library that is used to write distributed programs, and to provide native Python APIs where shell commands/interaction is usually required. While Ansible does primarily use Python, it also does call out to the shell from Python for quite a few different operations.</p>
<p>Mitogen also provide a way to use the library with Ansible directly. It replaces (where possible) calling out to the shell with pure Python equivalents, re-uses SSH connections and more. Mitogen claims that this will speed up Playbook execution anywhere from 1.5 to 7 times, as well as reducing CPU consumption considerably. They specifically mention that any task/action that uses loops or <code>with_*</code> directives should see noticeable improvements.</p>
<p>To use Mitogen with Ansible, there are a couple of options. If you plan to run Ansible from a standard host (i.e. a bastion host, your machine), then either clone the repository from their <a href="https://github.com/mitogen-hq/mitogen">GitHub</a> (or download a release tarball from their releases page) and refer to the location of the repository/extracted files in your Ansible configuration.</p>
<p>If you intend to run Ansible from a container/image, then you need to ensure that either the image/container has Mitogen installed/included, or that it is included as part of your Ansible repository.</p>
<p>To include it in your repository, you can either add it as a Git <a href="https://git-scm.com/book/en/v2/Git-Tools-Submodules">submodule</a>, or extract the contents of the release tarball into a directory in your repository directly.</p>
<p>Once you have done this, create/update <code>ansible.cfg</code> in your repository with the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml">[<span style="color:#a6e22e">defaults</span>]
<span style="color:#a6e22e">host_key_checking</span> = <span style="color:#a6e22e">False</span>
<span style="color:#a6e22e">strategy_plugins</span> = <span style="color:#a6e22e">mitogen</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#a6e22e">ansible_mitogen</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#a6e22e">plugins</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#a6e22e">strategy</span>
</code></pre></div><p>I have disabled <code>host_key_checking</code> because if Ansible runs in an ephemeral container, it hasn&rsquo;t SSH&rsquo;d to any of the hosts previously. Without this, running Ansible will fail from a container, as the process for accepting a host key would require user input.</p>
<p>In addition to this, I add the <code>strategy: mitogen_free</code> to the tasks in the overall Playbook. You can set this as a default option in the <code>ansible.cfg</code> file.</p>
<p>I have found that some roles/collections may have issues with Mitogen (the <a href="https://docs.ansible.com/ansible/latest/collections/netbox/netbox/index.html">netbox</a> collection didn&rsquo;t work on a release candidate of Mitogen, but does work on the latest GA release). Having the option to enable it per task/role inclusion allows me to run it against all roles it is compatible with, and revert to the in-built Ansible strategies if a role/collection has issues.</p>
<h3 id="did-it-make-a-difference">Did it make a difference?</h3>
<p>All of this would be moot if there were no improvements in execution time. So what difference has it made?</p>
<p>I am skipping ahead in the series slightly in showing the results from my Drone CI jobs. They are a good benchmark for the time to execute all the roles, and all of them run all roles defined in the overall Playbook.</p>
<h4 id="only-using-forks">Only using forks</h4>
<p>I set the amount of forks set to 15 ever since I started moving to roles, so I don&rsquo;t have any benchmarks/execution times without it!</p>
<p><img src="/img/homeinfra/drone-ansible-executiontime-notweaks.png" alt="Ansible Execution Time in Drone with forks"></p>
<p>The times at the end (<strong>15:51</strong> and <strong>16:11</strong>) are how long the jobs (in minutes and seconds) have taken to execute. The Pull Request (using <code>--check</code> and <code>--diff</code> mode only, no actual changes applied) takes just over 16 minutes. When the PR is merged, it takes just under 16 minutes.</p>
<p>This is quite a long time to wait for a single DNS change or adding a host!</p>
<h4 id="using-the-standard-free-strategy">Using the standard <code>free</code> strategy</h4>
<p>My first attempt to reduce the execution time was to change the strategy to <strong>free</strong>: -</p>
<p><img src="/img/homeinfra/drone-ansible-executiontime-free.png" alt="Ansible Execution Time in Drone with free strategy"></p>
<p>That is quite an improvement, saving 4 and 5 minutes of execution time.</p>
<p>Allowing hosts to get to the end of a playbook quicker saves on execution time overall. This makes sense, as some hosts may be quicker at some tasks and slower at others. Allowing them to not be penalised by other hosts (or to not slow down other hosts) cumulatively saves time overall.</p>
<h4 id="using-mitogen">Using Mitogen</h4>
<p>We are already using the <code>free</code> strategy, so what difference do we see when moving to <code>mitogen_free</code>?</p>
<p><img src="/img/homeinfra/drone-ansible-executiontime-mitogenfree.png" alt="Ansible Execution Time in Drone with mitogen_free strategy"></p>
<p>Wow, that is a huge difference! I can now do a full Pull Request AND Merge executions in less time than it had taken to run one or the other before.</p>
<p>5 minutes of execution time is still more than the 1-3 minutes it used to take to apply my single Playbooks previously, but bear in mind that this is now applying 11 roles against all hosts. For me, this is an acceptable trade-off for knowing that every host has all roles and the latest changes applied.</p>
<h2 id="other-changes">Other changes</h2>
<p>If you look over my list of roles I now use with Ansible, you may notice something missing.</p>
<h3 id="didnt-you-say-you-used-ansible-to-configure-prometheus">Didn&rsquo;t you say you used Ansible to configure Prometheus?</h3>
<p>Yes, earlier in this post I mentioned that before this refactor, Ansible would: -</p>
<blockquote>
<p>Update Prometheus configuration<br>
Install Prometheus exporters on new machines</p>
</blockquote>
<p>Why wasn&rsquo;t it included in the &ldquo;<a href="#what-other-roles-do-i-use">What other roles do I use?</a>&rdquo; section?</p>
<p>As part of this refactor, I thought about why I run both Ansible and Saltstack in my home infrastructure. Both achieve very similar goals.</p>
<p>One of the main differences between the two is that Ansible isn&rsquo;t agent-based. It doesn&rsquo;t need a machine/node to be bootstrapped/have additional packages installed to manage it, which is very useful when configuring a brand new host.</p>
<p>However Salt is incredibly quick in my experience. The section on performance tweaks and using Mitogen with Ansible is something you don&rsquo;t necessarily need to think about too much with Salt, as it has a lot of performance advantages to begin with.</p>
<p>Salt uses a queuing mechanism (based upon <a href="https://zeromq.org/">ZeroMQ</a>). The Salt Master doesn&rsquo;t tell each node what to do directly. Instead it adds messages to the queue and then the Agents will execute the actions describe by the messages (more information on this <a href="https://docs.saltproject.io/en/latest/topics/development/topology.html">here</a>).</p>
<p>The Salt agents then execute all the commands and operations to configure the server they run on. By contrast, Ansible has to tell each host what command/execution/task are required, and controls the order/strategy to execute them.</p>
<p>Because of this, I decided to use Ansible for what it is (in my opinion) best at, bootstrapping nodes (i.e. installing allowed SSH keys, configuring DHCP/DNS, installing the Salt agent) and managing nodes that can&rsquo;t run agents.</p>
<p>Ansible knows about every host on my network (in that there are variables that have the IP address/MAC address etc) for anything that needs a static allocation and needs basic monitoring (i.e. those that cannot run agents themselves), so it makes sense to add them into monitoring/DNS/DHCP with Ansible during the bootstrap/playbook run stage.</p>
<p>For hosts that can run Agents (i.e. all my home servers/virtual machines/VPSs etc), everything other than the initial bootstrap is managed by Salt. They get a default set of Prometheus exporters, ICMP monitoring, base packages and more. I can then include other applications for the machine to run, as well as the associated Prometheus exporter(s) that will be monitor the deployed applications (e.g. Wireguard and the <code>wireguard_exporter</code>, Docker and <code>cadvisor</code>).</p>
<p>To show the difference in execution time between the two, here is an example of Salt running against all hosts, applying all relevant states: -</p>
<p><img src="/img/homeinfra/drone-salt-executiontime.png" alt="Salt Execution Time"></p>
<p>My Salt environment has 47 different states/applications it can deploy. Even with all the tweaks I have done to my Ansible setup, Salt achieves a lot more in a lot less time!</p>
<p>This isn&rsquo;t to say Ansible is bad, as it isn&rsquo;t. I love and use Ansible in a lot of different ways, and could easily use only Ansible to manage everything on my infrastructure.</p>
<p>I like the option of having both Ansible and Salt. I can play to their strengths, rather than making compromises to use only one or the other.</p>
<h2 id="summary">Summary</h2>
<p>Hopefully this post helps you in either understanding Ansible roles, how to transition from single Playbooks to Ansible Roles (and why you would want to) and also helps with reducing the execution time of your Playbooks too.</p>
<p>The move from single Playbooks to Roles was probably the biggest part of the refactor, but I am now able to rely solely on my continuous integration to test and implement my Playbooks. Being able to trigger a full update of my infrastructure by making updates in the Gitea mobile app (<a href="https://gitnex.com/">GitNex</a>) is a wonderful thing 😁.</p>
<h2 id="next-post">Next post</h2>
<p>In the next post, I will cover the changes that have improved my Salt environment. This isn&rsquo;t as big of an overhaul as Ansible was, as I already had recent professional experience of Salt when I started using it at home.</p>
<p>However there are a few interesting changes I made that require less manual updates to the Salt states and pillars, or any in the case of a host that I am just testing/not running extra applications on.</p>
]]></content>
        </item>
        
        <item>
            <title>Home and Personal Infrastructure Overhaul: Part 1 - Introduction</title>
            <link>https://yetiops.net/posts/home-infra-overhaul-part1/</link>
            <pubDate>Sun, 09 Jan 2022 07:12:25 +0000</pubDate>
            
            <guid>https://yetiops.net/posts/home-infra-overhaul-part1/</guid>
            <description>Over the past month and a half, I have overhauled most of how my home and personal infrastructure is managed (as well as a couple of upgrades to machines I use).
Previously, most of my personal infrastructure was managed by single-purpose Ansible Playbooks and a minimal set of Salt states. When I first got back into Ansible (mid-to-late 2019) after a break of about 3-4 years, I didn&amp;rsquo;t have a great understanding of Ansible Roles.</description>
            <content type="html"><![CDATA[<p>Over the past month and a half, I have overhauled most of how my home and personal infrastructure is managed (as well as a couple of upgrades to machines I use).</p>
<p>Previously, most of my personal infrastructure was managed by single-purpose Ansible <a href="https://docs.ansible.com/ansible/latest/user_guide/playbooks.html">Playbooks</a> and a minimal set of Salt states. When I first got back into Ansible (mid-to-late 2019) after a break of about 3-4 years, I didn&rsquo;t have a great understanding of Ansible <a href="https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_roles.html">Roles</a>.</p>
<p>These Playbooks and States served their purpose for quite a while. After having managed DNS/DHCP manually (including SQL database manipulation with PowerDNS) and no monitoring at all, this was a massive step up and made managing my infrastructure much easier.</p>
<h2 id="why-change">Why change?</h2>
<p>So what changed? What problems did I still want to solve?</p>
<p>Firstly, a lot of my personal infrastructure  was still managed ad-hoc (i.e. logging in and installing/updating configuration). Everything from deploying SQL databases, managing IP ranges, VPN configuration, and more.</p>
<p>Also despite using Ansible and Salt, most of the Playbooks and States were not very idempotent. They would work fine on the first run, but a subsequent run would fail.</p>
<p>Finally, after the onboard network interface on one of my home servers (<a href="/posts/my-setup-and-workflow/##pink-floyd">Pink Floyd</a>) started to drop out frequently, it made me paranoid that if one of my home servers failed, I would struggle to return a replacement to the previous state.</p>
<p>All in all, Ansible had helped in solving my biggest pain points, but I couldn&rsquo;t rely on it if something went wrong.</p>
<h2 id="rethinking-the-approach">Rethinking the approach</h2>
<p>As happens quite often in tech, a failure usually leads to making improvements. Before it became apparent that the network interface on <strong>Pink Floyd</strong> was failing, I had assumed that I was reaching the limit of the server&rsquo;s resources. I&rsquo;d recently added a few virtual machines (mostly for development testing and playing with <a href="https://ebpf.io/">eBPF</a>), and soon after I started seeing frequent drop outs in my monitoring.</p>
<p>This prompted me to make a few changes: -</p>
<ul>
<li>Changing the <a href="https://prometheus.io/docs/prometheus/latest/configuration/configuration/##scrape_config">scrape_interval</a> for some intensive/long-running checks on my infrastructure</li>
<li>Remove a few virtual machines</li>
<li>Move some applications/machines to my other home server (<a href="/posts/my-setup-and-workflow/##archspire">Archspire</a>)</li>
<li>Use containers more where possible</li>
</ul>
<p>As part of this, I removed a self-hosted <a href="https://gitlab.com/">Gitlab</a> instance. I originally intended to use <a href="https://about.gitlab.com/stages-devops-lifecycle/continuous-integration/">Gitlab CI</a> on my infrastructure, but struggled to dedicate the time to it.</p>
<p>I moved all the repositories from the Gitlab instance to my <a href="https://gitea.com/">Gitea</a> instance and removed the Gitlab machine. I still wanted to move to using a CI solution, but using the in-built Gitlab approach was no longer an option.</p>
<h2 id="upgrades">Upgrades</h2>
<p>After a little more investigation (and discovering the network interface was at fault), I added a USB NIC to <strong>Pink Floyd</strong> and it has been running fine ever since. The USB NIC also supports 2.5Gbe, so once I buy a switch(es) that support multi-gig, I can take advantage of the extra bandwidth. For now, this just means I have a functioning interface (which is a big upgrade from an intermittently functioning interface!).</p>
<p>After moving a few applications and machines over to <strong>Archspire</strong> (and seeing the available memory/disk space dwindle), I decided it was time to give the machine a bit of a boost. I bought 2 16Gb of DDR4 SO-DIMM sticks of RAM and a 512Gb NVME, doubling both the memory and storage on this machine. This also helped solved some issues with a few applications/machines running out of memory (e.g. a Unifi Controller container, the Salt Master server), always a bonus!</p>
<p>Finally, I added two more VPSs for serving external services (i.e. this website, RSS, read-it-later, external monitoring). In addition to my existing Digital Ocean droplet, I now also run two Hetzner Hcloud instances.</p>
<h2 id="gitea-and-ci">Gitea and CI</h2>
<p>For those who haven&rsquo;t used Gitea, it is a self-hosted Git forge similar to <a href="https://github.com">GitHub</a>, <a href="https://gitlab.com">Gitlab</a> and <a href="https://bitbucket.org">BitBucket</a>. It is written in Go, and the resource usage is much lower than the self-hosted versions of GitHub, Gitlab or BitBucket. It started as a fork of <a href="https://gogs.io/">Gogs</a> but now appears to be more popular.</p>
<p>As already mentioned, I wanted to starting using continuous integration with my infrastructure. Now that I had removed my Gitlab instance, I couldn&rsquo;t use Gitlab CI. Gitea itself doesn&rsquo;t have any native continuous integration support out of the box. This is fine if all you need is Git repository hosting, but I wanted to start making use of CI because: -</p>
<ul>
<li>I wanted to lint and test my Ansible and Salt configuration before applying it</li>
<li>I now write a lot of tools in Go. I want to generate releases I can use across all of my machines (rather than building manually and SCP/rsync-ing them around), as well as lint and test them</li>
<li>I want to take the GitOps approach of updating a repository and the changes happen automatically, making Git the source of truth (and not the infrastructure)
<ul>
<li>It is much easier to update a line in a file and commit it than it is to login to a machine(s) to run a series of commands</li>
</ul>
</li>
</ul>
<p>It also needed to be self-hosted (because this is all running on a local Git instance, not a publicly available one), and ideally quite low in resource usage.</p>
<p>After some searching around (and finding the <a href="https://gitea.com/gitea/awesome-gitea">awesome-gitea</a> repository), I found <a href="https://www.drone.io/">Drone</a>.</p>
<h2 id="drone">Drone</h2>
<p>Drone is a CI/CD platform that uses <code>yaml</code> to define the steps to take when code is committed into your repository. It works in a similar way to the previously mentioned Gitlab CI and <a href="https://github.com/features/actions">GitHub Actions</a>, while not being tied to either platform. It integrates natively with Gitea, and can run jobs in different ways: -</p>
<ul>
<li>In containers (e.g. Docker)
<ul>
<li>For most jobs this makes sense (e.g. building Go tools, running Ansible Playbooks)</li>
</ul>
</li>
<li>Using the <strong>exec</strong> runner to run commands directly on a machine (for when running in a container isn&rsquo;t viable)
<ul>
<li>A good example of the use case is with Salt, as the Salt Master can tell all Minions to update</li>
</ul>
</li>
<li>Using the <strong>ssh</strong> runner to run commands on a machine available via SSH (for hosts that do not support containers and can&rsquo;t run the <strong>exec</strong> runner).
<ul>
<li>Running commands on something like OpenBSD, or a network device, where the agents do not run correctly (or at all in the case of network gear)</li>
</ul>
</li>
</ul>
<p>There is an enterprise version of Drone that supports ephemeral agents (i.e. the agents spin up when required, rather than running at all times), clustering and more, I don&rsquo;t require any of these features in my infrastructure, so the OSS version is more than enough for my needs.</p>
<p>Each repository that you want to run tasks on required a <code>.drone.yml</code> file, with a format quite similar to Gitlab CI/GitHub Actions/many other CI solutions: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">kind</span>: pipeline
<span style="color:#66d9ef">name</span>: default
<span style="color:#66d9ef">type</span>: docker

<span style="color:#66d9ef">trigger</span>:
  <span style="color:#66d9ef">branch</span>:
    - main

<span style="color:#66d9ef">steps</span>:
  - <span style="color:#66d9ef">name</span>: submodules
    <span style="color:#66d9ef">image</span>: alpine/git
    <span style="color:#66d9ef">commands</span>:
    - git submodule update --init --recursive

  - <span style="color:#66d9ef">name</span>: spellcheck
    <span style="color:#66d9ef">image</span>: tmaier/markdown-spellcheck
    <span style="color:#66d9ef">commands</span>:
      - mdspell --ignore-numbers --ignore-acronyms --report <span style="color:#e6db74">&#34;content/**/*.md&#34;</span>
    <span style="color:#66d9ef">when</span>:
      <span style="color:#66d9ef">event</span>:
       - pull_request
[...]
</code></pre></div><p>Each step will run when an event triggers it. For example, you could have a step which performs linting, but only when a pull request is raised. You could also have it deploy a version of your code to a staging/testing environment before it is merged into your main branch, and then roll out to production after it is merged.</p>
<p>Drone also has a number of <a href="https://plugins.drone.io/">plugins</a> so that you don&rsquo;t need to define every step yourself. For example, it already has plugins for Ansible and Hugo, as well as sending notifications to a number of platforms (Slack, Matrix, Telegram etc).</p>
<h2 id="configuration-management">Configuration management</h2>
<p>To ensure that all of my Ansible and Salt configuration was ready to be used with a CI platform (Drone in my case) then it needed a huge refactor. The requirements were: -</p>
<ul>
<li>Drone (or any other CI tool or automated process) can run them</li>
<li>Ansible must use roles</li>
<li>The CI job must run against all hosts and with all the necessary roles
<ul>
<li>This ensures all necessary changes go out at once, not when I remember to run them!</li>
</ul>
</li>
<li>Ansible and Salt must be idempotent (i.e. running the Playbooks/States again does not fail or make further changes)</li>
<li>Tasks/actions only run when required (i.e. do not restart a service on every run, only when the configuration/version changes)</li>
</ul>
<p>My previous posts on <a href="/tags/saltstack">Salt</a> and <a href="/tags/ansible">Ansible</a> followed these rules, I just never applied the same rules to my own infrastructure before! (<a href="https://www.youtube.com/watch?v=FQfgET6TxDY">Practice What You Preach</a>)</p>
<h2 id="containers">Containers</h2>
<p>I have been using and working with containers for about the past 6 years, and have run a few on my personal infrastructure previously. However it was all very ad-hoc, no consistency, and most of the applications I run were still native packages and/or installed inside virtual machines. Examples of those which I have ran on VPSs/Virtual Machines which could very easily be in containers are: -</p>
<ul>
<li>Hugo (for hosting this blog)</li>
<li>Read-it-later services</li>
<li>RSS</li>
<li>Netbox</li>
<li>Oxidized</li>
</ul>
<p>During this overhaul, I moved all of the above services, and added some other services too in the process.</p>
<p>Now the big question people may ask is am I running all of this on Kubernetes? The answer is not yet. I am comfortable with Kubernetes as a platform, having managed and deployed the platform and applications on Kubernetes in multiple companies now. However I don&rsquo;t feel like my infrastructure is quite ready for it yet (having two home servers goes against the idea of quorum for a start!), and it would require refactoring everything about my personal infrastructure. Given how long it has taken me to find the time to fix all the current issues with my infrastructure, a full refactor isn&rsquo;t on the cards just yet.</p>
<h2 id="more-to-come">More to come</h2>
<p>There is a lot to cover in what has changed, especially on the configuration management side. Rather than try and put this all into one huge post, this will be split out into: -</p>
<ul>
<li>Moving from basic Ansible Playbooks to using Roles</li>
<li>Improving my Salt configuration files</li>
<li>Managing dependencies and optional tasks/actions in both Ansible and Salt</li>
<li>Setting up Drone</li>
<li>Using Drone with Ansible and Salt</li>
<li>Using Drone with Go to build releases</li>
<li>Using Drone to run tasks on OpenBSD</li>
<li>Using Drone to build this website (including some useful linting/checking)</li>
<li>Managing secrets that can be used by Ansible, Salt and Drone</li>
<li>Additional services that provide benefits to my home and personal infrastructure</li>
<li>What else I want to do in future</li>
</ul>
<p>A lot of these posts are not specific to my home infrastructure, and should hopefully provide benefit to anyone using Ansible, Salt, Drone, or those who just like reading about other people&rsquo;s home labs (like me!).</p>
]]></content>
        </item>
        
        <item>
            <title>Late 2021 Update</title>
            <link>https://yetiops.net/posts/time-away/</link>
            <pubDate>Sun, 21 Nov 2021 07:23:34 +0000</pubDate>
            
            <guid>https://yetiops.net/posts/time-away/</guid>
            <description>It has been a while since I wrote a new post on this site, due to a number of reasons.
Firstly, I started a new job in May. Any new role takes time to get up to speed, this one no different. I am finding my feet there now, freeing up more mental capacity to look at new technologies and interests.
Secondly, I have struggled somewhat with ideas for new posts.</description>
            <content type="html"><![CDATA[<p>It has been a while since I wrote a new post on this site, due to a number of reasons.</p>
<p>Firstly, I started a new job in May. Any new role takes time to get up to speed, this one no different. I am finding my feet there now, freeing up more mental capacity to look at new technologies and interests.</p>
<p>Secondly, I have struggled somewhat with ideas for new posts. Part of this is that I am getting more into writing Go, and even learning some C (mainly for working with <a href="https://ebpf.io/">eBPF</a>). As I&rsquo;m still very much in the learning phase with these, I don&rsquo;t feel like I have much to say on either topic yet. Everything I&rsquo;m doing has many posts and resources on the wider internet already . I will probably put something together soon on all of this, but I still don&rsquo;t feel comfortable just yet.</p>
<p>In terms of more infrastructure-related technologies, I haven&rsquo;t really found anything new and interesting since discovering Pulumi. My experience is much greater on infrastructure-style projects and technologies, and where I feel most comfortable writing posts about. I just need something to come along and wow me again!</p>
<p>In the meantime, I am delving into other hobbies like gaming (specifically Mass Effect Legendary Edition) and building keyboards: -</p>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">It&#39;s alive!<br><br>Need to play with the layout a little as it isn&#39;t quite to my liking, but it works 😁<br><br>Wonderful PCB and kit <a href="https://twitter.com/pauljames?ref_src=twsrc%5Etfw">@pauljames</a>! <a href="https://t.co/JfCdulUOqk">pic.twitter.com/JfCdulUOqk</a></p>&mdash; Stuart Howlette (@stuh84) <a href="https://twitter.com/stuh84/status/1437084221017833482?ref_src=twsrc%5Etfw">September 12, 2021</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


<p>As far as this site goes, I&rsquo;ve made a couple of minor changes.</p>
<p>The first is that I&rsquo;ve removed comments. I have rarely found the need to read and write comments on other people&rsquo;s blogs and sites, at which point I found the idea of having them myself made little sense. Other contact methods are available to reach me, so if you want to discuss something on here, get in touch with me directly!</p>
<p>Secondly, I have removed Google Analytics. Initially when I started this site I wanted to see the interaction and growth on my posts. However as time has passed, I have realised this isn&rsquo;t a useful metric. The posts that have received the most visits are those which are either those which are too far removed from my current interests/work or for technologies I haven&rsquo;t used in a couple of years (and therefore cannot provide any follow up on).</p>
<p>There were times that I tried to cater my posts to what I thought would give higher interaction (although I was often wrong!). I didn&rsquo;t enjoy writing these posts anywhere near as much as something I just felt genuinely excited about (e.g. Pulumi, Prometheus).</p>
<p>Also I have, and have had for a long time, massive privacy concerns with Google and analytics in general. I already felt conflicted in using analytics when I first started this site, and over time I have grown increasingly uncomfortable with having them.</p>
<p>So from now on, there are no comments, no analytics, just the site and nothing else.</p>
<p>I will return soon with some new posts, hopefully a lot sooner than it has been between this post and the last one!</p>
]]></content>
        </item>
        
        <item>
            <title>Deploying Consul, Prometheus and Exporters using Puppet</title>
            <link>https://yetiops.net/posts/prometheus-consul-puppet-linux/</link>
            <pubDate>Sat, 03 Jul 2021 18:55:39 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/prometheus-consul-puppet-linux/</guid>
            <description>In previous posts on this site we have covered using both Ansible and Saltstack for configuration management. These are far from the only options out there, with Chef and Puppet also being very popular options.
With a recent job change came the chance to learn and use Puppet. For me, the best way to learn a different tool-set is to take something you have done before, and then try and do it using the new tool.</description>
            <content type="html"><![CDATA[<p>In previous posts on this site we have covered using both Ansible and Saltstack for configuration management. These are far from the only options out there, with Chef and Puppet also being very popular options.</p>
<p>With a recent job change came the chance to learn and use Puppet. For me, the best way to learn a different tool-set is to take something you have done before, and then try and do it using the new tool. In this case, we are using Puppet to configure Consul, Prometheus and the Prometheus <code>node_exporter</code> to monitor Linux machines (similar to a previous <a href="/posts/prometheus-consul-saltstack-part-1-linux/">series</a> using Saltstack).</p>
<h2 id="what-is-puppet">What is Puppet?</h2>
<p>Puppet allows you to manage a fleet of servers and other devices. The benefits of this are increasing consistency in application and infrastructure configuration, as well as reducing the toil (i.e. human interaction) of managing infrastructure.</p>
<p>Puppet&rsquo;s first release was in 2005, which predates Chef (2009), Saltstack (2011) and Ansible (2012), with only CFEngine being released beforehand. While other configuration management systems were release between CFEngine and Puppet, only CFEngine is still under active development. This means that some of the early advances in configuration management were made with Puppet, with the influence being seen in nearly all configuration management tools since.</p>
<p>Puppet&rsquo;s syntax is based upon a subset of the Ruby language. The Puppet Agent (the client that runs on servers) is written in Ruby as well. Up until recent versions of Puppet, the server side component of Puppet was also written in Ruby, but has transitioned to being written in Clojure (which itself is based upon Java).</p>
<p>Puppet has the concept of <strong>manifests</strong>, which are the equivalent of Saltstack&rsquo;s <strong>state</strong> files, and Ansible&rsquo;s <strong>playbooks</strong>. You can also run different <strong>environments</strong>, allowing you to have common code, but parameters specific to different classes of infrastructure (e.g. <strong>staging</strong>, <strong>production</strong>, different regions etc).</p>
<p>A collection of <strong>manifests</strong>, along with static files and templates, is known as a Puppet <strong>module</strong>. They are separated by directory (e.g. <code>$module_directory/my_module/manifests</code> and <code>$module_directory/my_other_module/manifests</code>), with the directory structure defining how they are referenced.</p>
<p>Variables can be defined directly inside the manifests themselves, or using <strong>Hiera</strong>.</p>
<h3 id="hiera">Hiera?</h3>
<p>Hiera is a way of separating out the configuration itself from data. For example, if you want to install a package, the action to be taken is defined in a manifest (i.e. package installation), but the version can be specified in Hiera.</p>
<p>Hiera data is written in YAML, It can be environment specific, and even node specific. You could refer to different DNS servers, different package versions (e.g. a testing/beta package for your staging environment, prior to general release), or whatever else you deem necessary.</p>
<h3 id="what-does-a-manifest-look-like">What does a manifest look like?</h3>
<p>Puppet manifests contain <strong>classes</strong>, which are unique blocks of code that define an action (or actions) to be taken. They can be referred to directly, inherited by other classes (e.g. defining a <strong>base</strong> class, and then extending them in other classes where necessary), or you can refer to them from other classes, so that multiple &ldquo;child&rdquo; classes are referenced using a &ldquo;parent&rdquo; class.</p>
<p>Using Debian (i.e. <code>apt</code>) package installation as an example, a basic manifest looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">packages</span>::<span style="color:#a6e22e">qemu</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#66d9ef">package</span>{<span style="color:#e6db74">&#39;qemu-guest-agent&#39;</span>: <span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#a6e22e">ensure</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;installed&#39;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    }<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>The equivalent Ansible code is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">name</span>: Install qemu-guest-agent
  <span style="color:#66d9ef">package</span>:
    <span style="color:#66d9ef">name</span>: qemu-guest-agent
    <span style="color:#66d9ef">state</span>: present
</code></pre></div><p>The equivalent Saltstack code would be: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e"># Shorthand</span>
<span style="color:#66d9ef">qemu-guest-agent</span>:
  pkg.installed

<span style="color:#75715e">#Longhand</span>
<span style="color:#66d9ef">install_qemu_guest_agent</span>:
  <span style="color:#66d9ef">pkg.installed</span>:
    - <span style="color:#66d9ef">pkgs</span>: 
      - qemu-guest-agent
</code></pre></div><p>The directory structure in Puppet defines how you refer to classes as well. Each class must be in one of your modules directory, and must be in a directory that is the same as your class name. For example, our class <code>packages::qemu</code> must be in the <code>$module_directory/packages/manifests</code> directory. If it was in the <code>mypackages</code> or <code>qemupackages</code> directory instead, Puppet would throw an error. Your directory structure matters more than in other configuration management systems, with Ansible and Saltstack not requiring named tasks/actions/blocks of code.</p>
<h3 id="roles-and-profiles">Roles and Profiles</h3>
<p>Roles and Profiles is a way of structuring your Puppet code that adds abstractions which lead to more generic modules (rather than environment or node-specific code). This method was referenced in <a href="http://craigdunn.org/2012/05/239/">this</a> post back in 2012, and has since become a semi-official way of defining Puppet code.</p>
<p>The idea is that rather than attaching classes (i.e. the code blocks previously mentioned) directly to a node or nodes, you create a node <strong>role</strong> and associate <strong>profiles</strong> with this node. For example, you could create a <strong>base</strong> role that includes standard packages, monitoring agents and other common configuration, but then create a separate role that is only used by the monitoring servers (in our case, <strong>Prometheus</strong>).</p>
<p>Roles can be extensions of other roles, meaning that if you choose to assign say, a <strong>prometheus</strong> role to the <strong>prometheus</strong> server, it will also inherit the <strong>base</strong> role too, which includes all common configuration.</p>
<p>A role can contain multiple <strong>profiles</strong>, which can be application specific, refer to an application and its dependencies, or profiles that align with how your business deploys applications. For example you could require that all applications must register a service with Consul, a set of firewall rules and SELinux policies, which would all be referred to in a single profile (or multiple profiles and grouped by class).</p>
<p>This means that it is perfectly possible to create a set of base roles and profiles that all nodes must use, but then additional profiles can be referenced for installing/managing the applications running on them.</p>
<h2 id="aims">Aims</h2>
<p>In this post, we will do the following: -</p>
<ul>
<li>Set up a Puppet Server</li>
<li>Install multiple Linux servers (physical and virtual) with Puppet agents
<ul>
<li>We will have machines running Debian (plain Buster and Proxmox), Rocky Linux, Ubuntu and Alma Linux</li>
</ul>
</li>
<li>Run Prometheus on one of the servers</li>
<li>Install and run Hashicorp&rsquo;s Consul on all of the servers (for service discovery)</li>
<li>Install and run the Prometheus Node Exporter on all of the servers</li>
<li>Register the Prometheus <code>node_exporter</code> as a service in Consul so that Prometheus can discover and monitor the servers</li>
</ul>
<p>I decided to include Rocky and Alma Linux to see how well Puppet works on both Debian-based and RHEL-based distributions, while also seeing the progress of both distributions vying to be the &ldquo;replacement&rdquo; for pre-Stream CentOS.</p>
<h2 id="setting-up-a-puppet-server">Setting up a Puppet Server</h2>
<p>The Puppet Server for this post is running on Debian Buster, with 2 CPU cores and 2G of memory. In a production scenario you would want to use more RAM and CPU, but for the purposes of this post, this is more than enough.</p>
<h3 id="add-the-apt-repository">Add the Apt repository</h3>
<p>To install the Puppet Apt repository, follow the instructions <a href="https://puppet.com/docs/puppet/7/install_puppet.html">here</a>. To summarise, you install a Debian package that adds the Puppet repository to your machine, including the relevant GPG keys.</p>
<h3 id="install-the-puppet-server">Install the Puppet Server</h3>
<p>As the Puppet Server uses Clojure, first install a compatible Java Virtual Machine. You can install either a Java 8 or 11 compatible JVM. The easiest to get started with is the OpenJDK (install with <code>apt install openjdk-11-jre-headless</code>). If you want to choose an alternative (e.g. Amazon&rsquo;s Corretto, the official Oracle JVM), then Puppet supports these too.</p>
<p>After this, install the Puppet Server using <code>apt install puppetserver</code>. You can also install the Puppet Agent (using <code>apt install puppet-agent</code>) so that the configuration of the server is also managed by Puppet.</p>
<h3 id="configure-the-server">Configure the server</h3>
<p>After installing the server we then need to configure it. It has mostly sane defaults, but we do need to make a couple of changes. If you are using less than 4Gb of memory, or you are using significantly more (and therefore have more resources that Puppet could use), update the memory assigned the JVM in <code>/etc/default/puppetserver</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># Modify this if you&#39;d like to change the memory allocation, enable JMX, etc</span>
JAVA_ARGS<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;-Xms1g -Xmx1g -Djruby.logger.class=com.puppetlabs.jruby_utils.jruby.Slf4jLogger&#34;</span>
</code></pre></div><p>The <code>-Xms</code> (initial memory assigned) and <code>-Xmx</code> (maximum amount of memory) are <code>2g</code> (i.e. 2Gb) of memory by default. With the machine I am running, this would take up all available memory, so I reduced this to <code>1g</code> (i.e. 1Gb) instead. If your Puppet Server has more memory available (say 8Gb, 16Gb or more), then raise this so that the server has more resources available to process more agents.</p>
<p>I also update the <code>basemodulepath</code> variable in <code>/etc/puppet/puppet.conf</code> to point to multiple directories: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">basemodulepath <span style="color:#f92672">=</span> $codedir/forge:$codedir/modules:$codedir/custom:modules
</code></pre></div><p>The <code>$codedir</code> can be changed, but by default is <code>/etc/puppetlabs/code</code> (i.e. where all of your manifest and modules reside). The purpose of each directory is: -</p>
<ul>
<li><code>module</code> - Standard directory for modules</li>
<li><code>forge</code> - Modules downloaded from <a href="https://forge.puppet.com/">Puppet Forge</a></li>
<li><code>custom</code> - Modules that I have created</li>
</ul>
<p>Puppet Forge is a place where users can submit modules they have created, which allows you to make use of other peoples code in your Puppet environment. This can be everything from package management to firewall rules. Using modules from Puppet Forge allows you to focus on what is unique to your environment, with common tasks using tried and tested modules.</p>
<h3 id="add-puppet-to-path">Add Puppet to <code>$PATH</code></h3>
<p>By default, Puppet is installed in the <code>/opt/puppetlabs</code> directory when on Linux. This is fine, except that if you want to interact with the Puppet Server or Puppet Agent, you have to refer to <code>/opt/puppetlabs/bin/puppet</code> or <code>/opt/puppetlabs/bin/puppetserver</code>.</p>
<p>The easiest way to change this is to add this to your <code>$PATH</code> variable (usually defined in <code>~/.bashrc</code>, <code>~/.zshrc</code>, <code>~/.profile</code>, or other shell configuration file). However Puppet usually requires privileged access (i.e. <code>sudo</code> or <code>root</code>). To avoid running commands as <code>root</code>, you can update an option in your <code>sudo</code> configuration called the <code>secure_path</code>. This is the <code>$PATH</code> variable that <code>sudo</code> uses, which makes it available to all users with <code>sudo</code> access.</p>
<p>This variable is in <code>/etc/sudoers</code> on both Debian-based and RHEL-based distributions. For example, on Debian, you would update it to this: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">Defaults	secure_path<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/puppetlabs/bin&#34;</span>
</code></pre></div><p>At this point, you can run <code>sudo puppet</code> or <code>sudo puppetserver</code> directly.</p>
<h2 id="setting-up-machines-as-puppet-agents">Setting up machines as Puppet Agents</h2>
<p>The next task is to install the agents on our nodes. In this post, I have installed the agent on: -</p>
<ul>
<li>A Debian Buster virtual machine with 2Gb of RAM and 2 CPU cores, running the Puppet server</li>
<li>A Debian Buster virtual machine with 2Gb of RAM and 2 CPU cores, to run Prometheus</li>
<li>An Ubuntu virtual machine with 1 CPU core and 2Gb of RAM</li>
<li>A Rocky Linux virtual machine with 2 CPU cores and 2Gb of RAM</li>
<li>An Alma Linux virtual machine with 2 CPU cores and 2Gb of RAM</li>
<li>Two Proxmox (based upon Debian Buster) hypervisors that run all of the aforementioned machines
<ul>
<li>One is a Lenovo X220 with an Intel i5-2420m and 16Gb of RAM</li>
<li>One is a HP Elitebook 9470m with an Intel i7-3687u and 16Gb of RAM</li>
<li>Both are running in a Proxmox cluster</li>
</ul>
</li>
</ul>
<h3 id="installing-the-repositories">Installing the repositories</h3>
<p>As with the Puppet Server installation, when on Debian or Debian-like systems (e.g. Ubuntu), download the Debian package that enables the repository. This is the same repository as used for the server. If you are on Debian, do the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># Debian Buster</span>
$ wget https://apt.puppet.com/puppet7-release-buster.deb

<span style="color:#75715e"># Ubuntu Focal Fossa (20.04 LTS)</span>
$ wget https://apt.puppet.com/puppet7-release-focal.deb
</code></pre></div><p>You then install the repository using <code>dpkg -i puppet7-release-$DISTRO.deb</code>.</p>
<p>On RHEL-based distributions, you can install the RPMs directly using: &ndash;</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># RHEL7-based (eg CentOS 7, Red Hat Enterprise Linux 7, Oracle Linux 7)</span>
$ rpm -Uvh https://yum.puppet.com/puppet7-release-el-7.noarch.rpm

<span style="color:#75715e"># RHEL8-based (eg Rocky Linux 8, Alma Linux 8, Red Hat Enterprise Linux 8)</span>
$ rpm -Uvh https://yum.puppet.com/puppet7-release-el-8.noarch.rpm
</code></pre></div><h3 id="installing-the-puppet-agent">Installing the Puppet Agent</h3>
<p>Once the repositories are enabled, install the Puppet Agent with either <code>apt install puppet-agent</code>, <code>yum install puppet-agent</code> or <code>dnf install puppet-agent</code> (depending on the distribution). The Puppet Agent is mostly self contained, so you do not need to install anything like Ruby or a Java Virtual Machine to start using it.</p>
<p>Once the agent is installed, you can register the agents against the Puppet Server</p>
<h2 id="registering-agents">Registering Agents</h2>
<h3 id="configure-the-puppet-server">Configure the Puppet Server</h3>
<p>To register agents, they need to know what server they are registering with. You can update the <code>/etc/puppet/puppet.conf</code> file, or you can run the following command: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo puppet config set server $DNS_OF_PUPPETSERVER --section main
</code></pre></div><p>Replace <code>$DNS_OF_PUPPETSERVER</code> with the DNS hostname of your Puppet Server. For me, this is <code>puppetserver.meshuggah.yetiops.lab</code>. This updates the Puppet configuration file to refer to the correct Puppet server to look like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[main]
server = puppetserver.meshuggah.yetiops.lab
</code></pre></div><h3 id="ssl-bootstrap">SSL Bootstrap</h3>
<p>Puppet Agents register against the server by creating a certificate signing request. This must be signed by the server to confirm that the Agent is authorized to run manifests and modules retrieved from.</p>
<p>To do this on a machine running the Puppet agent, run <code>puppet ssl bootstrap</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo puppet ssl bootstrap
Info: Creating a new RSA SSL key <span style="color:#66d9ef">for</span> prometheus.meshuggah.yetiops.lab
Info: csr_attributes file loading from /etc/puppetlabs/puppet/csr_attributes.yaml
Info: Creating a new SSL certificate request <span style="color:#66d9ef">for</span> prometheus.meshuggah.yetiops.lab
Info: Certificate Request fingerprint <span style="color:#f92672">(</span>SHA256<span style="color:#f92672">)</span>: 50:2A:55:A1:99:72:9F:A0:43:0C:A0:76:CD:01:24:9E:A0:63:19:EC:61:2E:50:1B:22:44:8A:4D:CF:0A:A4:3D
Info: Certificate <span style="color:#66d9ef">for</span> prometheus.meshuggah.yetiops.lab has not been signed yet
Couldn<span style="color:#e6db74">&#39;t fetch certificate from CA server; you might still need to sign this agent&#39;</span>s certificate <span style="color:#f92672">(</span>prometheus.meshuggah.yetiops.lab<span style="color:#f92672">)</span>.
Info: Will try again in <span style="color:#ae81ff">120</span> seconds.
</code></pre></div><p>Login to Puppet Server and sign the certificate with <code>puppetserver ca sign --certname prometheus.meshuggah.yetiops.lab</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo puppetserver ca sign --certname prometheus.meshuggah.yetiops.lab
Successfully signed certificate request <span style="color:#66d9ef">for</span> prometheus.meshuggah.yetiops.lab
</code></pre></div><p>The Puppet Agent will then check if the request has been signed within 2 minutes (120 seconds), and then confirm that the process is complete: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">Info: csr_attributes file loading from /etc/puppetlabs/puppet/csr_attributes.yaml
Info: Creating a new SSL certificate request <span style="color:#66d9ef">for</span> prometheus.meshuggah.yetiops.lab
Info: Certificate Request fingerprint <span style="color:#f92672">(</span>SHA256<span style="color:#f92672">)</span>: 50:2A:55:A1:99:72:9F:A0:43:0C:A0:76:CD:01:24:9E:A0:63:19:EC:61:2E:50:1B:22:44:8A:4D:CF:0A:A4:3D
Info: Downloaded certificate <span style="color:#66d9ef">for</span> prometheus.meshuggah.yetiops.lab from https://puppetserver.meshuggah.yetiops.lab:8140/puppet-ca/v1
Notice: Completed SSL initialization
</code></pre></div><h2 id="other-tasks">Other tasks</h2>
<h3 id="autosigning">Autosigning</h3>
<p>If you manage a lot of infrastructure, the process of signing each individual server can become quite unwieldy and time consuming. Instead you can use <strong>autosigning</strong>. This comes with a caveat in that, this potentially makes your Puppet estate less secure, as the rules can be quite <strong>open</strong> for what will be autosigned.</p>
<p>However if you are only on a private network with no external access, or are willing to overlook the potential security implications for the gains in time and automation, you can create rules in a file called <code>/etc/puppetlabs/puppet/autosign.conf</code> for Agents that should have their certificate signing requests automatically approved and assigned. More information on this is available <a href="https://puppet.com/docs/puppet/6/ssl_autosign.html">here</a>, but at a very basic level, you can autosign anything with a certain domain name like so: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ cat autosign.conf
*.meshuggah.yetiops.lab
</code></pre></div><h3 id="view-all-registered-agents">View all registered agents</h3>
<p>At this point, you can view all registered agents on the Server with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo puppetserver ca list --all
Signed Certificates:
    prometheus.meshuggah.yetiops.lab         <span style="color:#f92672">(</span>SHA256<span style="color:#f92672">)</span>  27:0C:C5:79:00:20:21:36:72:90:3C:DD:C5:2A:BE:5D:27:BA:A7:A2:B5:53:11:C9:EC:F6:88:10:AF:92:EC:92	alt names: <span style="color:#f92672">[</span><span style="color:#e6db74">&#34;DNS:prometheus.meshuggah.yetiops.lab&#34;</span><span style="color:#f92672">]</span>
    ubuntu-util.meshuggah.yetiops.lab        <span style="color:#f92672">(</span>SHA256<span style="color:#f92672">)</span>  BF:59:C4:B5:68:68:E1:2F:F1:74:D1:28:33:D3:75:00:17:63:6F:E9:2F:4D:2A:62:44:19:C0:E0:BC:3A:3A:AD	alt names: <span style="color:#f92672">[</span><span style="color:#e6db74">&#34;DNS:ubuntu-util.meshuggah.yetiops.lab&#34;</span><span style="color:#f92672">]</span>
    almautil.meshuggah.yetiops.lab           <span style="color:#f92672">(</span>SHA256<span style="color:#f92672">)</span>  1F:C0:51:D8:BF:48:FF:F7:C8:1B:87:CA:21:A6:29:4E:F3:71:5E:AF:58:65:13:51:CD:61:4A:AA:44:FB:3B:F6	alt names: <span style="color:#f92672">[</span><span style="color:#e6db74">&#34;DNS:almautil.meshuggah.yetiops.lab&#34;</span><span style="color:#f92672">]</span>
    pve.meshuggah.yetiops.lab                <span style="color:#f92672">(</span>SHA256<span style="color:#f92672">)</span>  04:C5:BE:D9:EE:55:71:1F:26:39:94:4C:1F:CC:9C:82:C6:2F:BD:A2:81:96:DC:5B:5B:61:0B:38:17:4E:5E:FD	alt names: <span style="color:#f92672">[</span><span style="color:#e6db74">&#34;DNS:pve.meshuggah.yetiops.lab&#34;</span><span style="color:#f92672">]</span>
    pve2.meshuggah.yetiops.lab               <span style="color:#f92672">(</span>SHA256<span style="color:#f92672">)</span>  FD:66:5F:D7:46:AF:38:91:31:4A:26:65:76:FA:4B:AE:37:B0:FD:E8:A0:01:38:6F:3E:D5:19:E0:33:D1:B9:37	alt names: <span style="color:#f92672">[</span><span style="color:#e6db74">&#34;DNS:pve2.meshuggah.yetiops.lab&#34;</span><span style="color:#f92672">]</span>
    rockyutil.meshuggah.yetiops.lab          <span style="color:#f92672">(</span>SHA256<span style="color:#f92672">)</span>  95:8B:C2:06:0F:D9:81:00:6D:B5:B7:DB:FA:78:74:C2:6C:54:1C:27:85:51:F8:12:D7:FF:80:69:3F:A8:B3:ED	alt names: <span style="color:#f92672">[</span><span style="color:#e6db74">&#34;DNS:rockyutil.meshuggah.yetiops.lab&#34;</span><span style="color:#f92672">]</span>
    puppetserver.meshuggah.yetiops.lab       <span style="color:#f92672">(</span>SHA256<span style="color:#f92672">)</span>  F4:C6:B6:11:A7:CC:03:5E:0B:79:1A:9B:F9:37:7D:CA:9E:63:7A:5D:C2:DA:5D:0A:79:4F:EE:6C:93:44:4B:91	alt names: <span style="color:#f92672">[</span><span style="color:#e6db74">&#34;DNS:puppet&#34;</span>, <span style="color:#e6db74">&#34;DNS:puppetserver.meshuggah.yetiops.lab&#34;</span><span style="color:#f92672">]</span>	authorization extensions: <span style="color:#f92672">[</span>pp_cli_auth: true<span style="color:#f92672">]</span>
</code></pre></div><p>If you run this without the <code>--all</code> option, it will show all unsigned requests, which is useful for if you need to determine the <code>certname</code> (effectively the hostname) of Agents that have registered.</p>
<h3 id="forge-modules">Forge modules</h3>
<p>As we don&rsquo;t want to define our own code to install packages, define firewall rules and other &ldquo;basic&rdquo; operating system tasks, we add Puppet modules from Puppet Forge. It is possible to install them all manually. However you may prefer to use a module manager for this.</p>
<p>There are multiple module managers available, including <a href="https://github.com/puppetlabs/r10k">r10k</a> (which is one of the most known and popular). For the purposes of this post, I am using one called <a href="https://github.com/voxpupuli/librarian-puppet">librarian-puppet</a> (mainly because this is what my workplace uses). It is supplied as a Ruby Gem, which is installed using <code>gem install librarian-puppet</code>.</p>
<p>Once it is installed, run <code>librarian-puppet init</code> from inside the <code>/etc/puppetlabs</code> directory. This creates a <code>Puppetfile</code>, which you populate with the required modules and versions. I also run <code>librarian-puppet config path code/forge</code> to make sure that any modules installed by <code>librarian-puppet</code> go into a separate directory, reducing the risk of conflicting with my own modules.</p>
<p>My <code>Puppetfile</code> looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#75715e">#!/usr/bin/env ruby
</span><span style="color:#75715e">#^syntax detection</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#a6e22e">forge</span> <span style="color:#e6db74">&#39;https://forgeapi.puppetlabs.com&#39;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#a6e22e">mod</span>   <span style="color:#e6db74">&#39;puppet-archive&#39;</span>,              <span style="color:#e6db74">&#39;3.2.0&#39;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#a6e22e">mod</span>   <span style="color:#e6db74">&#39;puppet-boolean&#39;</span>,              <span style="color:#e6db74">&#39;2.0.1&#39;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#a6e22e">mod</span>   <span style="color:#e6db74">&#39;puppetlabs-apt&#39;</span>,              <span style="color:#e6db74">&#39;4.5.1&#39;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#a6e22e">mod</span>   <span style="color:#e6db74">&#39;puppetlabs-concat&#39;</span>,           <span style="color:#e6db74">&#39;4.2.1&#39;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#a6e22e">mod</span>   <span style="color:#e6db74">&#39;puppetlabs-firewall&#39;</span>,         <span style="color:#e6db74">&#39;1.12.0&#39;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#a6e22e">mod</span>   <span style="color:#e6db74">&#39;puppet-firewalld&#39;</span>,            <span style="color:#e6db74">&#39;4.4.0&#39;</span><span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>If you then run <code>librarian-puppet install</code>, all of the above modules with their specified versions are installed, allowing you to refer to them in your code.</p>
<h2 id="puppet-module-setting-an-motd">Puppet Module: Setting an MOTD</h2>
<p>The first module we create is a Message Of The Day module. This is displayed to users on login. First, create a folder called <code>motd</code> in your modules directory (in my case <code>/etc/puppetlabs/code/custom/motd</code>) and then create a manifests and templates directory.</p>
<p>The following is the directory structure of this module: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">/etc/puppetlabs/code$ tree custom/motd/
custom/motd/
├── manifests
│   └── env.pp
└── templates
    └── motd.erb
</code></pre></div><h3 id="manifest">Manifest</h3>
<p>The <code>env.pp</code> manifest looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">motd</span>::<span style="color:#a6e22e">env</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	$pve_host <span style="color:#f92672">=</span> <span style="color:#a6e22e">hiera</span>(<span style="color:#e6db74">&#39;pve_host&#39;</span>, <span style="color:#e6db74">&#39;&#39;</span>)<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#66d9ef">file</span> { <span style="color:#e6db74">&#39;/etc/motd&#39;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">content</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">template</span>(<span style="color:#e6db74">&#39;motd/motd.erb&#39;</span>),<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">owner</span>   <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;root&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">group</span>   <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;root&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">mode</span>	<span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;644&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>The Puppet class is called <code>motd::env</code>. In this we: -</p>
<ul>
<li>Lookup a variable called <code>pve_host</code> that we define in <strong>hiera</strong> data</li>
<li>Create an action to update a file (<code>/etc/motd</code>), based upon a template</li>
<li>The file will be owned by <code>root</code>, the <code>group</code> root, and have <code>rw-r--r--</code> permissions</li>
</ul>
<p>This is a very basic class, but is very common as a lot of what we will do is based upon templates.</p>
<h3 id="template">Template</h3>
<p>The template itself looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">Configured</span> <span style="color:#a6e22e">by</span> <span style="color:#a6e22e">Puppet</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#a6e22e">Environment</span>: <span style="color:#f92672">&lt;</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">@</span><span style="color:#a6e22e">environment</span> <span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">&gt;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#a6e22e">Hostname</span>: <span style="color:#f92672">&lt;</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">@</span><span style="color:#a6e22e">hostname</span> <span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">&gt;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#a6e22e">Hypervisor</span>: <span style="color:#f92672">&lt;</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">@</span><span style="color:#a6e22e">pve_host</span> <span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">&gt;</span><span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>For those who have used Jinja2, the templating system in Puppet can take a little getting used to. However a lot of the functionality is similar, with the difference mainly being in syntax.</p>
<p>In the above, the <code>&lt;%= $VAR %&gt;</code> syntax is a placeholder for a variable (like <code>{{ $VAR }}</code> in Jinja2). The <code>environment</code> and <code>hostname</code> variables are derived from Puppet facts about the node this runs on, whereas the <code>pve_host</code> variable is derived from our <strong>hiera</strong> data in the previous section.</p>
<h3 id="profile">Profile</h3>
<p>As mentioned, we are using roles and profiles to associate the actions required to nodes. This profile can be found in <code>/etc/puppetlabs/code/environments/production/modules/profile/manifests/mymotd.pp</code></p>
<p>It is worth noting that I am not taking full advantage of the profiles approach, in that I am using a &ldquo;default&rdquo; classifier in the profile: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">profile</span>::<span style="color:#a6e22e">mymotd</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>{<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#66d9ef">case</span> $facts[<span style="color:#e6db74">&#39;hostname_role&#39;</span>] {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#66d9ef">default</span>: {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>            <span style="color:#66d9ef">include</span> <span style="color:#a6e22e">motd</span>::<span style="color:#a6e22e">env</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        }<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    }<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>We could derive different roles based upon a fact called <code>hostname_role</code> (which would be a created/custom fact applied to nodes). You could choose to include additional/different classes based upon matching a different <code>hostname_role</code> (e.g. if the <code>hostname_role</code> is <code>monitoring-server</code>, also include additional classes that update the MOTD).</p>
<p>In most cases in this post, we will include a single class, but there is nothing saying this is a strict requirement.</p>
<h3 id="role">Role</h3>
<p>The role that refers to this profile is the <code>base</code> role: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">class</span> <span style="color:#66d9ef">role</span>::<span style="color:#a6e22e">base</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>{<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#66d9ef">include</span> <span style="color:#a6e22e">profile</span>::<span style="color:#a6e22e">mymotd</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#66d9ef">include</span> <span style="color:#a6e22e">profile</span>::<span style="color:#a6e22e">packages</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#66d9ef">include</span> <span style="color:#a6e22e">profile</span>::<span style="color:#a6e22e">consul</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#66d9ef">include</span> <span style="color:#a6e22e">profile</span>::<span style="color:#a6e22e">node_exporter</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>This role can be found in <code>/etc/puppetlabs/code/environments/production/modules/profile/role/base.pp</code>.</p>
<p>As you can see, in this base profile, we are including multiple profiles, which is where the power of the Roles and Profiles abstraction method starts to come to fruition.</p>
<p>Now all nodes that match our base role will include the <code>mymotd</code> profile, <code>packages</code>, profile, <code>consul</code> profile and <code>node_exporter</code> profile. This is where the consistency of configuration management is achieved, as we can effectively guarantee a minimum set of applications/configuration in place for the entire Puppet-managed estate.</p>
<h3 id="matching-nodes">Matching nodes</h3>
<p>Now we need to make sure this role is applied to nodes. This can be achieved by adding node declarations in our <code>site.pp</code> file (found in <code>/etc/puppetlabs/code/environments/production/manifests/site.pp</code>): -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">node</span> <span style="color:#f92672">/</span><span style="color:#960050;background-color:#1e0010">^</span><span style="color:#a6e22e">prometheus</span><span style="color:#960050;background-color:#1e0010">.</span><span style="color:#f92672">/</span>     <span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>{ <span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#66d9ef">include</span> <span style="color:#66d9ef">role</span>::<span style="color:#a6e22e">prometheus</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">node</span> <span style="color:#66d9ef">default</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>{<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#66d9ef">include</span> <span style="color:#66d9ef">role</span>::<span style="color:#a6e22e">base</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>As we can see, we have a <strong>default</strong> node selector, as well a selector that matches the <code>prometheus</code> role. Any node that has a hostname that doesn&rsquo;t begin with <code>prometheus.</code> will be matched by the default role.</p>
<h2 id="puppet-module-installing-packages">Puppet Module: Installing packages</h2>
<p>The next module is for installing packages. In this, we are just installing the <code>qemu-guest-agent</code>, but it could be any package(s): -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">packages</span>::<span style="color:#a6e22e">qemu</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#66d9ef">package</span>{<span style="color:#e6db74">&#39;qemu-guest-agent&#39;</span>: <span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#a6e22e">ensure</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;installed&#39;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    }<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>There are no templates being referenced, and we are using the distribution packages rather than adding any additional repositories.</p>
<p>Our profile is almost identical to the previous profile: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">profile</span>::<span style="color:#a6e22e">packages</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>{<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#66d9ef">case</span> $facts[<span style="color:#e6db74">&#39;hostname_role&#39;</span>] {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#66d9ef">default</span>: {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>            <span style="color:#66d9ef">include</span> <span style="color:#a6e22e">packages</span>::<span style="color:#a6e22e">qemu</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        }<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    }<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>As we saw in the previous module, we are referencing the profile from the <code>role::base</code> class, meaning all machines referencing this class will have the agent installed.</p>
<h2 id="puppet-module-hashicorp-repository">Puppet Module: Hashicorp Repository</h2>
<p>The next module is one that is not referenced by a role or profile. Instead it is used by other modules. This module adds the Hashicorp Debian or RPM repositories so that we can install the latest versions of Hashicorp&rsquo;s packages (e.g. <strong>Consul</strong>), rather than relying on out-of-date distribution packages.</p>
<p>The contents of the manifest are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">hashicorp</span>::<span style="color:#a6e22e">repository</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	$distro <span style="color:#f92672">=</span> $facts[&#39;os&#39;][&#39;distro&#39;][<span style="color:#e6db74">&#39;codename&#39;</span>]<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#66d9ef">if</span> $facts[&#39;os&#39;][<span style="color:#e6db74">&#39;family&#39;</span>] <span style="color:#f92672">==</span> <span style="color:#e6db74">&#39;Debian&#39;</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">apt</span>::<span style="color:#a6e22e">source</span> { <span style="color:#e6db74">&#39;hashicorp&#39;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">comment</span>  <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;Hashicorp Repository&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">location</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;https://apt.releases.hashicorp.com&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">release</span>  <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;${distro}&#34;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">repos</span>    <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;main&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">key</span>      <span style="color:#f92672">=&gt;</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>				<span style="color:#e6db74">&#39;id&#39;</span><span style="color:#f92672">=&gt;</span><span style="color:#e6db74">&#39;E8A032E094D8EB4EA189D270DA418C88A3219F7B&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>				<span style="color:#e6db74">&#39;server&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;keyserver.ubuntu.com&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			},<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#66d9ef">include</span>  <span style="color:#f92672">=&gt;</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>				<span style="color:#e6db74">&#39;deb&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			},<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#66d9ef">if</span> $facts[&#39;os&#39;][<span style="color:#e6db74">&#39;family&#39;</span>] <span style="color:#f92672">==</span>  <span style="color:#e6db74">&#39;RedHat&#39;</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#66d9ef">yumrepo</span> { <span style="color:#e6db74">&#39;hashicorp&#39;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">enabled</span>  <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">1</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">descr</span>    <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;Hashicorp Repository&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">baseurl</span>  <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;https://rpm.releases.hashicorp.com/RHEL/$releasever/$basearch/stable&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">gpgkey</span>   <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;https://rpm.releases.hashicorp.com/gpg&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">gpgcheck</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">1</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">target</span>    <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;/etc/yum.repo.d/hashicorp.repo&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  		}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>In this, we are referencing <strong>facts</strong> (i.e. information about or applied to the Agent(s)) to get the <code>codename</code> (e.g. <code>buster</code> for Debian 10, <code>focal</code> for Ubuntu 20.04 LTS). We also run a conditional against the operating system <code>family</code>, which for us is either Debian-based or Red Hat-based.</p>
<p>If the host is Debian-based, add the Apt repository using the <code>codename</code>. If the host is Red Hat-based, then add the Hashicorp Yum repository. The <code>codename</code> is not required for the Yum repository, as this information is derived by Yum/DNF when installing packages.</p>
<p>I have separated this out because it is not Consul specific, and instead could be used to install Hashicorp&rsquo;s Terraform, Packer or other tools. However it is only required if we want any of Hashicorp&rsquo;s tools, so referring to it from the Consul module (and potentially Terraform or Packer modules) makes a lot of sense.</p>
<h2 id="puppet-module-installing-and-running-consul">Puppet Module: Installing and running Consul</h2>
<p>The Consul module has multiple manifests, allowing us to separate out the logical steps taken. This makes it easier to understand what the module is doing. The structure of the module is as follows: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">/etc/puppetlabs/code$ tree custom/consul/
custom/consul/
├── manifests
│   ├── config.pp
│   ├── firewall.pp
│   ├── init.pp
│   ├── install.pp
│   └── service.pp
└── templates
    ├── consul.client.hcl.erb
    ├── consul.prom_service.hcl.erb
    └── consul.server.hcl.erb
</code></pre></div><p>We use an <code>init.pp</code> manifest that contains other classes: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">consul</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#a6e22e">contain</span> <span style="color:#a6e22e">hashicorp</span>::<span style="color:#a6e22e">repository</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#a6e22e">contain</span> <span style="color:#a6e22e">consul</span>::<span style="color:#a6e22e">install</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#a6e22e">contain</span> <span style="color:#a6e22e">consul</span>::<span style="color:#a6e22e">config</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#a6e22e">contain</span> <span style="color:#a6e22e">consul</span>::<span style="color:#a6e22e">firewall</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#a6e22e">contain</span> <span style="color:#a6e22e">consul</span>::<span style="color:#66d9ef">service</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#66d9ef">Class</span>[<span style="color:#e6db74">&#39;hashicorp::repository&#39;</span>]<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#f92672">~&gt;</span> <span style="color:#66d9ef">Class</span>[<span style="color:#e6db74">&#39;consul::install&#39;</span>]<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#f92672">~&gt;</span> <span style="color:#66d9ef">Class</span>[<span style="color:#e6db74">&#39;consul::config&#39;</span>]<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#f92672">~&gt;</span> <span style="color:#66d9ef">Class</span>[<span style="color:#e6db74">&#39;consul::firewall&#39;</span>]<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#f92672">~&gt;</span> <span style="color:#66d9ef">Class</span>[<span style="color:#e6db74">&#39;consul::service&#39;</span>]<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>What we see here is that not only are we referencing all of our Consul manifests, but we also reference the Hashicorp module (specifically the <code>hashicorp::repository</code> class) so that we can install Consul directly from Hashicorp.</p>
<p>In our profile, rather than referencing each class in this module, we can just reference <code>class consul</code> to include all of the manifests.</p>
<p>Also, we can see that there are a number of <code>Class['$CLASS_NAME']</code> definitions. This is to guarantee the order in which each Class is executed. The reason behind this is that task execution in Puppet is ran in parallel rather than serial (i.e. one after the other). We wouldn&rsquo;t be able to start the Consul service until Consul is actually installed, so we need to ensure the step that controls the service does not trigger too early.</p>
<p>In my experience so far, the RHEL-based distributions installed Consul in a short enough time that the task execution still succeeded. Debian-based distributions however hadn&rsquo;t finished installing Consul by the time we want to configure Consul and start the service.</p>
<h3 id="installing-consul">Installing Consul</h3>
<h4 id="manifest-1">Manifest</h4>
<p>The <code>install.pp</code> manifest looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">consul</span>::<span style="color:#a6e22e">install</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#a6e22e">group</span> { <span style="color:#e6db74">&#39;consul&#39;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">name</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;consul&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#66d9ef">user</span> { <span style="color:#e6db74">&#39;consul&#39;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">name</span>   <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;consul&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">groups</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;consul&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">shell</span>  <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;/bin/false&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#66d9ef">if</span> $facts[&#39;os&#39;][<span style="color:#e6db74">&#39;family&#39;</span>] <span style="color:#f92672">==</span> <span style="color:#e6db74">&#39;Debian&#39;</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#66d9ef">package</span>{<span style="color:#e6db74">&#39;consul&#39;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">ensure</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;latest&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">require</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">Class</span>[<span style="color:#e6db74">&#39;apt::update&#39;</span>]<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#66d9ef">if</span> $facts[&#39;os&#39;][<span style="color:#e6db74">&#39;family&#39;</span>] <span style="color:#f92672">==</span> <span style="color:#e6db74">&#39;RedHat&#39;</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#66d9ef">package</span>{<span style="color:#e6db74">&#39;consul&#39;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">ensure</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;latest&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#66d9ef">file</span> { <span style="color:#e6db74">&#39;/var/lib/consul&#39;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">ensure</span>  <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;directory&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">owner</span>   <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;consul&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">group</span>   <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;consul&#39;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>To explain the steps here, we are: -</p>
<ul>
<li>Creating a <code>consul</code> user and group</li>
<li>Installing the latest <code>consul</code> package, as well as triggering an update of <code>apt</code> if running on a Debian-based system
<ul>
<li>Without the update, Debian-based distributions will not use the repository, instead using whatever is in their package archive (usually a very out of date version)</li>
</ul>
</li>
<li>Creating the <code>/var/lib/consul</code> directory for the <code>consul</code> data, owned by the <code>consul</code> user and group</li>
</ul>
<p>At this point Consul is installed, but is not running or configured.</p>
<h3 id="configuring-consul">Configuring Consul</h3>
<h4 id="manifest-2">Manifest</h4>
<p>The next manifest is the <code>config.pp</code> manifest: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">consul</span>::<span style="color:#a6e22e">config</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	$consul_dc <span style="color:#f92672">=</span> <span style="color:#a6e22e">lookup</span>(<span style="color:#e6db74">&#39;consul_vars.dc&#39;</span>)<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	$consul_enc <span style="color:#f92672">=</span> <span style="color:#a6e22e">lookup</span>(<span style="color:#e6db74">&#39;consul_vars.enckey&#39;</span>)<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	$servers <span style="color:#f92672">=</span> <span style="color:#a6e22e">lookup</span>(<span style="color:#e6db74">&#39;consul_vars.servers&#39;</span>)<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	$is_server <span style="color:#f92672">=</span> <span style="color:#a6e22e">lookup</span>(<span style="color:#e6db74">&#39;consul.server&#39;</span>)<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#66d9ef">if</span> $is_server {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#66d9ef">file</span> { <span style="color:#e6db74">&#39;/etc/consul.d/consul.hcl&#39;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">content</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">template</span>(<span style="color:#e6db74">&#39;consul/consul.server.hcl.erb&#39;</span>),<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">owner</span>   <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;root&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">group</span>   <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;root&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">mode</span>	<span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;644&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	} <span style="color:#66d9ef">else</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#66d9ef">file</span> { <span style="color:#e6db74">&#39;/etc/consul.d/consul.hcl&#39;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">content</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">template</span>(<span style="color:#e6db74">&#39;consul/consul.client.hcl.erb&#39;</span>),<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">owner</span>   <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;root&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">group</span>   <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;root&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">mode</span>	<span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;644&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>In the above we are: -</p>
<ul>
<li>Using Hiera lookups to retrieve our Consul Datacentre (<code>consul_vars.dc</code>), encryption key (<code>consul_vars.enc</code>), the list of Consul servers (<code>consul_vars.servers</code>) and whether the host is a Consul server itself (<code>consul.server</code>)</li>
<li>If the host has <code>consul.server</code> set to <code>true</code>, we use our <code>consul.server.hcl.erb</code> template</li>
<li>If the host has <code>consul.server</code> set to <code>false</code>, we use our <code>consul.client.hcl.erb</code> template</li>
</ul>
<h4 id="hiera-data">Hiera Data</h4>
<p>The Hiera data being referenced here can be found in <code>code/environments/production/data</code>. We have a <code>common.yaml</code> file which applies to all nodes in the <code>production</code> environment, and a node-specific <code>nodes/puppetserver.meshuggah.yetiops.lab.yaml</code> that overrides the <code>common.yaml</code> file for that node only: -</p>
<p><code>common.yaml</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul</span>:
    <span style="color:#66d9ef">server</span>: <span style="color:#66d9ef">false</span>
<span style="color:#66d9ef">consul_vars</span>:
    <span style="color:#66d9ef">dc</span>: meshuggah
    <span style="color:#66d9ef">servers</span>:
    - puppetserver.meshuggah.yetiops.lab
    <span style="color:#66d9ef">enckey</span>: <span style="color:#e6db74">&#34;fUPA97Ga7IDLl/HST3H3xw==&#34;</span>
</code></pre></div><p><code>nodes/puppetserver.meshuggah.yetiops.lab.yaml</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul</span>:
    <span style="color:#66d9ef">server</span>: <span style="color:#66d9ef">true</span>
</code></pre></div><p>What this means is that almost all Consul values are the same. The only exception is that the <code>puppetserver</code> will be a Consul server as well, rather than just a client.</p>
<h4 id="templates">Templates</h4>
<p>The templates that we reference are for the <code>server</code> and the <code>client</code>: -</p>
<p><code>consul.server.hcl.erb</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet">{<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#f92672">&lt;</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">-</span> <span style="color:#960050;background-color:#1e0010">@</span><span style="color:#a6e22e">networking</span>[<span style="color:#e6db74">&#39;interfaces&#39;</span>]<span style="color:#960050;background-color:#1e0010">.</span><span style="color:#a6e22e">each</span> <span style="color:#a6e22e">do</span> <span style="color:#f92672">|</span><span style="color:#66d9ef">interface</span><span style="color:#f92672">|</span> <span style="color:#f92672">-</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">&gt;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#f92672">&lt;</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">-</span> <span style="color:#66d9ef">if</span> <span style="color:#66d9ef">interface</span>[<span style="color:#ae81ff">1</span>][<span style="color:#e6db74">&#39;ip&#39;</span>] <span style="color:#f92672">=~</span> <span style="color:#f92672">/</span><span style="color:#960050;background-color:#1e0010">^</span><span style="color:#ae81ff">10.15.40</span><span style="color:#f92672">/</span> <span style="color:#f92672">-</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">&gt;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#e6db74">&#34;advertise_addr&#34;</span>: <span style="color:#e6db74">&#34;&lt;%= interface[1][&#39;ip&#39;] %&gt;&#34;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#e6db74">&#34;bind_addr&#34;</span>: <span style="color:#e6db74">&#34;&lt;%= interface[1][&#39;ip&#39;] %&gt;&#34;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#f92672">&lt;</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">-</span> <span style="color:#a6e22e">end</span> <span style="color:#f92672">-</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">&gt;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#f92672">&lt;</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">-</span> <span style="color:#a6e22e">end</span> <span style="color:#f92672">-</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">&gt;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#e6db74">&#34;bootstrap_expect&#34;</span>: <span style="color:#ae81ff">1</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#e6db74">&#34;client_addr&#34;</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#e6db74">&#34;data_dir&#34;</span>: <span style="color:#e6db74">&#34;/var/lib/consul&#34;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#e6db74">&#34;datacenter&#34;</span>: <span style="color:#e6db74">&#34;&lt;%= @consul_dc %&gt;&#34;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#e6db74">&#34;encrypt&#34;</span>: <span style="color:#e6db74">&#34;&lt;%= @consul_enc %&gt;&#34;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#e6db74">&#34;node_name&#34;</span>: <span style="color:#e6db74">&#34;&lt;%= @hostname %&gt;&#34;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#e6db74">&#34;retry_join&#34;</span>: [<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#f92672">&lt;</span><span style="color:#960050;background-color:#1e0010">%</span> <span style="color:#960050;background-color:#1e0010">@</span><span style="color:#a6e22e">servers</span><span style="color:#960050;background-color:#1e0010">.</span><span style="color:#a6e22e">each</span> <span style="color:#a6e22e">do</span> <span style="color:#f92672">|</span><span style="color:#a6e22e">server</span><span style="color:#f92672">|</span> <span style="color:#f92672">-</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">&gt;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#e6db74">&#34;&lt;%= server %&gt;&#34;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#f92672">&lt;</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">-</span> <span style="color:#a6e22e">end</span> <span style="color:#f92672">-</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">&gt;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  ],<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#e6db74">&#34;server&#34;</span>: <span style="color:#66d9ef">true</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#e6db74">&#34;autopilot&#34;</span>: {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#e6db74">&#34;cleanup_dead_servers&#34;</span>: <span style="color:#66d9ef">true</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#e6db74">&#34;last_contact_threshold&#34;</span>: <span style="color:#e6db74">&#34;200ms&#34;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#e6db74">&#34;max_trailing_logs&#34;</span>: <span style="color:#ae81ff">250</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#e6db74">&#34;server_stabilization_time&#34;</span>: <span style="color:#e6db74">&#34;10s&#34;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  },<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#e6db74">&#34;ui&#34;</span>: <span style="color:#66d9ef">true</span>,<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p><code>consul.client.hcl.erb</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet">{<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#f92672">&lt;</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">-</span> <span style="color:#960050;background-color:#1e0010">@</span><span style="color:#a6e22e">networking</span>[<span style="color:#e6db74">&#39;interfaces&#39;</span>]<span style="color:#960050;background-color:#1e0010">.</span><span style="color:#a6e22e">each</span> <span style="color:#a6e22e">do</span> <span style="color:#f92672">|</span><span style="color:#66d9ef">interface</span><span style="color:#f92672">|</span> <span style="color:#f92672">-</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">&gt;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#f92672">&lt;</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">-</span> <span style="color:#66d9ef">if</span> <span style="color:#66d9ef">interface</span>[<span style="color:#ae81ff">1</span>][<span style="color:#e6db74">&#39;ip&#39;</span>] <span style="color:#f92672">=~</span> <span style="color:#f92672">/</span><span style="color:#960050;background-color:#1e0010">^</span><span style="color:#ae81ff">10.15.40</span><span style="color:#f92672">/</span> <span style="color:#f92672">-</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">&gt;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#e6db74">&#34;advertise_addr&#34;</span>: <span style="color:#e6db74">&#34;&lt;%= interface[1][&#39;ip&#39;] %&gt;&#34;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#e6db74">&#34;bind_addr&#34;</span>: <span style="color:#e6db74">&#34;&lt;%= interface[1][&#39;ip&#39;] %&gt;&#34;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#f92672">&lt;</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">-</span> <span style="color:#a6e22e">end</span> <span style="color:#f92672">-</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">&gt;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#f92672">&lt;</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">-</span> <span style="color:#a6e22e">end</span> <span style="color:#f92672">-</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">&gt;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#e6db74">&#34;data_dir&#34;</span>: <span style="color:#e6db74">&#34;/var/lib/consul&#34;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#e6db74">&#34;datacenter&#34;</span>: <span style="color:#e6db74">&#34;&lt;%= @consul_dc %&gt;&#34;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#e6db74">&#34;encrypt&#34;</span>: <span style="color:#e6db74">&#34;&lt;%= @consul_enc %&gt;&#34;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#e6db74">&#34;node_name&#34;</span>: <span style="color:#e6db74">&#34;&lt;%= @hostname %&gt;&#34;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#e6db74">&#34;retry_join&#34;</span>: [<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#f92672">&lt;</span><span style="color:#960050;background-color:#1e0010">%</span> <span style="color:#960050;background-color:#1e0010">@</span><span style="color:#a6e22e">servers</span><span style="color:#960050;background-color:#1e0010">.</span><span style="color:#a6e22e">each</span> <span style="color:#a6e22e">do</span> <span style="color:#f92672">|</span><span style="color:#a6e22e">server</span><span style="color:#f92672">|</span> <span style="color:#f92672">-</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">&gt;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#e6db74">&#34;&lt;%= server %&gt;&#34;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#f92672">&lt;</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">-</span> <span style="color:#a6e22e">end</span> <span style="color:#f92672">-</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">&gt;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  ],<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#e6db74">&#34;server&#34;</span>: <span style="color:#66d9ef">false</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>In these templates, we see our first use of loops and conditionals in Puppet/ERB (Extended Ruby Template) syntax. If we look at the <code>server</code> loop, and compare to what we would use in Jinja2, you can start to see some similarities: -</p>
<p><strong>ERB</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#f92672">&lt;</span><span style="color:#960050;background-color:#1e0010">%</span> <span style="color:#960050;background-color:#1e0010">@</span><span style="color:#a6e22e">servers</span><span style="color:#960050;background-color:#1e0010">.</span><span style="color:#a6e22e">each</span> <span style="color:#a6e22e">do</span> <span style="color:#f92672">|</span><span style="color:#a6e22e">server</span><span style="color:#f92672">|</span> <span style="color:#f92672">-</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">&gt;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#e6db74">&#34;&lt;%= server %&gt;&#34;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#f92672">&lt;</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">-</span> <span style="color:#a6e22e">end</span> <span style="color:#f92672">-</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">&gt;</span><span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p><strong>Jinja2</strong></p>
<pre><code class="language-jinja2" data-lang="jinja2">{% for server in servers -%}
  &quot;{{ server }}&quot;,
{%- endfor -%}
</code></pre><p>There are differences, but not to the point of making it hard to understand. In the <strong>ERB</strong> example, the variable we will reference is inside pipes (<code>|</code>), and the variable to run the loop against has a <code>.each</code> method attached to it. In Jinja2, the language describes the action (i.e. for <code>var</code> in <code>list_of_vars</code>).</p>
<p>The conditional logic is also very similar: -</p>
<p><strong>ERB</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#f92672">&lt;</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">-</span> <span style="color:#66d9ef">if</span> <span style="color:#66d9ef">interface</span>[<span style="color:#ae81ff">1</span>][<span style="color:#e6db74">&#39;ip&#39;</span>] <span style="color:#f92672">=~</span> <span style="color:#f92672">/</span><span style="color:#960050;background-color:#1e0010">^</span><span style="color:#ae81ff">10.15.40</span><span style="color:#f92672">/</span> <span style="color:#f92672">-</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">&gt;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  [<span style="color:#960050;background-color:#1e0010">...</span>]<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#f92672">&lt;</span><span style="color:#960050;background-color:#1e0010">%</span> <span style="color:#a6e22e">end</span> <span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">&gt;</span><span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p><strong>Jinja2</strong></p>
<pre><code class="language-jinja2" data-lang="jinja2">{%- if (interface[1]['ip'] | regex_search(&quot;^10.15.40&quot;)) -%}
  [...]
{% endif %}
</code></pre><p>If anything, the <strong>ERB</strong> syntax is much less verbose.</p>
<p>To explain what is happening in the templates themselves: -</p>
<ul>
<li>The network interface facts are checked
<ul>
<li>If one of the interfaces has an IP starting with <code>10.15.40</code> (my lab range), then use that for the Consul <code>advertise_addr</code> and <code>bind_addr</code> values</li>
<li>This ensures Consul is listening on the correct interface</li>
</ul>
</li>
<li>We define the directory Consul will store it&rsquo;s data (<code>/var/lib/consul</code>)</li>
<li>We reference our Consul Datacentre, Encryption Key and the machine&rsquo;s hostname</li>
<li>We list all of the Consul Servers so that when a Consul Client or Server starts, it attempts to reach an existing Consul Server first</li>
<li>In addition, if the machine is running as a Consul Server, the template: -
<ul>
<li>Sets the <code>bootstrap_expect</code> value to <code>1</code> (i.e. don&rsquo;t wait for quorum, a single server is enough)</li>
<li>Set <code>server</code> to <code>true</code></li>
<li>Set some timeouts/thresholds for Consul</li>
<li>Enable the UI</li>
</ul>
</li>
</ul>
<h3 id="configuring-the-firewall">Configuring the firewall</h3>
<p>This task is only for Red Hat-based distributions, as Rocky and Alma Linux both have <code>firewalld</code> enabled by default. This isn&rsquo;t required for Debian, although in a production scenario it is advisable to run some form of firewall.</p>
<h4 id="manifests">Manifests</h4>
<p>The <code>firewall.pp</code> manifest looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">consul</span>::<span style="color:#a6e22e">firewall</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#66d9ef">if</span> $facts[&#39;os&#39;][<span style="color:#e6db74">&#39;family&#39;</span>] <span style="color:#f92672">==</span> <span style="color:#e6db74">&#39;RedHat&#39;</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">firewalld</span>::<span style="color:#a6e22e">custom_service</span>{<span style="color:#e6db74">&#39;consul&#39;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">short</span>       <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;consul&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">description</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;Consul&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">port</span>        <span style="color:#f92672">=&gt;</span> [<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			  {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			      <span style="color:#e6db74">&#39;port&#39;</span>     <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;8301&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			      <span style="color:#e6db74">&#39;protocol&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;tcp&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			  },<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			  {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			      <span style="color:#e6db74">&#39;port&#39;</span>     <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;8500&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			      <span style="color:#e6db74">&#39;protocol&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;tcp&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			  },<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			  {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			      <span style="color:#e6db74">&#39;port&#39;</span>     <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;8600&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			      <span style="color:#e6db74">&#39;protocol&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;tcp&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			  },<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			  {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			      <span style="color:#e6db74">&#39;port&#39;</span>     <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;8301&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			      <span style="color:#e6db74">&#39;protocol&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;udp&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			  },<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			  {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			      <span style="color:#e6db74">&#39;port&#39;</span>     <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;8600&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			      <span style="color:#e6db74">&#39;protocol&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;udp&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			  },<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			]<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">firewalld_service</span> { <span style="color:#e6db74">&#39;Consul&#39;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">ensure</span>  <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;present&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#66d9ef">service</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;consul&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#66d9ef">zone</span>    <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;public&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>The above creates a <code>firewalld</code> custom service for Consul, and then attaches it to the <code>public</code> zone. This will allow Consul to communicate with other hosts.</p>
<p>As noted, we only apply this if the <code>os.family</code> fact contains <code>RedHat</code> (i.e. the machine is running a Red Hat-based distribution).</p>
<h3 id="enabling-the-service">Enabling the service</h3>
<p>Finally, we need to enable the Consul service. All distributions in this lab run <code>systemd</code>, so we use the same manifest and template for Debian-based and Red Hat-based distributions.</p>
<h4 id="manifests-1">Manifests</h4>
<p>The <code>service.pp</code> manifest looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">consul</span>::<span style="color:#66d9ef">service</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#66d9ef">service</span> {<span style="color:#e6db74">&#39;consul&#39;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#a6e22e">ensure</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;running&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#a6e22e">enable</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#a6e22e">restart</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;consul reload&#34;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    }<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>When installed via the Hashicorp repositories, Consul includes a <code>systemd</code> unit file. The only interesting part here is the <code>restart =&gt; &quot;consul reload&quot;</code> section. What this says is that rather than restarting the entire service, if another class/manifest triggers a reload/refresh, use the <code>consul reload</code> command rather than restarting the entire service.</p>
<p>In most cases, <code>consul reload</code> is all we need to use. For example, if we add or take away a service (using a Consul HCL or JSON file), a <code>consul reload</code> will discover the changes.. A full restart has the potential to break the Consul cluster, or at least break the connection from Prometheus to Consul for service discovery. Triggering a reload is much safer.</p>
<h2 id="puppet-module-install-the-node_exporter">Puppet Module: Install the <code>node_exporter</code></h2>
<p>Like the <code>consul</code> module, the <code>node_exporter</code> module has multiple manifests and templates too. The structure is as follows: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">/etc/puppetlabs/code$ tree custom/node_exporter/
custom/node_exporter/
├── manifests
│   ├── consul.pp
│   ├── firewall.pp
│   ├── init.pp
│   ├── install.pp
│   ├── prereqs.pp
│   └── service.pp
└── templates
    └── systemd.erb
</code></pre></div><p>Again, we use the <code>init.pp</code> manifest to include other classes: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">node_exporter</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#a6e22e">contain</span> <span style="color:#a6e22e">node_exporter</span>::<span style="color:#a6e22e">prereqs</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#a6e22e">contain</span> <span style="color:#a6e22e">node_exporter</span>::<span style="color:#a6e22e">install</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#a6e22e">contain</span> <span style="color:#a6e22e">node_exporter</span>::<span style="color:#66d9ef">service</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#a6e22e">contain</span> <span style="color:#a6e22e">node_exporter</span>::<span style="color:#a6e22e">firewall</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#a6e22e">contain</span> <span style="color:#a6e22e">node_exporter</span>::<span style="color:#a6e22e">consul</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><h3 id="prerequisites">Prerequisites</h3>
<p>As the latest <code>node_exporter</code> isn&rsquo;t packaged for Debian or Red Hat directly, we must create the users, groups and required directories before we attempt to install the <code>node_exporter</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">node_exporter</span>::<span style="color:#a6e22e">prereqs</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#a6e22e">group</span> { <span style="color:#e6db74">&#39;node_exporter&#39;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">name</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;node_exporter&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#66d9ef">user</span> { <span style="color:#e6db74">&#39;node_exporter&#39;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">name</span>   <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;node_exporter&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">groups</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;node_exporter&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">shell</span>  <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;/sbin/nologin&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#66d9ef">file</span> { [ <span style="color:#e6db74">&#39;/opt/node_exporter&#39;</span>, <span style="color:#e6db74">&#39;/opt/node_exporter/exporters&#39;</span>, <span style="color:#e6db74">&#39;/opt/node_exporter/exporters/dist&#39;</span>, <span style="color:#e6db74">&#39;/opt/node_exporter/exporters/dist/textfile&#39;</span> ]:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">ensure</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;directory&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">owner</span>  <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;node_exporter&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">group</span>  <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;node_exporter&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>The first action is to create the <code>node_exporter</code> group. After this, we create the <code>node_exporter</code> user. Finally, we create the directory structure for the <code>textfile_collector</code> (i.e. where <code>node_exporter</code> will ingest metrics from files in a certain directory).</p>
<p>What you&rsquo;ll notice here is that we are creating each directory in the tree. This is because Puppet does not have a native way of creating a directory and it&rsquo;s parent directories if they do not already exist. To achieve this in Saltstack, you would use: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">/opt/exporters/dist/textfile</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: node_exporter
    - <span style="color:#66d9ef">group</span>: node_exporter
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True
</code></pre></div><p>The <code>makedirs</code> option is what creates the parent directories. In Ansible, we would use something like: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">name</span>: Create a directory if it does not exist
  <span style="color:#66d9ef">ansible.builtin.file</span>:
    <span style="color:#66d9ef">path</span>: /opt/exporters/dist/textfile 
    <span style="color:#66d9ef">state</span>: directory
    <span style="color:#66d9ef">mode</span>: <span style="color:#e6db74">&#39;0755&#39;</span>
</code></pre></div><p>Ansible natively creates the parent directories, so nothing changes here.</p>
<p>At this point, we can now install the <code>node_exporter</code>.</p>
<h3 id="install-the-node_exporter">Install the <code>node_exporter</code></h3>
<p>As no Debian/RPM package is available of the latest <code>node_exporter</code> version, we retrieve the <code>node_exporter</code> tarball file from GitHub: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">node_exporter</span>::<span style="color:#a6e22e">install</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    $install_path   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/usr/local/bin&#34;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    $package_org    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;prometheus&#34;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    $package_name   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;node_exporter&#34;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    $package_type   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;linux-amd64&#34;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    $package_ensure <span style="color:#f92672">=</span> <span style="color:#a6e22e">lookup</span>(<span style="color:#e6db74">&#34;node_exporter.version&#34;</span>)<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    $archive_name   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${package_name}-${package_ensure}.${package_type}.tar.gz&#34;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    $repository_url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://github.com/${package_org}/${package_name}/releases/download&#34;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    $package_source <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${repository_url}/v${package_ensure}/${archive_name}&#34;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#a6e22e">archive</span> { $archive_name:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#a6e22e">path</span>         <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;/tmp/${archive_name}&#34;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#a6e22e">source</span>       <span style="color:#f92672">=&gt;</span> $package_source,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#a6e22e">extract</span>      <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#a6e22e">extract_command</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;tar xfz %s --strip-components=1&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#a6e22e">extract_path</span> <span style="color:#f92672">=&gt;</span> $install_path,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#a6e22e">creates</span>      <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;${install_path}/${package_name}&#34;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#a6e22e">cleanup</span>      <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    }<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>What we can see here is that rather than specifying the full paths and URLs directly in the action, we use variables. This allows us to change the package type (e.g. use a different architecture), the package name, location to download from and more in one place. In the above, the <code>package_name</code> variable is reference three times, so being able to change it once place avoids typos or forgetting to change it everywhere.</p>
<p>The Puppet <code>archive</code> action is able to use a URL as the source directly, rather than needing to download the file with another action first.</p>
<p>One point to note is the <code>extract_command</code> field. This is supplied because otherwise the destination directory in <code>/usr/local/bin</code> would be <code>/usr/local/bin/node_exporter-v1.1.2.linux-amd64/node_exporter</code>. Using the <code>strip-components</code> flag means that the top-level directory inside of the archive is &ldquo;stripped&rdquo;, with the contents of said directory being placed directly in <code>/usr/local/bin</code> instead.</p>
<p>The package version itself is sourced from <strong>hiera</strong> data. In this lab, it looks like this: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">node_exporter</span>:
    <span style="color:#66d9ef">version</span>: <span style="color:#ae81ff">1.1.2</span>
</code></pre></div><h3 id="defining-and-enabling-the-service">Defining and enabling the service</h3>
<p>We use <code>systemd</code> to run the <code>node_exporter</code>, which requires us to create a <code>systemd</code> unit file. The manifest for this is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">node_exporter</span>::<span style="color:#66d9ef">service</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#66d9ef">file</span> { <span style="color:#e6db74">&#39;/etc/systemd/system/node_exporter.service&#39;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">content</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">template</span>(<span style="color:#e6db74">&#39;node_exporter/systemd.erb&#39;</span>),<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">owner</span>   <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;root&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">group</span>   <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;root&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">mode</span>	<span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;644&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#66d9ef">service</span> {<span style="color:#e6db74">&#39;node_exporter&#39;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#a6e22e">ensure</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;running&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#a6e22e">enable</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>This is similar to Consul, except we just need to create the unit file first. The referenced template looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#f92672">[</span>Unit<span style="color:#f92672">]</span>
Description<span style="color:#f92672">=</span>Node Exporter
After<span style="color:#f92672">=</span>network.target

<span style="color:#f92672">[</span>Service<span style="color:#f92672">]</span>
User<span style="color:#f92672">=</span>node_exporter
Group<span style="color:#f92672">=</span>node_exporter
Type<span style="color:#f92672">=</span>simple
ExecStart<span style="color:#f92672">=</span>/usr/local/bin/node_exporter --collector.systemd --collector.wifi --collector.textfile --collector.textfile.directory<span style="color:#f92672">=</span>/opt/exporters/dist/textfile

<span style="color:#f92672">[</span>Install<span style="color:#f92672">]</span>
WantedBy<span style="color:#f92672">=</span>multi-user.target
</code></pre></div><p>There are no template variables in this, but it does give us the option to add them if required in future.</p>
<h3 id="firewall-rules">Firewall rules</h3>
<p>As with Consul, this is for Red Hat-based distributions only, as they come with <code>firewalld</code> enabled by default: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">node_exporter</span>::<span style="color:#a6e22e">firewall</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#66d9ef">if</span> $facts[&#39;os&#39;][<span style="color:#e6db74">&#39;family&#39;</span>] <span style="color:#f92672">==</span> <span style="color:#e6db74">&#39;RedHat&#39;</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">firewalld</span>::<span style="color:#a6e22e">custom_service</span>{<span style="color:#e6db74">&#39;node_exporter&#39;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">short</span>       <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;node_exporter&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">description</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;Prometheus Node Expoter&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">port</span>        <span style="color:#f92672">=&gt;</span> [<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			  {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			      <span style="color:#e6db74">&#39;port&#39;</span>     <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;9100&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			      <span style="color:#e6db74">&#39;protocol&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;tcp&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			  }<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			]<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">firewalld_service</span> { <span style="color:#e6db74">&#39;Allow Node Exporter&#39;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#a6e22e">ensure</span>  <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;present&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#66d9ef">service</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;node_exporter&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#66d9ef">zone</span>    <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;public&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>The <code>node_exporter</code> runs on fewer ports than Consul does, so we only need to allow <code>TCP/9100</code> for Prometheus to be able to scrape metrics from it.</p>
<h3 id="registering-the-service-with-consul">Registering the service with Consul</h3>
<p>Now that the <code>node_exporter</code> is running, we need to register a service with Consul. This service tells Consul what port the <code>node_exporter</code> is listening on and associated tags. This means that rather than needing to tell Prometheus what machines are running <code>node_exporter</code>, Prometheus discovers from Consul what each machine is running. Adding additional exporters and services requires no change on the Prometheus side.</p>
<p>The manifest looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">node_exporter</span>::<span style="color:#a6e22e">consul</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	$consul_service_name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;node_exporter&#34;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	$consul_service_port <span style="color:#f92672">=</span> <span style="color:#ae81ff">9100</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#66d9ef">file</span> { <span style="color:#e6db74">&#34;/etc/consul.d/${consul_service_name}.hcl&#34;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">content</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">template</span>(<span style="color:#e6db74">&#39;consul/consul.prom_service.hcl.erb&#39;</span>),<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">owner</span>   <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;root&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">group</span>   <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;root&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">mode</span>	<span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;644&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#66d9ef">exec</span> {<span style="color:#e6db74">&#39;consul_reload&#39;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">command</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;/usr/bin/consul reload&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#66d9ef">subscribe</span> <span style="color:#f92672">=&gt;</span> [<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>			<span style="color:#66d9ef">File</span>[<span style="color:#e6db74">&#34;/etc/consul.d/${consul_service_name}.hcl&#34;</span>],<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		],<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">refreshonly</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    }<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>In this, we are referencing a template in the Consul module. This is so that rather than needing to have this template exist in every <code>exporter</code> module, we can use a common Consul template instead.</p>
<p>We also have an <code>exec</code> action, which is how arbitrary commands are ran on the machine by Puppet. This has the <code>subscribe</code> option, which says that we will only reload Consul if there is a change in the Consul service file.</p>
<p>The template itself looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet">{<span style="color:#e6db74">&#34;service&#34;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#e6db74">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;&lt;%= @consul_service_name %&gt;&#34;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#e6db74">&#34;tags&#34;</span>: [<span style="color:#e6db74">&#34;&lt;%= @consul_service_name %&gt;&#34;</span>, <span style="color:#e6db74">&#34;prometheus&#34;</span>],<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#e6db74">&#34;port&#34;</span>: <span style="color:#f92672">&lt;</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">@</span><span style="color:#a6e22e">consul_service_port</span> <span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">&gt;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  }<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>As you can see, we are using the Consul service name and port as placeholders in this template. In this case, they are replaced with <code>node_exporter</code> and <code>9100</code>, producing the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{<span style="color:#f92672">&#34;service&#34;</span>:
  {
    <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;node_exporter&#34;</span>,
    <span style="color:#f92672">&#34;tags&#34;</span>: [<span style="color:#e6db74">&#34;node_exporter&#34;</span>, <span style="color:#e6db74">&#34;prometheus&#34;</span>],
    <span style="color:#f92672">&#34;port&#34;</span>: <span style="color:#ae81ff">9100</span>, 
  }
}
</code></pre></div><p>After this runs, the Consul client will register this service with the Consul server. This allows the Prometheus server (and anything else that can use Consul as a source) to discover all available services by only needing to contact the Consul server(s).</p>
<h2 id="puppet-module-prometheus">Puppet Module: Prometheus</h2>
<p>This module is only applied to any node with a hostname that starts <code>prometheus</code>, as seen in the role, profile and <code>site.pp</code> manifests below: -</p>
<p><strong>site.pp</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">node</span> <span style="color:#f92672">/</span><span style="color:#960050;background-color:#1e0010">^</span><span style="color:#a6e22e">prometheus</span><span style="color:#960050;background-color:#1e0010">.</span><span style="color:#f92672">/</span>		{ <span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#66d9ef">include</span> <span style="color:#66d9ef">role</span>::<span style="color:#a6e22e">prometheus</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p><strong>Role - prometheus.pp</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">class</span> <span style="color:#66d9ef">role</span>::<span style="color:#a6e22e">prometheus</span> <span style="color:#66d9ef">inherits</span> <span style="color:#66d9ef">role</span>::<span style="color:#a6e22e">base</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>{<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#66d9ef">include</span> <span style="color:#a6e22e">profile</span>::<span style="color:#a6e22e">prometheus</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>Note here that we are inheriting the base role as well, meaning we also include the MOTD, base package, Consul and <code>node_exporter</code> profiles too.</p>
<p><strong>Profile - prometheus.pp</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">profile</span>::<span style="color:#a6e22e">prometheus</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>{<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#66d9ef">case</span> $facts[<span style="color:#e6db74">&#39;hostname_role&#39;</span>] {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#66d9ef">default</span>: {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>            <span style="color:#66d9ef">include</span> <span style="color:#a6e22e">prometheus</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        }<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    }<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>The structure of the module itself is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ tree custom/prometheus/
custom/prometheus/
├── manifests
│   ├── config.pp
│   ├── init.pp
│   ├── install.pp
│   ├── prereqs.pp
│   └── service.pp
└── templates
    ├── prometheus.yml.erb
    └── systemd.erb
</code></pre></div><p>The structure is very similar to the <code>node_exporter</code> module, except we are not registering any services with Consul.</p>
<p>Again, our <code>init.pp</code> references the other classes and manifests: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">prometheus</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#a6e22e">contain</span> <span style="color:#a6e22e">prometheus</span>::<span style="color:#a6e22e">prereqs</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#a6e22e">contain</span> <span style="color:#a6e22e">prometheus</span>::<span style="color:#a6e22e">install</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#a6e22e">contain</span> <span style="color:#a6e22e">prometheus</span>::<span style="color:#a6e22e">config</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#a6e22e">contain</span> <span style="color:#a6e22e">prometheus</span>::<span style="color:#66d9ef">service</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><h3 id="prerequisites-1">Prerequisites</h3>
<p>The following manifest is very similar to the <code>node_exporter</code> prerequisites manifest: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">prometheus</span>::<span style="color:#a6e22e">prereqs</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#a6e22e">group</span> { <span style="color:#e6db74">&#39;prometheus&#39;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">name</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;prometheus&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#66d9ef">user</span> { <span style="color:#e6db74">&#39;prometheus&#39;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">name</span>   <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;prometheus&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">groups</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;prometheus&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">shell</span>  <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;/sbin/nologin&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#66d9ef">file</span> { [ <span style="color:#e6db74">&#39;/etc/prometheus&#39;</span>, <span style="color:#e6db74">&#39;/etc/prometheus/alerts&#39;</span>, <span style="color:#e6db74">&#39;/etc/prometheus/rules&#39;</span>, <span style="color:#e6db74">&#39;/var/lib/prometheus&#39;</span> ]:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">ensure</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;directory&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">owner</span>  <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;prometheus&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">group</span>  <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;prometheus&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>Again, we create our group and user, and also create some directories for use with Prometheus. The first three directories contain Prometheus&rsquo;s configuration, as well as locations for alerts (for use with Alertmanager) and recording rules. The last directory is where Prometheus stores it&rsquo;s data.</p>
<h3 id="install-prometheus">Install Prometheus</h3>
<p>Again, this manifest is very similar to the <code>node_exporter</code> module: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">prometheus</span>::<span style="color:#a6e22e">install</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    $install_path   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/usr/local/bin&#34;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    $package_name   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;prometheus&#34;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    $package_type   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;linux-amd64&#34;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    $package_ensure <span style="color:#f92672">=</span> <span style="color:#a6e22e">lookup</span>(<span style="color:#e6db74">&#34;prometheus.version&#34;</span>)<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    $archive_name   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${package_name}-${package_ensure}.${package_type}.tar.gz&#34;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    $repository_url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://github.com/${package_name}/${package_name}/releases/download&#34;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    $package_source <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${repository_url}/v${package_ensure}/${archive_name}&#34;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#a6e22e">archive</span> { $archive_name:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#a6e22e">path</span>         <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;/tmp/${archive_name}&#34;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#a6e22e">source</span>       <span style="color:#f92672">=&gt;</span> $package_source,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#a6e22e">extract</span>      <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#a6e22e">extract_command</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;tar xfz %s --strip-components=1&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#a6e22e">extract_path</span> <span style="color:#f92672">=&gt;</span> $install_path,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#a6e22e">creates</span>      <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;${install_path}/${package_name}&#34;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#a6e22e">cleanup</span>      <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>The only difference here is that the <code>package_name</code> and <code>package_org</code> would be the same, so I have eschewed it in favour of using the same variable twice. If in future the organisation changes, then it is quite straightforward to add <code>package_org</code> variable in the <code>repository_url</code> instead.</p>
<h3 id="prometheus-configuration">Prometheus Configuration</h3>
<p>Unlike the <code>node_exporter</code>, Prometheus requires a configuration file to run, so we use the following manifest to create one: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">prometheus</span>::<span style="color:#a6e22e">config</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	$consul_servers <span style="color:#f92672">=</span> <span style="color:#a6e22e">lookup</span>(<span style="color:#e6db74">&#39;consul_vars.servers&#39;</span>)<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#66d9ef">file</span> { <span style="color:#e6db74">&#39;/etc/prometheus/prometheus.yml&#39;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#66d9ef">notify</span>  <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">Service</span>[<span style="color:#e6db74">&#39;prometheus&#39;</span>],<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">content</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">template</span>(<span style="color:#e6db74">&#39;prometheus/prometheus.yml.erb&#39;</span>),<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">owner</span>   <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;prometheus&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">group</span>   <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;prometheus&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">mode</span>	<span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;644&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>In this, we also reference the Consul servers, because we will be using them for service discovery. We also use the <code>notify</code> property to tell Prometheus to reload if the configuration changes.</p>
<p>The configuration template looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#a6e22e">global</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#a6e22e">scrape_interval</span>:     <span style="color:#ae81ff">15</span><span style="color:#a6e22e">s</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#a6e22e">evaluation_interval</span>: <span style="color:#ae81ff">15</span><span style="color:#a6e22e">s</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#a6e22e">alerting</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#a6e22e">alertmanagers</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#f92672">-</span> <span style="color:#a6e22e">static_configs</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#f92672">-</span> <span style="color:#a6e22e">targets</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>      <span style="color:#f92672">-</span> <span style="color:#a6e22e">localhost</span>:<span style="color:#ae81ff">9093</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#a6e22e">rule_files</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#f92672">-</span> <span style="color:#e6db74">&#39;alerts/*.yml&#39;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#f92672">-</span> <span style="color:#e6db74">&#39;rules/*.yml&#39;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#a6e22e">scrape_configs</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#f92672">-</span> <span style="color:#a6e22e">job_name</span>: <span style="color:#e6db74">&#39;prometheus&#39;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#a6e22e">static_configs</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#f92672">-</span> <span style="color:#a6e22e">targets</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>      <span style="color:#f92672">-</span> <span style="color:#e6db74">&#39;localhost:9090&#39;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#f92672">-</span> <span style="color:#a6e22e">job_name</span>: <span style="color:#e6db74">&#39;consul&#39;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#a6e22e">consul_sd_configs</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#f92672">&lt;</span><span style="color:#960050;background-color:#1e0010">%</span> <span style="color:#960050;background-color:#1e0010">@</span><span style="color:#a6e22e">consul_servers</span><span style="color:#960050;background-color:#1e0010">.</span><span style="color:#a6e22e">each</span> <span style="color:#a6e22e">do</span> <span style="color:#f92672">|</span><span style="color:#a6e22e">server</span><span style="color:#f92672">|</span> <span style="color:#f92672">-</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">&gt;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>      <span style="color:#f92672">-</span> <span style="color:#a6e22e">server</span>: <span style="color:#e6db74">&#39;&lt;%= server %&gt;:8500&#39;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#f92672">&lt;</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">-</span> <span style="color:#a6e22e">end</span> <span style="color:#f92672">-</span><span style="color:#960050;background-color:#1e0010">%</span><span style="color:#f92672">&gt;</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>    <span style="color:#a6e22e">relabel_configs</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>      <span style="color:#f92672">-</span> <span style="color:#a6e22e">action</span>: <span style="color:#a6e22e">labelmap</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#a6e22e">regex</span>: <span style="color:#a6e22e">__meta_consul_metadata_</span>(<span style="color:#960050;background-color:#1e0010">.</span><span style="color:#f92672">+</span>)<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>      <span style="color:#f92672">-</span> <span style="color:#a6e22e">source_labels</span>: [<span style="color:#a6e22e">__meta_consul_tags</span>]<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#a6e22e">regex</span>: <span style="color:#960050;background-color:#1e0010">.</span><span style="color:#f92672">*</span>,<span style="color:#a6e22e">prometheus</span>,<span style="color:#960050;background-color:#1e0010">.</span><span style="color:#f92672">*</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#a6e22e">action</span>: <span style="color:#a6e22e">keep</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>      <span style="color:#f92672">-</span> <span style="color:#a6e22e">source_labels</span>: [<span style="color:#a6e22e">__meta_consul_service</span>]<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>        <span style="color:#a6e22e">target_label</span>: <span style="color:#a6e22e">job</span><span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>The two jobs are for Prometheus itself, and <code>consul_sd_configs</code>, which is the job used to discover services from Consul.</p>
<h3 id="defining-and-enabling-the-service-1">Defining and enabling the service</h3>
<p>As with the <code>node_exporter</code>, we need to define the <code>systemd</code> service and run it, as per below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-puppet" data-lang="puppet"><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">prometheus</span>::<span style="color:#66d9ef">service</span> {<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#66d9ef">file</span> { <span style="color:#e6db74">&#39;/etc/systemd/system/prometheus.service&#39;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">content</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">template</span>(<span style="color:#e6db74">&#39;prometheus/systemd.erb&#39;</span>),<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">owner</span>   <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;root&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">group</span>   <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;root&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">mode</span>	<span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;644&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	<span style="color:#66d9ef">service</span> {<span style="color:#e6db74">&#39;prometheus&#39;</span>:<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>		<span style="color:#a6e22e">ensure</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;running&#39;</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>                <span style="color:#a6e22e">enable</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>,<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>	}<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>This references the below template: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#f92672">[</span>Unit<span style="color:#f92672">]</span>
Description<span style="color:#f92672">=</span>Prometheus
Wants<span style="color:#f92672">=</span>network-online.target
After<span style="color:#f92672">=</span>network-online.target

<span style="color:#f92672">[</span>Service<span style="color:#f92672">]</span>
User<span style="color:#f92672">=</span>prometheus
Group<span style="color:#f92672">=</span>prometheus
Type<span style="color:#f92672">=</span>simple
ExecStart<span style="color:#f92672">=</span>/usr/local/bin/prometheus <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    --config.file /etc/prometheus/prometheus.yml <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    --storage.tsdb.path /var/lib/prometheus/ <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    --web.console.templates<span style="color:#f92672">=</span>/etc/prometheus/consoles <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    --web.enable-lifecycle <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    --web.external-url<span style="color:#f92672">=</span>http://prometheus.noisepalace.home <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    --web.console.libraries<span style="color:#f92672">=</span>/etc/prometheus/console_libraries

<span style="color:#f92672">[</span>Install<span style="color:#f92672">]</span>
WantedBy<span style="color:#f92672">=</span>multi-user.target
</code></pre></div><p>As with the <code>node_exporter</code>, we aren&rsquo;t supplying any template variables, but we have the option to if required in future.</p>
<h2 id="running-puppet">Running Puppet</h2>
<p>At this point, the only action we need to take on each machine running the Puppet agent is: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo puppet agent -t
</code></pre></div><p>This will tell Puppet to contact the Puppet server, and run any modules that apply to it. To summarise, for our machines, this is what would apply: -</p>
<table>
<thead>
<tr>
<th>Machine</th>
<th>MOTD</th>
<th>Packages</th>
<th>Consul</th>
<th><code>node_exporter</code></th>
<th>Prometheus</th>
</tr>
</thead>
<tbody>
<tr>
<td>puppetserver</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
<td>❌</td>
</tr>
<tr>
<td>pve</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
<td>❌</td>
</tr>
<tr>
<td>pve2</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
<td>❌</td>
</tr>
<tr>
<td>prometheus</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
</tr>
<tr>
<td>rockyutil</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
<td>❌</td>
</tr>
<tr>
<td>almautil</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
<td>❌</td>
</tr>
<tr>
<td>debutil</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
<td>❌</td>
</tr>
<tr>
<td>ubuntuutil</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
<td>❌</td>
</tr>
</tbody>
</table>
<p>By the end of the run on all of these, you would have: -</p>
<ul>
<li>8 servers running Consul client and the Prometheus <code>node_exporter</code></li>
<li>The Puppet server running as a Consul server as well</li>
<li>The Prometheus server also running Prometheus</li>
</ul>
<p>To show some of this in action, I have put together a video demonstration of running Puppet on all of the &ldquo;util&rdquo; servers: -</p>

<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe src="https://www.youtube.com/embed/y5OlNv5QwLs" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allowfullscreen title="YouTube Video"></iframe>
</div>

<p>This doesn&rsquo;t show Prometheus server or Puppet server being configured, but the steps to allow Puppet to manage them are identical to what is shown in the video.</p>
<h2 id="comparisons-with-ansible-and-saltstack">Comparisons with Ansible and Saltstack</h2>
<p>As noted, I have used Saltstack and Ansible quite extensively in the past, whereas using Puppet is still relatively new for me. However already I have noticed some interesting differences (as well as similarities) with Ansible and Saltstack.</p>
<h3 id="ansible">Ansible</h3>
<p>One of the reasons Ansible was created was so that configuration management could be achieved without needing an <strong>agent</strong>. Puppet, by contrast, relies heavily on the use of agents. This has positives and negatives.</p>
<p>The setup process to get started with Ansible is minimal, in that you install Ansible (from package managers or <code>pip</code>) and then start managing machines straight away. If you have SSH, WinRM or other required access to machines (even API access is enough for some tasks), you can run your Playbooks against them. With Puppet, you need to setup the server, ensure working DNS (otherwise you might encounter issues), install agents on every machine, and then start applying your modules and manifests.</p>
<p>However when it comes to the speed of tasks/events, Puppet is almost always quicker to apply changes in my experience. As you need to run a Puppet server, this will often be placed much closer to your infrastructure than where you are. This means that latency and bandwidth of your connection becomes much less of an issue.</p>
<p>There are caveats to both of the above points, in that there is nothing stopping you from running Ansible playbooks from a server(s) much closer to your infrastructure (or using something like Ansible Tower/AWX), and also Puppet does have an agentless mode (called <a href="https://puppet.com/docs/bolt/latest/bolt.html">Bolt</a>), but in &ldquo;traditional&rdquo; usage of both tools, the above still applies.</p>
<p>The syntax differences are massive, in that Ansible aims to be very human-readable, relying on a YAML-based configuration. Puppet by contrast has some idiosyncrasies to the syntax and approach that do take a while to understand in my experience. As mentioned, if you come from a Ruby background, Puppet will feel quite natural.</p>
<p>Finally, the Ansible ecosystem around managing other systems like BSD (OpenBSD, FreeBSD etc), networking equipment and more is very mature and well supported (the lack of agents being an advantage here). By contrast, Puppet do not package agents directly for the BSDs, and the requirement for running agents does make managing networking hardware much more difficult.</p>
<p>One major advantage of Puppet is it&rsquo;s longevity. There are articles from the early 2000s on some Puppet concepts which are still relevant. Also, as a symptom of nearly two decades of Puppet usage, the reliability of the product is stellar in my experience. The only time I have found Puppet to have issues is due to outside factors. Good examples would be non-synchronised clock sources (meaning certificate validation fails), failing DNS resolution or the hardware is simply not powerful enough to run it. From an ongoing usage and maintenance perspective, I have yet to see any issues with Puppet.</p>
<h3 id="saltstack">Saltstack</h3>
<p>As Saltstack uses agents in a similar way to Puppet, there are fewer major differences between it and Puppet.</p>
<p>One of the biggest differences I have noticed is that there is no requirement for Puppet agents to maintain ongoing communication with the Puppet server. This has the upside in that hosts which are not always available are well supported. Saltstack expects hosts to always be available, meaning that for ad-hoc/hosts intermittently online it will timeout when applying configuration. This can make applying configuration changes quite slow if some of the hosts are timing out, or at least throw a lot of errors when running tasks from the Saltstack Master.</p>
<p>However this also means that because the bulk of the communication in Puppet is controlled by the agent, as far as I am aware there is no mechanism to run ad-hoc commands against all managed hosts, or retrieve data from all managed hosts in real time. The closest you can get with Puppet is viewing facts/catalogs from the last time Puppet made contact with the server. By contrast, running a command against every single Salt-managed machine is as simple as <code>salt '*' cmd.run 'sudo iptables -vnL'</code>, and you can get node-specific facts using <code>salt '*' grains.items</code>.</p>
<p>As with Ansible, the syntax differences between Saltstack and Puppet are quite large. Saltstack (like Ansible) uses YAML-based configuration, but with the added ability to dynamically generate tasks with Jinja2, rather than just using Jinja2 inside of templates. For example, you can build a task in Saltstack like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>] is defined %}
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;prometheus_services&#39;</span>] is defined %}
{% for service in pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;prometheus_services&#39;</span>] %}
/etc/consul.d/{{ <span style="color:#66d9ef">service }}.hcl</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://consul/services/files/{{ service }}.hcl
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0640</span>
    - <span style="color:#66d9ef">template</span>: jinja

consul_reload_{{ <span style="color:#66d9ef">service }}</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: consul reload
    - <span style="color:#66d9ef">watch</span>:
      - <span style="color:#66d9ef">file</span>: /etc/consul.d/{{ service }}.hcl
{% endfor %}
{% endif %}
{% endif %}
</code></pre></div><p>This means that we have a dynamically-generated task based upon pillar data (pillar data is very similar to Hiera, for those more familiar with Puppet), meaning that there is less repetition in your Saltstack state files.</p>
<p>As far as I am aware, this is not possible with Puppet. However it is also not possible with Ansible either, so this is more of a general Saltstack advantage rather than specifically a difference to Puppet.</p>
<p>Again, like Puppet, Saltstack can be ran without an agent. However the &ldquo;traditional&rdquo; installation approach tends to use a central Salt server (called a <strong>master</strong>) with agents (<strong>minions</strong>) registered with it.</p>
<p>The support across operating systems with Saltstack is also quite good, and it has also become increasingly popular in the networking community as well (see Mircea Ulnic&rsquo;s <a href="https://mirceaulinic.net/">blog</a> for more on this).</p>
<h2 id="summary">Summary</h2>
<p>Puppet is a great configuration management tool, and given it has been around for nearly two decades as well, it is also incredibly mature at this stage too.</p>
<p>If you are in the process of choosing a configuration management tool, Puppet is definitely worth your consideration. While the syntax is very different from tools like Ansible and Saltstack, this doesn&rsquo;t necessarily make it more difficult, just different.</p>
<p>In future posts, I will show how to integrate Puppet with Cloud-Init for managing cloud-based infrastructure, as well as managing operating systems that are not Linux too!</p>
<p>For all the Puppet code in this blog, see my <a href="https://gitlab.com/stuh84/puppet-lab">Puppet Lab</a> repository.</p>
]]></content>
        </item>
        
        <item>
            <title>Configuring BGP Anycast on Equinix Metal using Pulumi and Saltstack</title>
            <link>https://yetiops.net/posts/pulumi-saltstack-equinix-anycast/</link>
            <pubDate>Sat, 15 May 2021 16:17:13 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/pulumi-saltstack-equinix-anycast/</guid>
            <description>If you have been following my previous posts for a while, you are likely to have seen posts on Saltstack, and more recently Pulumi. You may also be aware that I come from a networking background, meaning I&amp;rsquo;ll often deep dive on topics like BGP, anycast and routing protocols.
Around 18 months ago, I put together a post on providing anycast services with BGP. It covered what anycast is, how to provide anycast-based services with BGP, and also how to conditionally advertise your anycast IPs using ExaBGP.</description>
            <content type="html"><![CDATA[<p>If you have been following my previous posts for a while, you are likely to have seen posts on <a href="/tags/saltstack/">Saltstack</a>, and more recently <a href="/tags/pulumi">Pulumi</a>. You may also be aware that I come from a networking background, meaning I&rsquo;ll often deep dive on topics like BGP, anycast and routing protocols.</p>
<p>Around 18 months ago, I put together a post on providing <a href="/posts/anycast-bgp/">anycast services with BGP</a>. It covered what anycast is, how to provide anycast-based services with BGP, and also how to conditionally advertise your anycast IPs using <a href="https://github.com/Exa-Networks/exabgp">ExaBGP</a>.</p>
<p>This post will take it a step further, by using <a href="https://pulumi.com">Pulumi</a> to provision servers and BGP sessions on <a href="https://metal.equinix.com">Equinix Metal</a>, and <a href="https://saltproject.io/">Saltstack</a> that will configure the servers to advertise routes via BGP to Equinix.</p>
<h2 id="equinix-metal">Equinix Metal</h2>
<p>As mentioned in my <a href="/posts/pulumi-more-providers/#equinix-metal">previous post</a> using Pulumi, Equinix Metal are a provider of bare metal servers and infrastructure. For those who need dedicated machines with no contention over CPU/memory/storage on the server, they are able to provision them with the same tooling that others would use to create a VPS/instance on other cloud providers (e.g. AWS, Digital Ocean, Linode).</p>
<p>I recently guested on a stream with <a href="https://twitter.com/rawkode">David McKay/Rawkode</a>, a developer advocate at Equinix Metal going through some of the networking options available when using their infrastructure. While we didn&rsquo;t get into configuring BGP, you can at least get an idea of what Equinix Metal is like as a cloud/infrastructure provider: -</p>

<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe src="https://www.youtube.com/embed/3bkIN8TrNJE" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allowfullscreen title="YouTube Video"></iframe>
</div>

<p>A big point to take away from this is that we are able to configure BGP within Equinix Metal, and advertise routes from the servers we create. This option isn&rsquo;t always available in other cloud/infrastructure providers, or at minimum requiring your own IP space.</p>
<h2 id="anycast-with-exabgp">Anycast with ExaBGP</h2>
<p>As mentioned in the introduction, I have covered using ExaBGP for Anycast before. The aim of this post is very similar to the previous Anycast post, except that there will be no user interaction involved (other than typing <code>pulumi up</code>).</p>
<p>We won&rsquo;t go into great depth about how ExaBGP works, as this is already covered <a href="/posts/anycast-bgp/#how-do-we-track-the-dns-software">here</a>.</p>
<h2 id="saltstack">Saltstack</h2>
<p>In this post we will cover the Saltstack states and pillars to configure our servers, and the use of <a href="https://docs.saltproject.io/en/latest/topics/grains/index.html">grains</a> to retrieve information from the servers that we provision.</p>
<p>For the impatient among you (I&rsquo;m one of them!), you can view all of these in <a href="https://gitlab.com/stuh84/salt-anycast-equinix">this</a> repository.</p>
<p>As a brief refresher, or for those who are used to other configuration management systems, the following terms are used in Saltstack: -</p>
<ul>
<li><strong>states</strong> - They contain the tasks that need to run. Similar to Ansible Playbooks, Puppet Manifests or Chef Cookbooks</li>
<li><strong>pillars</strong> - These are variables which can be used in our states and templates. These are similar to Ansible&rsquo;s host variables</li>
<li><strong>grains</strong> - These are per-node facts, that are derived from the hosts themselves (e.g. host operating system, IP addresses etc)
<ul>
<li>You can also set custom grains which will can then be referenced in your <strong>state</strong> or <strong>pillars</strong></li>
</ul>
</li>
<li><strong>minions</strong> - Hosts running the Salt Minion agent, that register with the master</li>
</ul>
<h3 id="states">States</h3>
<p>In our states directory, we have the following <strong>top</strong> file (i.e. the file which determines what states apply to what nodes): -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">base</span>:
  <span style="color:#66d9ef">&#39;G@os:Ubuntu or G@os:Debian&#39;</span>:
    - <span style="color:#66d9ef">match</span>: compound
    - base

  <span style="color:#66d9ef">&#39;bgp-*&#39;</span>:
    - pip
    - network
    - exabgp
    - nginx
</code></pre></div><p>The first section installs some base packages on any minion running Ubuntu or Debian. None of the packages are relevant to the anycast deployment (other than <code>tcpdump</code>, if you want to run packet captures to prove where traffic is being forwarded).</p>
<p>The second section matches nodes with a <strong>nodename</strong> (often the same as the <strong>hostname</strong>, but not a requirement) that starts with <strong>bgp</strong>. If a node is called <code>bgp-paris</code> or <code>bgp-01</code> or <code>bgp-meshuggah</code>, they would all be matched by this. However a node called <code>more-bgp-01</code> wouldn&rsquo;t match.</p>
<h4 id="the-pip-state">The <code>pip</code> state</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">python3-pip</span>:
 pkg.installed
</code></pre></div><p>As you can see, this just installs the <code>python3-pip</code> package. Without this, we cannot install ExaBGP (as it is packaged as a Python PIP module).</p>
<h4 id="the-network-state">The <code>network</code> state</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">{% for interface in pillar[<span style="color:#e6db74">&#39;network&#39;</span>][<span style="color:#e6db74">&#39;interfaces&#39;</span>] %}
{{ <span style="color:#66d9ef">interface[&#39;name&#39;] }}</span>:
  <span style="color:#66d9ef">network.managed</span>:
    - <span style="color:#66d9ef">enabled</span>: True
    {%- if interface[<span style="color:#e6db74">&#39;type&#39;</span>] is defined %}
    - <span style="color:#66d9ef">type</span>: {{ interface[<span style="color:#e6db74">&#39;type&#39;</span>] }}
    {%- endif %}
    {%- if interface[<span style="color:#e6db74">&#39;proto&#39;</span>] is defined %}
    - <span style="color:#66d9ef">proto</span>: {{ interface[<span style="color:#e6db74">&#39;proto&#39;</span>] }}
    {%- endif %}
    - <span style="color:#66d9ef">ipaddr</span>: {{ interface[<span style="color:#e6db74">&#39;ipv4&#39;</span>][<span style="color:#e6db74">&#39;address&#39;</span>] }}
    - <span style="color:#66d9ef">netmask</span>: {{ interface[<span style="color:#e6db74">&#39;ipv4&#39;</span>][<span style="color:#e6db74">&#39;mask&#39;</span>] }}
    {%- if interface[<span style="color:#e6db74">&#39;ipv4&#39;</span>][<span style="color:#e6db74">&#39;gateway&#39;</span>] is defined %}
    - <span style="color:#66d9ef">gateway</span>: {{ interface[<span style="color:#e6db74">&#39;ipv4&#39;</span>][<span style="color:#e6db74">&#39;gateway&#39;</span>] }}
    {%- endif %}
    {%- if interface[<span style="color:#e6db74">&#39;ipv4&#39;</span>][<span style="color:#e6db74">&#39;nameservers&#39;</span>] is defined %}
    - <span style="color:#66d9ef">dns</span>:
      {% for ns in interface[<span style="color:#e6db74">&#39;ipv4&#39;</span>][<span style="color:#e6db74">&#39;nameservers&#39;</span>] %}
      - {{ ns }}
      {% endfor %}
    {%- endif %}
{%- if interface[<span style="color:#e6db74">&#39;routes&#39;</span>] is defined %}
routes_{{ <span style="color:#66d9ef">interface[&#39;name&#39;] }}</span>:
  <span style="color:#66d9ef">network.routes</span>:
    - <span style="color:#66d9ef">name</span>: {{ interface[<span style="color:#e6db74">&#39;name&#39;</span>] }}
    - <span style="color:#66d9ef">routes</span>:
       {%- for route in interface[<span style="color:#e6db74">&#39;routes&#39;</span>] %}
       - <span style="color:#66d9ef">name</span>: {{ route[<span style="color:#e6db74">&#39;name&#39;</span>] }}
         <span style="color:#66d9ef">ipaddr</span>: {{ route[<span style="color:#e6db74">&#39;prefix&#39;</span>] }}
         <span style="color:#66d9ef">netmask</span>: {{ route[<span style="color:#e6db74">&#39;mask&#39;</span>] }}
         <span style="color:#66d9ef">gateway</span>: {{ route[<span style="color:#e6db74">&#39;gateway&#39;</span>] }}
       {%- endfor %}
{%- endif %}
{% endfor %} 
</code></pre></div><p>While this initially looks quite complex, it is mostly <strong>if</strong> statements on whether to supply a value for each field in the the Saltstack <code>network.managed</code> module to create network interfaces. Not every field needs to be defined, or should (e.g. a loopback interface doesn&rsquo;t need a default gateway or a nameserver).</p>
<p>We can add static routes as well, if we want traffic to take a different path than the default gateway.</p>
<h4 id="the-exabgp-state">The <code>exabgp</code> state</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">exabgp</span>:
  pip.installed

<span style="color:#66d9ef">/etc/exabgp</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/etc/exabgp/scripts</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

{% if not salt[<span style="color:#e6db74">&#39;file.is_fifo&#39;</span>](<span style="color:#e6db74">&#39;/var/run/exabgp.in&#39;</span>) %}
<span style="color:#66d9ef">exabgp_in_fifo</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: mkfifo /var/run/exabgp.in
{% endif %}

{% if not salt[<span style="color:#e6db74">&#39;file.is_fifo&#39;</span>](<span style="color:#e6db74">&#39;/var/run/exabgp.out&#39;</span>) %}
<span style="color:#66d9ef">exabgp_out_fifo</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: mkfifo /var/run/exabgp.out
{% endif %}

<span style="color:#66d9ef">/etc/systemd/system/exabgp.service</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://exabgp/files/exabgp.service.j2
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0644</span>
    - <span style="color:#66d9ef">template</span>: jinja

<span style="color:#66d9ef">/etc/exabgp/exabgp.conf</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://exabgp/files/exabgp.conf.j2
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0644</span>
    - <span style="color:#66d9ef">template</span>: jinja

<span style="color:#66d9ef">exabgp_scripts</span>:
  <span style="color:#66d9ef">file.recurse</span>:
    - <span style="color:#66d9ef">name</span>: /etc/exabgp/scripts
    - <span style="color:#66d9ef">source</span>: salt://exabgp/scripts
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root
    - <span style="color:#66d9ef">file_mode</span>: <span style="color:#ae81ff">0755</span>
    - <span style="color:#66d9ef">template</span>: jinja

<span style="color:#66d9ef">exabgp_systemd_reload</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: systemctl daemon-reload
    - <span style="color:#66d9ef">onchanges</span>:
      - <span style="color:#66d9ef">file</span>: /etc/systemd/system/exabgp.service

<span style="color:#66d9ef">exabgp.service</span>:
  <span style="color:#66d9ef">service.running</span>:
  - <span style="color:#66d9ef">enable</span>: True
  - <span style="color:#66d9ef">reload</span>: True
  - <span style="color:#66d9ef">restart</span>: True
  - <span style="color:#66d9ef">watch</span>:
      - <span style="color:#66d9ef">file</span>: /etc/exabgp/exabgp.conf
      - <span style="color:#66d9ef">file</span>: /etc/systemd/system/exabgp.service
      - <span style="color:#66d9ef">file</span>: exabgp_scripts
</code></pre></div><p>There are a few parts to this: -</p>
<ul>
<li>We install ExaBGP using Python&rsquo;s PIP</li>
<li>We create the <code>/etc/exabgp</code> and <code>/etc/exabgp/scripts</code> directory
<ul>
<li>The base directory contains global ExaBGP configuration</li>
<li>The scripts directory contains check scripts that test if applications are functioning correctly</li>
</ul>
</li>
<li>We create named pipes using <code>mkfifo</code> that will be used by ExaBGP itself for communication</li>
<li>We create a SystemD unit file to manage the startup/shutdown of ExaBGP</li>
<li>We create the <code>exabgp.conf</code> file with all of the necessary ExaBGP configuration (including BGP peers)</li>
<li>We make sure to run a <code>daemon-reload</code> on the ExaBGP service if the ExaBGP service file changes (ensuring we are running on the most recent version of the service)</li>
<li>Finally, we make sure the service is enabled, reloaded and restarted if any of the ExaBGP files change (i.e. service, configuration or scripts)</li>
</ul>
<p>Unfortunately we must perform a restart on ExaBGP if the scripts or configuration changes, as it doesn&rsquo;t pick up the changes automatically.</p>
<p>The SystemD unit file is found in <code>files/exabgp.service.j2</code>, and looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[Unit]
Description=ExaBGP
After=network.target
ConditionPathExists=/etc/exabgp/exabgp.conf

[Service]
Environment=exabgp_daemon_daemonize=false
Environment=ETC=/etc
ExecStart=/usr/local/bin/exabgp /etc/exabgp/exabgp.conf
ExecReload=/bin/kill -USR1 $MAINPID

[Install]
WantedBy=multi-user.target
</code></pre></div><p>The <code>files/exabgp.conf.j2</code> file looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">process announce-routes {
  run /etc/exabgp/scripts/{{ pillar[<span style="color:#e6db74">&#39;exabgp&#39;</span>][<span style="color:#e6db74">&#39;check_script&#39;</span>][<span style="color:#e6db74">&#39;name&#39;</span>] }};
  encoder {{ pillar[<span style="color:#e6db74">&#39;exabgp&#39;</span>][<span style="color:#e6db74">&#39;check_script&#39;</span>][<span style="color:#e6db74">&#39;encoder&#39;</span>] }};
}

{% for peer in pillar[<span style="color:#e6db74">&#39;exabgp&#39;</span>][<span style="color:#e6db74">&#39;peers&#39;</span>] %}
neighbor {{ peer[<span style="color:#e6db74">&#39;ip&#39;</span>] }} {
    local-address {{ grains[<span style="color:#e6db74">&#39;bgp&#39;</span>][<span style="color:#e6db74">&#39;localip&#39;</span>] }};
    local-as {{ pillar[<span style="color:#e6db74">&#39;exabgp&#39;</span>][<span style="color:#e6db74">&#39;asn&#39;</span>] }};
    peer-as {{ peer[<span style="color:#e6db74">&#39;asn&#39;</span>] }}; 

    api {
        processes [ announce-routes ];
   }

}
{% endfor %}
</code></pre></div><p>In this, we define the process (<code>announce-routes</code>) and which script controls the route announcements and withdrawals. We also specify the <code>encoder</code>. This is either <code>text</code> or <code>json</code>, depending on the format of the message returned by the script.</p>
<p>The last section creates the BGP peering sessions. It loops over a list of peers in the pillars for the node, creating a <code>neighbor</code> statement with the local autonomous system number, Equinix&rsquo;s autonomous system number, and also the local address. This is sourced from the node&rsquo;s <strong>grains</strong>, which will covered in more detail in the Pulumi section.</p>
<p>We also have our scripts, which look like the below: -</p>
<p><strong>scripts/nginx-check.sh</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/bin/bash
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">while</span> true; <span style="color:#66d9ef">do</span>
  curl -s localhost:80 &gt; /dev/null;
  <span style="color:#66d9ef">if</span> <span style="color:#f92672">[[</span> $? !<span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
          <span style="color:#f92672">{</span>%- <span style="color:#66d9ef">for</span> ip in grains<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;anycast&#39;</span><span style="color:#f92672">][</span><span style="color:#e6db74">&#39;ipv4&#39;</span><span style="color:#f92672">]</span> %<span style="color:#f92672">}</span>
          echo <span style="color:#e6db74">&#34;withdraw route {{ ip[&#39;address&#39;] }} next-hop {{ grains[&#39;bgp&#39;][&#39;localip&#39;] }}\n&#34;</span>
          <span style="color:#f92672">{</span>%- endfor %<span style="color:#f92672">}</span>
  <span style="color:#66d9ef">else</span>
          <span style="color:#f92672">{</span>%- <span style="color:#66d9ef">for</span> ip in grains<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;anycast&#39;</span><span style="color:#f92672">][</span><span style="color:#e6db74">&#39;ipv4&#39;</span><span style="color:#f92672">]</span> %<span style="color:#f92672">}</span>
          echo <span style="color:#e6db74">&#34;announce route {{ ip[&#39;address&#39;] }} next-hop {{ grains[&#39;bgp&#39;][&#39;localip&#39;] }}\n&#34;</span>
          <span style="color:#f92672">{</span>%- endfor %<span style="color:#f92672">}</span>
  <span style="color:#66d9ef">fi</span>
  sleep <span style="color:#ae81ff">5</span>
<span style="color:#66d9ef">done</span>
</code></pre></div><p><strong>scripts/dns-check.sh</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/bin/bash
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">while</span> true; <span style="color:#66d9ef">do</span>
  /usr/bin/dig yetiops.net @127.0.0.1 &gt; /dev/null;
  <span style="color:#66d9ef">if</span> <span style="color:#f92672">[[</span> $? !<span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
	  <span style="color:#f92672">{</span>%- <span style="color:#66d9ef">for</span> ip in grains<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;anycast&#39;</span><span style="color:#f92672">][</span><span style="color:#e6db74">&#39;ipv4&#39;</span><span style="color:#f92672">]</span> %<span style="color:#f92672">}</span>
          echo <span style="color:#e6db74">&#34;withdraw route {{ ip[&#39;address&#39;] }} next-hop {{ grains[&#39;bgp&#39;][&#39;localip&#39;] }}\n&#34;</span>
	  <span style="color:#f92672">{</span>%- endfor %<span style="color:#f92672">}</span>
  <span style="color:#66d9ef">else</span>
	  <span style="color:#f92672">{</span>%- <span style="color:#66d9ef">for</span> ip in grains<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;anycast&#39;</span><span style="color:#f92672">][</span><span style="color:#e6db74">&#39;ipv4&#39;</span><span style="color:#f92672">]</span> %<span style="color:#f92672">}</span>
          echo <span style="color:#e6db74">&#34;announce route {{ ip[&#39;address&#39;] }} next-hop {{ grains[&#39;bgp&#39;][&#39;localip&#39;] }}\n&#34;</span>
	  <span style="color:#f92672">{</span>%- endfor %<span style="color:#f92672">}</span>
  <span style="color:#66d9ef">fi</span>
  sleep <span style="color:#ae81ff">5</span>
<span style="color:#66d9ef">done</span>
</code></pre></div><p>Both are very similar, in that they check the status of a command (either doing a cURL request against the local webserver, or a dig request against the local DNS server). The commands run inside of a <code>while</code> loop, making sure they are continuously evaluated. On every iteration, we check to see if the exit code of the command is not <code>0</code> (i.e. did the command fail).</p>
<p>If the command succeeded (e.g. the cURL request returned a page, or the DNS request returned an A record) then we return the text <code>announce route $ROUTE next-hop $LOCAL-IP</code> for each route we want to announce. If the command fails (e.g. the cURL request was refused, or the DNS request returned no result) then we return the text <code>withdraw route $ROUTE next-hop $LOCAL-IP</code> for each route to want to stop announcing.</p>
<p>Finally, we sleep for 5 seconds between each check, otherwise we may risk overloading ExaBGP with route announcements/withdrawals.</p>
<p>The key point here is that we are using <strong>grains</strong> rather than pillars. We will see in the Pulumi section why this is relevant, but in summary this is so that the code is reusable and not tied to specific IPs, and doesn&rsquo;t require updating Salt pillars manually after provisioning our machines.</p>
<h4 id="the-nginx-state">The <code>nginx</code> state</h4>
<p>Finally, we have the <code>nginx</code> state: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">nginx</span>:
  pkg.installed

<span style="color:#66d9ef">/etc/nginx/nginx.conf</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://nginx/files/nginx.conf.j2
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root
    - <span style="color:#66d9ef">mode</span>: <span style="color:#e6db74">&#39;0644&#39;</span>
    - <span style="color:#66d9ef">template</span>: jinja

<span style="color:#66d9ef">/etc/nginx/sites-available/default</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://nginx/files/default.j2
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root
    - <span style="color:#66d9ef">mode</span>: <span style="color:#e6db74">&#39;0644&#39;</span>
    - <span style="color:#66d9ef">template</span>: jinja

<span style="color:#66d9ef">nginx.service</span>:
  <span style="color:#66d9ef">service.running</span>:
    - <span style="color:#66d9ef">watch</span>:
      - <span style="color:#66d9ef">file</span>: /etc/nginx/nginx.conf
      - <span style="color:#66d9ef">file</span>: /etc/nginx/sites-available/default

<span style="color:#66d9ef">/var/www/html/index.nginx-debian.html</span>:
  file.absent

<span style="color:#66d9ef">/var/www/html/index.html</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://nginx/files/index.html.j2
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root
    - <span style="color:#66d9ef">mode</span>: <span style="color:#e6db74">&#39;0644&#39;</span>
    - <span style="color:#66d9ef">template</span>: jinja
</code></pre></div><p>In this state, we do the following: -</p>
<ul>
<li>We install <code>nginx</code></li>
<li>We update the <code>nginx.conf</code> configuration file using a template</li>
<li>We update the <code>default</code> server configuration using a template</li>
<li>We make sure <code>nginx</code> is running
<ul>
<li>We also watch for changes in the configuration file or default server file, reloading if they do change</li>
</ul>
</li>
<li>We remove the default Debian <code>nginx</code> index file</li>
<li>We place our own <code>index.html</code> file using a template</li>
</ul>
<p>The <code>files/nginx.conf.j2</code> file looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
	worker_connections 768;
	# multi_accept on;
}

http {

	##
	# Basic Settings
	##

	sendfile on;
	tcp_nopush on;
	tcp_nodelay on;
	keepalive_timeout 65;
	types_hash_max_size 2048;
	# server_tokens off;

	# server_names_hash_bucket_size 64;
	# server_name_in_redirect off;

	include /etc/nginx/mime.types;
	default_type application/octet-stream;

	##
	# SSL Settings
	##

	ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
	ssl_prefer_server_ciphers on;

	##
	# Logging Settings
	##

	access_log /var/log/nginx/access.log;
	error_log /var/log/nginx/error.log;

	##
	# Gzip Settings
	##

	gzip on;

	# gzip_vary on;
	# gzip_proxied any;
	# gzip_comp_level 6;
	# gzip_buffers 16 8k;
	# gzip_http_version 1.1;
	# gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

	##
	# Virtual Host Configs
	##

	include /etc/nginx/conf.d/*.conf;
	include /etc/nginx/sites-enabled/*;
}
</code></pre></div><p>The <code>files/default.j2</code> file looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">server {
        listen 127.0.0.1:80;
        {%- for ip in grains[&#39;anycast&#39;][&#39;ipv4&#39;] %}
	listen {{ ip[&#39;address&#39;] }}:80;
        {%- endfor %}
	listen [::]:80 default_server;

	root /var/www/html;

	server_name _;

	location / {
		# First attempt to serve request as file, then
		# as directory, then fall back to displaying a 404.
		try_files $uri $uri/ =404;
	}
}
</code></pre></div><p>In this, we ensure that the default site listens on <code>127.0.0.1:80</code> (<code>localhost</code>, for our ExaBGP check script) and also on our anycast IPs.</p>
<p>Last, we have our <code>files/index.html.j2</code> file: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-html" data-lang="html"><span style="color:#75715e">&lt;!DOCTYPE html&gt;</span>
&lt;<span style="color:#f92672">html</span>&gt;
&lt;<span style="color:#f92672">head</span>&gt;
&lt;<span style="color:#f92672">title</span>&gt;Welcome to nginx!&lt;/<span style="color:#f92672">title</span>&gt;
&lt;<span style="color:#f92672">style</span>&gt;
    <span style="color:#f92672">body</span> {
        <span style="color:#66d9ef">width</span>: <span style="color:#ae81ff">35</span><span style="color:#66d9ef">em</span>;
        <span style="color:#66d9ef">margin</span>: <span style="color:#ae81ff">0</span> <span style="color:#66d9ef">auto</span>;
        <span style="color:#66d9ef">font-family</span>: Tahoma, Verdana, Arial, <span style="color:#66d9ef">sans-serif</span>;
    }
&lt;/<span style="color:#f92672">style</span>&gt;
&lt;/<span style="color:#f92672">head</span>&gt;
&lt;<span style="color:#f92672">body</span>&gt;
	&lt;<span style="color:#f92672">h1</span>&gt;Welcome to nginx on {{ grains[&#39;nodename&#39;] }}!&lt;/<span style="color:#f92672">h1</span>&gt;
&lt;<span style="color:#f92672">p</span>&gt;If you see this page, the nginx web server is successfully installed and
working serving from {{ grains[&#39;nodename&#39;] }}. Further configuration is required.&lt;/<span style="color:#f92672">p</span>&gt;

&lt;<span style="color:#f92672">p</span>&gt;For online documentation and support please refer to
&lt;<span style="color:#f92672">a</span> <span style="color:#a6e22e">href</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;http://nginx.org/&#34;</span>&gt;nginx.org&lt;/<span style="color:#f92672">a</span>&gt;.&lt;<span style="color:#f92672">br</span>/&gt;
Commercial support is available at
&lt;<span style="color:#f92672">a</span> <span style="color:#a6e22e">href</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;http://nginx.com/&#34;</span>&gt;nginx.com&lt;/<span style="color:#f92672">a</span>&gt;.&lt;/<span style="color:#f92672">p</span>&gt;

&lt;<span style="color:#f92672">p</span>&gt;&lt;<span style="color:#f92672">em</span>&gt;Thank you for using nginx.&lt;/<span style="color:#f92672">em</span>&gt;&lt;/<span style="color:#f92672">p</span>&gt;
&lt;/<span style="color:#f92672">body</span>&gt;
&lt;/<span style="color:#f92672">html</span>&gt;
</code></pre></div><p>This is the standard <code>nginx</code> index page, but with our <code>nodename</code> displayed. This allows us to see which node our traffic to the anycast IP is forwarded to.</p>
<h3 id="pillars">Pillars</h3>
<p>Now that we have our states defined, we can cover our pillars.</p>
<p>The <code>top.sls</code> file for our pillars looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">base</span>:
  <span style="color:#66d9ef">&#39;bgp-*&#39;</span>:
    - exabgp.bgp
    - bind.bgp
    - network.bgp
</code></pre></div><p>While we aren&rsquo;t using <code>bind</code> in this post, the states and pillars exist for it in our repository so that you can change between serving <code>nginx</code> and running anycast DNS.</p>
<h4 id="the-exabgp-pillar">The <code>exabgp</code> pillar</h4>
<p>In the <code>exabgp/bgp.sls</code> file, we have the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">exabgp</span>:
  <span style="color:#66d9ef">asn</span>: <span style="color:#ae81ff">65000</span>
  <span style="color:#66d9ef">check_script</span>:
    <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;nginx-check.sh&#34;</span>
    <span style="color:#66d9ef">encoder</span>: text
  <span style="color:#66d9ef">peers</span>:
    - <span style="color:#66d9ef">ip</span>: <span style="color:#ae81ff">169.254.255.1</span> 
      <span style="color:#66d9ef">asn</span>: <span style="color:#ae81ff">65530</span> 
    - <span style="color:#66d9ef">ip</span>: <span style="color:#ae81ff">169.254.255.2</span> 
      <span style="color:#66d9ef">asn</span>: <span style="color:#ae81ff">65530</span> 
</code></pre></div><p>Why are we hardcoding the peer values? This is because when you use Equinix Metal, you always peer with the same IPs no matter where you provision your servers. Therefore we do not need to derive what IPs we will peer with at runtime, as it is always the same.</p>
<p>Also, when using the <a href="https://metal.equinix.com/developers/docs/networking/local-global-bgp">Local BGP</a> option with Equinix Metal, you always use the autonomous system number <strong>65000</strong> for your BGP process, and the Equinix Metal BGP sessions will always use the autonomous system number <strong>65530</strong>.</p>
<p>This makes it quite straightforward in terms of what values we need to derive when creating our BGP sessions, as we already know most of them before they are even provisioned.</p>
<p>We also define our <code>check_script</code>, in this case the <code>nginx-check.sh</code> script, as we are using <code>nginx</code> to prove the anycast functionality.</p>
<h4 id="the-network-pillar">The <code>network</code> pillar</h4>
<p>In the <code>network/bgp.sls</code> file, we have the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">network</span>:
  <span style="color:#66d9ef">interfaces</span>:
{% for ip in grains[<span style="color:#e6db74">&#39;anycast&#39;</span>][<span style="color:#e6db74">&#39;ipv4&#39;</span>] %}
    - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;lo:{{ loop.index }}&#34;</span>
      <span style="color:#66d9ef">type</span>: eth
      <span style="color:#66d9ef">proto</span>: static
      <span style="color:#66d9ef">ipv4</span>:
        <span style="color:#66d9ef">address</span>: {{ ip[<span style="color:#e6db74">&#39;address&#39;</span>] }}
        <span style="color:#66d9ef">mask</span>: {{ ip[<span style="color:#e6db74">&#39;mask&#39;</span>] }}
{% endfor %}
</code></pre></div><p>This is used to discover values from the node <strong>grains</strong>. We could also do directly in the state files (i.e. referring to the grains in templates/state files). However using the above allows us to set additional interfaces not derived from grains, and the states don&rsquo;t need to care whether the values are originally <strong>grains</strong> or manually set in pillars.</p>
<h2 id="pulumi">Pulumi</h2>
<p>Now that we have our Salt states and pillars defined, we can create the following: -</p>
<ul>
<li>Two servers that will serve <code>nginx</code>, and advertise anycast IPs to Equinix</li>
<li>A single Salt Master for the above servers to register with
<ul>
<li>The Salt Master will then configure the other two servers with ExaBGP and <code>nginx</code></li>
</ul>
</li>
</ul>
<p>If you want to see the Pulumi code first, go <a href="https://gitlab.com/stuh84/pulumi-equinix-metal/-/tree/main/anycast-bgp">here</a>.</p>
<p>If you haven&rsquo;t used Pulumi before, or Equinix Metal, then see my <a href="/posts/pulumi-iac-terraform/#pulumi-using-go">Pulumi Introduction post</a> and the Equinix Metal section of my <a href="/posts/pulumi-more-providers/#equinix-metal">Pulumi with other providers post</a>.</p>
<h3 id="steps-required">Steps required</h3>
<p>As a brief summary, we need to do the following with our Pulumi code: -</p>
<ul>
<li>Create an Equinix Metal project, and enable BGP on the project</li>
<li>Add an SSH key to the project</li>
<li>Create a server running as a Salt Master, using <code>cloud-init</code> for first-time boot configuration</li>
<li>Reserve a single Equinix-owned IPv4 address that we can use as our anycast IP
<ul>
<li>I don&rsquo;t own any IPv4 or IPv6 blocks, hence the need to use Equinix&rsquo;s IP space</li>
</ul>
</li>
<li>Create two servers that will serve <code>nginx</code>, and advertise our anycast IPv4 address back to Equinix (so they know which server(s) to send it to)</li>
<li>Create Equinix IPv4 BGP sessions to each server, otherwise we have nothing to peer with!</li>
</ul>
<p>Equinix Metal doesn&rsquo;t provide firewalling or anything similar, meaning that if you want to secure your systems you can either: -</p>
<ul>
<li>Run host-based firewalling</li>
<li>Provision Equinix Metal machines that serve as the security boundary to your other machines (effectively acting as a firewall(s) for the other servers)</li>
</ul>
<p>As we have already covered the basics of Pulumi and using it with Equinix Metal in previous posts, I won&rsquo;t cover how to set up a stack, a Pulumi project or how to generate credentials to use with Equinix Metal.</p>
<h3 id="the-equinix-metal-project">The Equinix Metal project</h3>
<p>As I have been doing a lot with Equinix Metal recently, it makes sense to have an overall/&ldquo;root&rdquo; project, rather than projects that are created and deleted every time I run Pulumi or destroy resources in Pulumi.</p>
<p>To do this, we use something like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#a6e22e">metal</span> <span style="color:#e6db74">&#34;github.com/pulumi/pulumi-equinix-metal/sdk/v2/go/equinix&#34;</span>
	<span style="color:#e6db74">&#34;github.com/pulumi/pulumi/sdk/v3/go/pulumi&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Context</span>) <span style="color:#66d9ef">error</span> {
		<span style="color:#75715e">// Create an Equinix Metal resource (Project)
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">project</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">NewProject</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;pulumi-yetiops&#34;</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">ProjectArgs</span>{
			<span style="color:#a6e22e">Name</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;pulumi-yetiops&#34;</span>),
			<span style="color:#a6e22e">BgpConfig</span>: <span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">ProjectBgpConfigArgs</span>{
				<span style="color:#a6e22e">Asn</span>:            <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Int</span>(<span style="color:#ae81ff">65000</span>),
				<span style="color:#a6e22e">DeploymentType</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;local&#34;</span>),
			},
		})
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#75715e">// Export the name of the project
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Export</span>(<span style="color:#e6db74">&#34;projectName&#34;</span>, <span style="color:#a6e22e">project</span>.<span style="color:#a6e22e">Name</span>)
		<span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Export</span>(<span style="color:#e6db74">&#34;projectId&#34;</span>, <span style="color:#a6e22e">project</span>.<span style="color:#a6e22e">ID</span>())
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
	})
}
</code></pre></div><p>The above creates a project called <code>pulumi-yetiops</code> and enables BGP, using an autonomous system number of <strong>65000</strong> and a BGP type of <strong>local</strong>. Local means we are going to use Equinix&rsquo;s own IP addresses to advertise back to them, rather than any we may own.</p>
<p>An important point to note is that this project is created in it&rsquo;s own Pulumi stack, which we can then reference in other stacks. If you are familiar with Terraform, this is similar to referencing <a href="https://www.terraform.io/docs/language/state/remote-state-data.html">remote states</a>.</p>
<h3 id="referencing-the-project">Referencing the project</h3>
<p>After creating a stack specific to the anycast BGP deployment, we reference the project we created above: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Context</span>) <span style="color:#66d9ef">error</span> {
		<span style="color:#a6e22e">conf</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;&#34;</span>)
		<span style="color:#a6e22e">commonName</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">conf</span>.<span style="color:#a6e22e">Require</span>(<span style="color:#e6db74">&#34;common_name&#34;</span>)

		<span style="color:#a6e22e">rootStack</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">NewStackReference</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;yetiops/equinix-metal-yetiops/staging&#34;</span>, <span style="color:#66d9ef">nil</span>)

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}
</code></pre></div><p>As in the previous posts, we have our <code>commonName</code> variable that we can use across our resources. We can now focus on the <code>pulumi.NewStackReference</code> function.</p>
<p>In this, we pass in the context (standard for Pulumi with Go). We then reference the stack name for our overall Equinix Metal project. From herein, we can now refer to the <code>Export</code> values/Outputs from the root project. An example of this is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#a6e22e">projectId</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">rootStack</span>.<span style="color:#a6e22e">GetStringOutput</span>(<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;projectId&#34;</span>))
</code></pre></div><p>In the above, we get a string output of the root project&rsquo;s <code>projectId</code> value (which we created using <code>ctx.Export(&quot;projectId&quot;, project.ID())</code> in the root project). Now if the root project ID changes, this stack will be aware of the change.</p>
<h3 id="supplying-a-project-ssh-key">Supplying a Project SSH key</h3>
<p>Next, we add an SSH key to our project that will allow us to login to the servers (and also their serial consoles): -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">		<span style="color:#a6e22e">user</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">Current</span>()

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">sshkey_path</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%v/.ssh/id_rsa.pub&#34;</span>, <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">HomeDir</span>)

		<span style="color:#a6e22e">sshkey_file</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">sshkey_path</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">sshkey_contents</span> <span style="color:#f92672">:=</span> string(<span style="color:#a6e22e">sshkey_file</span>)

		<span style="color:#a6e22e">sshkey</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">NewProjectSshKey</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">ProjectSshKeyArgs</span>{
			<span style="color:#a6e22e">Name</span>:      <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">commonName</span>),
			<span style="color:#a6e22e">PublicKey</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">sshkey_contents</span>),
			<span style="color:#a6e22e">ProjectId</span>: <span style="color:#a6e22e">projectId</span>,
		})

</code></pre></div><p>As in the previous Pulumi posts, this gets the home directory of the current user running Pulumi, sources their RSA-based SSH key, and then adds it to the project.</p>
<h3 id="using-cloud-init">Using <code>cloud-init</code></h3>
<p>In previous posts on Pulumi, I provided <code>UserData</code> (i.e. first-boot configuration) using either strings containing a small amount of <code>cloud-config</code> YAML or referenced a YAML file with the required <code>cloud-config</code>. However this can be a bit limiting in terms of how we provide variables, templated values and conditional logic.</p>
<p>Instead I am going to use the <a href="https://github.com/juju/juju/tree/develop/cloudconfig/cloudinit">Juju Cloud-Init module</a> so that generating the <code>UserData</code> is more flexible, and keeps our Pulumi stack entirely in Go.</p>
<p>The inspiration for this is the <a href="https://github.com/tinkerbell/infrastructure/tree/main/pulumi/src">Tinkerbell Testing Infrastructure</a> code created by <a href="https://twitter.com/rawkode">David McKay/Rawkode</a> (who also happens to be a fan of Saltstack and Pulumi!).</p>
<h4 id="the-salt-master">The <code>salt-master</code></h4>
<p>The <code>cloud-config</code> function for the <code>salt-master</code> is defined as follows: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">saltMasterCloudInitConfig</span>() <span style="color:#66d9ef">string</span> {
	<span style="color:#a6e22e">c</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">New</span>(<span style="color:#e6db74">&#34;focal&#34;</span>)

	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;curl -fsSL https://bootstrap.saltproject.io -o install_salt.sh&#34;</span>)
	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;sh install_salt.sh -P -M -x python3&#34;</span>)

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunTextFile</span>(<span style="color:#e6db74">&#34;/etc/salt/master.d/master.conf&#34;</span>, <span style="color:#e6db74">`autosign_grains_dir: /etc/salt/autosign-grains
</span><span style="color:#e6db74">fileserver_backend:
</span><span style="color:#e6db74">  - roots
</span><span style="color:#e6db74">  - gitfs
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">gitfs_remotes:
</span><span style="color:#e6db74">  - https://gitlab.com/stuh84/salt-anycast-equinix:
</span><span style="color:#e6db74">    - root: states
</span><span style="color:#e6db74">    - base: main
</span><span style="color:#e6db74">    - update_interval: 120
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">pillar_roots:
</span><span style="color:#e6db74">  base:
</span><span style="color:#e6db74">    - /srv/salt/pillars
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">ext_pillar:
</span><span style="color:#e6db74">  - git:
</span><span style="color:#e6db74">    - main https://gitlab.com/stuh84/salt-anycast-equinix:
</span><span style="color:#e6db74">      - root: pillars
</span><span style="color:#e6db74">      - env: base`</span>, <span style="color:#ae81ff">0644</span>)

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunTextFile</span>(<span style="color:#e6db74">&#34;/etc/salt/minion.d/minion.conf&#34;</span>, <span style="color:#e6db74">`autosign_grains:
</span><span style="color:#e6db74">- role
</span><span style="color:#e6db74">startup_states: highstate
</span><span style="color:#e6db74">grains:
</span><span style="color:#e6db74">  role: master`</span>, <span style="color:#ae81ff">0644</span>)

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;mkdir -p /etc/salt/autosign-grains&#34;</span>)

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;echo -e \&#34;master\nbgp\n\&#34; &gt; /etc/salt/autosign-grains/role&#34;</span>)

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;PRIVATE_IP=$(curl -s https://metadata.platformequinix.com/metadata | jq -r &#39;.network.addresses | map(select(.public==false)) | first | .address&#39;)&#34;</span>)

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;mkdir -p /srv/salt&#34;</span>)

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;echo interface: ${PRIVATE_IP} &gt; /etc/salt/master.d/private-interface.conf&#34;</span>)
	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;echo master: ${PRIVATE_IP} &gt; /etc/salt/minion.d/master.conf&#34;</span>)
	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;systemctl daemon-reload&#34;</span>)
	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;systemctl enable salt-master.service&#34;</span>)
	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;systemctl restart --no-block salt-master.service&#34;</span>)
	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;systemctl enable salt-minion.service&#34;</span>)
	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;systemctl restart --no-block salt-minion.service&#34;</span>)

	<span style="color:#a6e22e">script</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">RenderScript</span>()

	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}

	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">script</span>
}
</code></pre></div><p>To explain what we are doing here: -</p>
<ul>
<li>We create a new CloudInit definition
<ul>
<li>The <code>focal</code> reference works for both Ubuntu and Debian</li>
</ul>
</li>
<li>We use <code>AddRunCmd</code> functions (i.e. those that run only on first boot, rather than every boot) to install Salt, and make this machine a Salt Master</li>
<li>We create a <code>master.conf</code> file to instruct the Salt Master to: -
<ul>
<li>Add autosigned grains based upon a node&rsquo;s role (full explanation below)</li>
<li>Use the root file system, and also <code>gitfs</code> (i.e. mounting a Git repository as a file system)</li>
<li>Specify our Salt repository as a <code>gitfs_remote</code>, meaning it will be mounted and used by Salt to provide states
<ul>
<li>We also tell it to update every 120s, to allow changes in the repository to be reflected on the file system</li>
</ul>
</li>
<li>Provide our standard pillars directory</li>
<li>Reference the <code>pillars</code> directory in our Salt repository as an external pillar, allowing us to update the pillars directly through Git</li>
</ul>
</li>
<li>We add a minion configuration file which says to: -
<ul>
<li>Autosign our role grain</li>
<li>When the minion is accepted by the master, automatically run a high state (i.e. apply all configuration specific to this node)</li>
<li>Add a grain of our role called <code>master</code></li>
</ul>
</li>
<li>We create our <code>autosign_grains_dir</code> directory</li>
<li>We place inside it a file called role, containing the lines <code>master</code> and <code>bgp</code></li>
<li>We derive our private IPv4 address from the Equinix Metal metadata service</li>
<li>We specify that Salt should only accept minions on our private interface (i.e. the interface with our private IP)</li>
<li>We specify to the Minion that it should communicate with the master on the same private IPv4 address</li>
</ul>
<p>The subsequent steps run a SystemD <code>daemon-reload</code> to ensure we are taking into account the latest SystemD unit files, and then we enable and restart the <code>salt-master</code> and <code>salt-minion</code> services.</p>
<p>Finally, we render all of our steps into a <code>cloud-init</code> script and return it.</p>
<p>To explain what Grain Autosigning is, this is where a Salt Master will automatically accept any Minion that has a Grain that matches those specified in the files in the <code>autosign-grains</code> directory. For example, our <code>autosign-grains/role</code> file will look like the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">master
bgp
</code></pre></div><p>Any minion that has a grain of <code>role</code> with a value of <code>master</code> or <code>bgp</code> is automatically accepted. Without this, you need to manually login to the <code>salt-master</code> and then accept the minions using something like <code>salt-key -a 'salt-master.example.com'</code> or <code>salt-key -a 'bgp-01*'</code>.</p>
<p>So long as we have a grain called <code>role</code>, and the contents of this grain match either <code>master</code> or <code>bgp</code>, the minion will then be accepted automatically (notice in the <code>minion.conf</code> file creation, we add the <code>role</code> grain).</p>
<p>In a production scenario, you need to ensure that either your master/minion communication is completely private or that the accepted grain values are sufficiently unique (e.g. a UUID value) and masked/encrypted in your repository, otherwise any server running a Salt minion that has these grains could be accepted by the master. Depending on your Salt setup, this could provide anything from basic information about your infrastructure to user creation that allows connectivity to other areas of your network.</p>
<h4 id="referencing-the-function">Referencing the function</h4>
<p>To reference this function in our code, we use the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#a6e22e">saltCloudInit</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">saltMasterCloudInitConfig</span>()
</code></pre></div><p>As the return type is a string, we can use this directly in the Equinix Device creation functions like so: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#a6e22e">UserData</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">saltCloudInit</span>)
</code></pre></div><h4 id="the-bgpanycast-nodes">The <code>bgp</code>/anycast nodes</h4>
<p>The function we use for the anycast nodes is similar to the <code>salt-master</code> Cloud-Init creation function. However we do need to supply a few variables to the function for ExaBGP and the anycast IP to be configured.</p>
<p>We do this by creating a <strong>struct</strong>, and allowing our function to have an instance of this struct passed to it.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">BgpNodeConfig</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">anycastIP</span>     <span style="color:#66d9ef">string</span>
	<span style="color:#a6e22e">anycastIPMask</span> <span style="color:#66d9ef">string</span>
	<span style="color:#a6e22e">saltMasterIP</span>  <span style="color:#66d9ef">string</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">bgpCloudInitConfig</span>(<span style="color:#a6e22e">config</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">BgpNodeConfig</span>) <span style="color:#66d9ef">string</span> {
	<span style="color:#a6e22e">c</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">New</span>(<span style="color:#e6db74">&#34;focal&#34;</span>)

	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;curl -fsSL https://bootstrap.saltproject.io -o install_salt.sh&#34;</span>)
	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;sh install_salt.sh -P -x python3&#34;</span>)

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;PRIVATE_IP=$(curl -s https://metadata.platformequinix.com/metadata | jq -r &#39;.network.addresses | map(select(.public==false)) | first | .address&#39;)&#34;</span>)

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;echo master: %s &gt; /etc/salt/minion.d/master.conf&#34;</span>, <span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">saltMasterIP</span>))

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunTextFile</span>(<span style="color:#e6db74">&#34;/etc/salt/grains&#34;</span>,
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(
			<span style="color:#e6db74">&#34;anycast:\n  ipv4:\n    - address: %s\n      mask: %s\n&#34;</span>,
			<span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">anycastIP</span>,
			<span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">anycastIPMask</span>,
		), <span style="color:#ae81ff">0400</span>,
	)

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunTextFile</span>(<span style="color:#e6db74">&#34;/etc/salt/minion.d/minion.conf&#34;</span>, <span style="color:#e6db74">`autosign_grains:
</span><span style="color:#e6db74">- role
</span><span style="color:#e6db74">startup_states: highstate`</span>, <span style="color:#ae81ff">0644</span>)

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;echo \&#34;bgp:\n  localip: ${PRIVATE_IP}\nrole: bgp\&#34; &gt;&gt; /etc/salt/grains&#34;</span>)

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;systemctl daemon-reload&#34;</span>)
	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;systemctl enable salt-minion.service&#34;</span>)
	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;systemctl restart --no-block salt-minion.service&#34;</span>)

	<span style="color:#a6e22e">script</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">RenderScript</span>()

	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}

	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">script</span>
}
</code></pre></div><p>The differences between this and the <code>saltMasterCloudInitConfig</code> function are as follows: -</p>
<ul>
<li>We create a <code>BgpNodeConfig</code> struct, which will be used to provide the anycast IPv4 address, it&rsquo;s subnet mask, and the IPv4 address of the Salt Master</li>
<li>We create the <code>bgpCloudInitConfig</code> function, which requires an input variable (an instance of the <code>BgpNodeConfig</code> struct) and returns the <code>cloud-init</code> script as a string</li>
<li>We install <code>salt</code>, but omit the <code>-M</code> flag so that we don&rsquo;t make this machine a master as well</li>
<li>We create our <code>/etc/salt/grains</code> file, and populate it with our anycast IPv4 address and subnet mask</li>
<li>We use a similar minion configuration file as for the master, except we are placing our grains in a separate file</li>
<li>We append the private IPv4 address as a grain to our <code>/etc/salt/grains/</code> file as the <code>bgp.localip</code> grain, as well as adding our <code>bgp</code> role grain (used to autosign this minion)</li>
<li>Finally, we run a SystemD <code>daemon-reload</code>, and then enable and restart the <code>salt-minion</code> process</li>
</ul>
<p>We render the <code>cloud-init</code> script, and return it as a string.</p>
<p>Because the local IP, Salt Master IP and Anycast IP address and mask will only be known after the machines are provisioned, we must ensure that we are not hardcoding values into the <code>cloud-init</code> templates. This avoids them being out-of-date/invalid if we recreate our infrastructure.</p>
<p>Another point to note is nothing is specific to each node in this function, so we can use the same generated <code>cloud-init</code> script for one, two or ten BGP-speaking anycast nodes.</p>
<h4 id="referencing-the-function-1">Referencing the function</h4>
<p>Unlike the Salt Master <code>cloud-init</code> script, we must supply a few variables that are only known at the time of being provisioned. To do this, we use something like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">		<span style="color:#a6e22e">bgpCloudConfig</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">All</span>(
			<span style="color:#a6e22e">anycastIpAllocation</span>.<span style="color:#a6e22e">Address</span>,
			<span style="color:#a6e22e">anycastIpAllocation</span>.<span style="color:#a6e22e">Netmask</span>,
			<span style="color:#a6e22e">saltMaster</span>.<span style="color:#a6e22e">AccessPrivateIpv4</span>).<span style="color:#a6e22e">ApplyT</span>(
			<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">args</span> []<span style="color:#66d9ef">interface</span>{}) <span style="color:#66d9ef">string</span> {
				<span style="color:#a6e22e">bgpCloudInitNodeConfig</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">BgpNodeConfig</span>{
					<span style="color:#a6e22e">anycastIP</span>:     <span style="color:#a6e22e">args</span>[<span style="color:#ae81ff">0</span>].(<span style="color:#66d9ef">string</span>),
					<span style="color:#a6e22e">anycastIPMask</span>: <span style="color:#a6e22e">args</span>[<span style="color:#ae81ff">1</span>].(<span style="color:#66d9ef">string</span>),
					<span style="color:#a6e22e">saltMasterIP</span>:  <span style="color:#a6e22e">args</span>[<span style="color:#ae81ff">2</span>].(<span style="color:#66d9ef">string</span>),
				}
				<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">bgpCloudInitConfig</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">bgpCloudInitNodeConfig</span>)
			})

</code></pre></div><p>In the above we are:-</p>
<ul>
<li>Using an <a href="https://www.pulumi.com/docs/intro/concepts/inputs-outputs/#all">All</a> Pulumi function to supply values not yet known (i.e. the Anycast IPv4 details and Salt Master IPv4 address) to an Apply function</li>
<li>The Apply function will run after the creation of the Salt Master server and the Anycast IP allocation</li>
<li>It creates an instance of the <code>BgpNodeConfig</code> struct, with the values derived from the provisioned anycast IP and Salt Master</li>
<li>This instance of the struct is then passed to the <code>bgpCloudInitConfig</code> function</li>
<li>The function returns the <code>cloud-init</code> script ready to pass to our BGP nodes, containing the correct anycast IP and Salt Master details</li>
</ul>
<p>As mentioned, we don&rsquo;t know the details required by the function until Pulumi runs, at which point they are considered Pulumi Outputs. The Juju <code>cloudinit</code> module has no concept of a Pulumi Output (i.e. it cannot natively evaluate a Pulumi Output type). Therefore we use the Pulumi Apply function to take the Output values, turn them into a more common type (e.g. a <code>string</code> or <code>int</code>) to pass to our <code>cloud-init</code> function.</p>
<p>The <code>bgpCloudConfig</code> will be of a Pulumi Output type after the Apply function runs, meaning that Pulumi is aware of the context and history of creating and evaluating the other Pulumi Outputs (i.e. the anycast IP details and Salt Master IP).</p>
<h3 id="creating-the-salt-master">Creating the <code>salt-master</code></h3>
<p>Now that we have the correct <code>cloud-init</code> data for provisioning a <code>salt-master</code>, we can create one like so: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">		<span style="color:#a6e22e">saltMaster</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">NewDevice</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;salt-master&#34;</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">DeviceArgs</span>{
			<span style="color:#a6e22e">Hostname</span>:        <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;salt-master&#34;</span>),
			<span style="color:#a6e22e">Plan</span>:            <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;c3.small.x86&#34;</span>),
			<span style="color:#a6e22e">Metro</span>:           <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;am&#34;</span>),
			<span style="color:#a6e22e">OperatingSystem</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;ubuntu_20_04&#34;</span>),
			<span style="color:#a6e22e">BillingCycle</span>:    <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;hourly&#34;</span>),
			<span style="color:#a6e22e">ProjectId</span>:       <span style="color:#a6e22e">projectId</span>,
			<span style="color:#a6e22e">ProjectSshKeyIds</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
				<span style="color:#a6e22e">sshkey</span>.<span style="color:#a6e22e">ID</span>(),
			},
			<span style="color:#a6e22e">UserData</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">saltCloudInit</span>),
		})

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

</code></pre></div><p>A small difference between the previous Pulumi post and this one is that Equinix Metal now use the term <strong>Metro</strong> rather than <strong>Facility</strong> to refer to the location you create infrastructure in. The reason for this is Equinix Metal can now provide VLANs, elastic IPs and Private IPv4 connectivity across multiple facilities in a geographic area, rather than being limited to a single location. If you are familiar with AWS, think of a <strong>Metro</strong> like a <strong>Region</strong>, and a <strong>Facility</strong> like an <strong>Availability Zone</strong>.</p>
<p>This has the added benefit that more capacity is available across multiple facilities, without being limited by network topology (i.e. VLANs being restricted to a single facility) between the infrastructure you provision.</p>
<p>Other than this, the above is very similar to other servers we have created in Equinix Metal. We pass in our <code>saltCloudInit</code> variable, which contains the <code>cloud-init</code> script generated by the <code>saltMasterCloudInitConfig</code> function.</p>
<h3 id="creating-the-anycast-ip">Creating the anycast IP</h3>
<p>The anycast IP uses Equinix&rsquo;s <code>NewReservedIPBlock</code> function to reserve a single IP from the metro&rsquo;s IPv4 allocation. If you have your own IP space and use the <strong>Global BGP</strong> option when adding BGP to the initial Equinix Metal project, you are not limited by the metro as to where you can advertise the IPs from.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">		<span style="color:#a6e22e">anycastIpAllocation</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">NewReservedIpBlock</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;anycast-ip&#34;</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">ReservedIpBlockArgs</span>{
			<span style="color:#a6e22e">ProjectId</span>: <span style="color:#a6e22e">projectId</span>,
			<span style="color:#a6e22e">Metro</span>:     <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;am&#34;</span>),
			<span style="color:#a6e22e">Type</span>:      <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;public_ipv4&#34;</span>),
			<span style="color:#a6e22e">Quantity</span>:  <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Int</span>(<span style="color:#ae81ff">1</span>),
		})

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}
</code></pre></div><p>The above allocates a single IPv4 address in the Equinix Metal <code>am</code> (Amsterdam) metro. We specify <code>public_ipv4</code>, as the other option (<code>global_ipv4</code>) is only valid if deploying directly to a facility.</p>
<h3 id="creating-our-anycast-nodes-and-bgp-sessions">Creating our anycast nodes and BGP sessions</h3>
<p>Now that we have our anycast IP, and we have generated our <code>cloud-init</code> script, we can create our anycast nodes and the BGP sessions from Equinix to the nodes: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">		<span style="color:#a6e22e">bgpPrimary</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">NewDevice</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;bgp-01&#34;</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">DeviceArgs</span>{
			<span style="color:#a6e22e">Hostname</span>:        <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;bgp-01&#34;</span>),
			<span style="color:#a6e22e">Plan</span>:            <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;c3.small.x86&#34;</span>),
			<span style="color:#a6e22e">Metro</span>:           <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;am&#34;</span>),
			<span style="color:#a6e22e">OperatingSystem</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;debian_10&#34;</span>),
			<span style="color:#a6e22e">BillingCycle</span>:    <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;hourly&#34;</span>),
			<span style="color:#a6e22e">ProjectId</span>:       <span style="color:#a6e22e">projectId</span>,
			<span style="color:#a6e22e">ProjectSshKeyIds</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
				<span style="color:#a6e22e">sshkey</span>.<span style="color:#a6e22e">ID</span>(),
			},
			<span style="color:#a6e22e">UserData</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringOutput</span>(<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%v&#34;</span>, <span style="color:#a6e22e">bgpCloudConfig</span>)),
		})

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">bgpPrimarySession</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">NewBgpSession</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;bgp-01_BGP_SESSION&#34;</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">BgpSessionArgs</span>{
			<span style="color:#a6e22e">AddressFamily</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;ipv4&#34;</span>),
			<span style="color:#a6e22e">DeviceId</span>:      <span style="color:#a6e22e">bgpPrimary</span>.<span style="color:#a6e22e">ID</span>(),
		})

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

</code></pre></div><p>In the device creation, we are using Debian Buster rather than Ubuntu 20.04 LTS (which we used for the &lsquo;salt-master&rsquo;), mainly due to ease of managing networks on Debian Buster currently. Also, we must turn the <code>bgpCloudConfig</code> Pulumi Output into a Pulumi String type (a <code>StringOutput</code> is fine in this context) as the <code>UserData</code> field doesn&rsquo;t accept values of type Pulumi Output (only Pulumi String types).</p>
<p>After this, we create an IPv4 BGP session that is associated with this server.</p>
<p>The second anycast node we create is identical to this, except we use the word <code>Secondary</code> instead of <code>Primary</code>, and <code>02</code> instead of <code>01</code> for the hostname and resource names.</p>
<h3 id="all-the-code">All the code</h3>
<p>We have the code in two separate files, one called <code>main.go</code> (which contains all of the Equinix Metal-specific code) and one called <code>cloud-init.go</code> that contains all of the <code>cloud-init</code>-specific code.</p>
<h4 id="maingo"><code>main.go</code></h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;io/ioutil&#34;</span>
	<span style="color:#e6db74">&#34;os/user&#34;</span>

	<span style="color:#a6e22e">metal</span> <span style="color:#e6db74">&#34;github.com/pulumi/pulumi-equinix-metal/sdk/v2/go/equinix&#34;</span>
	<span style="color:#e6db74">&#34;github.com/pulumi/pulumi/sdk/v3/go/pulumi&#34;</span>
	<span style="color:#e6db74">&#34;github.com/pulumi/pulumi/sdk/v3/go/pulumi/config&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Context</span>) <span style="color:#66d9ef">error</span> {
		<span style="color:#a6e22e">conf</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;&#34;</span>)
		<span style="color:#a6e22e">commonName</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">conf</span>.<span style="color:#a6e22e">Require</span>(<span style="color:#e6db74">&#34;common_name&#34;</span>)

		<span style="color:#a6e22e">rootStack</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">NewStackReference</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;yetiops/equinix-metal-yetiops/staging&#34;</span>, <span style="color:#66d9ef">nil</span>)

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">projectId</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">rootStack</span>.<span style="color:#a6e22e">GetStringOutput</span>(<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;projectId&#34;</span>))

		<span style="color:#a6e22e">user</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">Current</span>()

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">sshkey_path</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%v/.ssh/id_rsa.pub&#34;</span>, <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">HomeDir</span>)

		<span style="color:#a6e22e">sshkey_file</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">sshkey_path</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">sshkey_contents</span> <span style="color:#f92672">:=</span> string(<span style="color:#a6e22e">sshkey_file</span>)

		<span style="color:#a6e22e">sshkey</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">NewProjectSshKey</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">ProjectSshKeyArgs</span>{
			<span style="color:#a6e22e">Name</span>:      <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">commonName</span>),
			<span style="color:#a6e22e">PublicKey</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">sshkey_contents</span>),
			<span style="color:#a6e22e">ProjectId</span>: <span style="color:#a6e22e">projectId</span>,
		})

		<span style="color:#a6e22e">saltCloudInit</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">saltMasterCloudInitConfig</span>()

		<span style="color:#a6e22e">saltMaster</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">NewDevice</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;salt-master&#34;</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">DeviceArgs</span>{
			<span style="color:#a6e22e">Hostname</span>:        <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;salt-master&#34;</span>),
			<span style="color:#a6e22e">Plan</span>:            <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;c3.small.x86&#34;</span>),
			<span style="color:#a6e22e">Metro</span>:           <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;am&#34;</span>),
			<span style="color:#a6e22e">OperatingSystem</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;ubuntu_20_04&#34;</span>),
			<span style="color:#a6e22e">BillingCycle</span>:    <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;hourly&#34;</span>),
			<span style="color:#a6e22e">ProjectId</span>:       <span style="color:#a6e22e">projectId</span>,
			<span style="color:#a6e22e">ProjectSshKeyIds</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
				<span style="color:#a6e22e">sshkey</span>.<span style="color:#a6e22e">ID</span>(),
			},
			<span style="color:#a6e22e">UserData</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">saltCloudInit</span>),
		})

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">anycastIpAllocation</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">NewReservedIpBlock</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;anycast-ip&#34;</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">ReservedIpBlockArgs</span>{
			<span style="color:#a6e22e">ProjectId</span>: <span style="color:#a6e22e">projectId</span>,
			<span style="color:#a6e22e">Metro</span>:     <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;am&#34;</span>),
			<span style="color:#a6e22e">Type</span>:      <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;public_ipv4&#34;</span>),
			<span style="color:#a6e22e">Quantity</span>:  <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Int</span>(<span style="color:#ae81ff">1</span>),
		})

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">bgpCloudConfig</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">All</span>(
			<span style="color:#a6e22e">anycastIpAllocation</span>.<span style="color:#a6e22e">Address</span>,
			<span style="color:#a6e22e">anycastIpAllocation</span>.<span style="color:#a6e22e">Netmask</span>,
			<span style="color:#a6e22e">saltMaster</span>.<span style="color:#a6e22e">AccessPrivateIpv4</span>).<span style="color:#a6e22e">ApplyT</span>(
			<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">args</span> []<span style="color:#66d9ef">interface</span>{}) <span style="color:#66d9ef">string</span> {
				<span style="color:#a6e22e">bgpCloudInitNodeConfig</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">BgpNodeConfig</span>{
					<span style="color:#a6e22e">anycastIP</span>:     <span style="color:#a6e22e">args</span>[<span style="color:#ae81ff">0</span>].(<span style="color:#66d9ef">string</span>),
					<span style="color:#a6e22e">anycastIPMask</span>: <span style="color:#a6e22e">args</span>[<span style="color:#ae81ff">1</span>].(<span style="color:#66d9ef">string</span>),
					<span style="color:#a6e22e">saltMasterIP</span>:  <span style="color:#a6e22e">args</span>[<span style="color:#ae81ff">2</span>].(<span style="color:#66d9ef">string</span>),
				}
				<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">bgpCloudInitConfig</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">bgpCloudInitNodeConfig</span>)
			})

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">bgpPrimary</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">NewDevice</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;bgp-01&#34;</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">DeviceArgs</span>{
			<span style="color:#a6e22e">Hostname</span>:        <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;bgp-01&#34;</span>),
			<span style="color:#a6e22e">Plan</span>:            <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;c3.small.x86&#34;</span>),
			<span style="color:#a6e22e">Metro</span>:           <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;am&#34;</span>),
			<span style="color:#a6e22e">OperatingSystem</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;debian_10&#34;</span>),
			<span style="color:#a6e22e">BillingCycle</span>:    <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;hourly&#34;</span>),
			<span style="color:#a6e22e">ProjectId</span>:       <span style="color:#a6e22e">projectId</span>,
			<span style="color:#a6e22e">ProjectSshKeyIds</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
				<span style="color:#a6e22e">sshkey</span>.<span style="color:#a6e22e">ID</span>(),
			},
			<span style="color:#a6e22e">UserData</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringOutput</span>(<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%v&#34;</span>, <span style="color:#a6e22e">bgpCloudConfig</span>)),
		})

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">bgpPrimarySession</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">NewBgpSession</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;bgp-01_BGP_SESSION&#34;</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">BgpSessionArgs</span>{
			<span style="color:#a6e22e">AddressFamily</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;ipv4&#34;</span>),
			<span style="color:#a6e22e">DeviceId</span>:      <span style="color:#a6e22e">bgpPrimary</span>.<span style="color:#a6e22e">ID</span>(),
		})

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">bgpSecondary</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">NewDevice</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;bgp-02&#34;</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">DeviceArgs</span>{
			<span style="color:#a6e22e">Hostname</span>:        <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;bgp-02&#34;</span>),
			<span style="color:#a6e22e">Plan</span>:            <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;c3.small.x86&#34;</span>),
			<span style="color:#a6e22e">Metro</span>:           <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;am&#34;</span>),
			<span style="color:#a6e22e">OperatingSystem</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;debian_10&#34;</span>),
			<span style="color:#a6e22e">BillingCycle</span>:    <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;hourly&#34;</span>),
			<span style="color:#a6e22e">ProjectId</span>:       <span style="color:#a6e22e">projectId</span>,
			<span style="color:#a6e22e">ProjectSshKeyIds</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
				<span style="color:#a6e22e">sshkey</span>.<span style="color:#a6e22e">ID</span>(),
			},
			<span style="color:#a6e22e">UserData</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringOutput</span>(<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%v&#34;</span>, <span style="color:#a6e22e">bgpCloudConfig</span>)),
		})

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">bgpSecondarySession</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">NewBgpSession</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;bgp-02_BGP_SESSION&#34;</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">BgpSessionArgs</span>{
			<span style="color:#a6e22e">AddressFamily</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;ipv4&#34;</span>),
			<span style="color:#a6e22e">DeviceId</span>:      <span style="color:#a6e22e">bgpSecondary</span>.<span style="color:#a6e22e">ID</span>(),
		})

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Export</span>(<span style="color:#e6db74">&#34;saltMasterIP&#34;</span>, <span style="color:#a6e22e">saltMaster</span>.<span style="color:#a6e22e">AccessPublicIpv4</span>)
		<span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Export</span>(<span style="color:#e6db74">&#34;bgpPrimaryIP&#34;</span>, <span style="color:#a6e22e">bgpPrimary</span>.<span style="color:#a6e22e">AccessPublicIpv4</span>)
		<span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Export</span>(<span style="color:#e6db74">&#34;bgpSecondaryIP&#34;</span>, <span style="color:#a6e22e">bgpSecondary</span>.<span style="color:#a6e22e">AccessPublicIpv4</span>)
		<span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Export</span>(<span style="color:#e6db74">&#34;bgpPrimarySessionStatus&#34;</span>, <span style="color:#a6e22e">bgpPrimarySession</span>.<span style="color:#a6e22e">Status</span>)
		<span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Export</span>(<span style="color:#e6db74">&#34;bgpSecondarySessionStatus&#34;</span>, <span style="color:#a6e22e">bgpSecondarySession</span>.<span style="color:#a6e22e">Status</span>)
		<span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Export</span>(<span style="color:#e6db74">&#34;anycastIP&#34;</span>, <span style="color:#a6e22e">anycastIpAllocation</span>.<span style="color:#a6e22e">Address</span>)
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
	})
}
</code></pre></div><h4 id="cloud-initgo"><code>cloud-init.go</code></h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>

	<span style="color:#e6db74">&#34;github.com/juju/juju/cloudconfig/cloudinit&#34;</span>
)

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">BgpNodeConfig</span> <span style="color:#66d9ef">struct</span> {
	<span style="color:#a6e22e">anycastIP</span>     <span style="color:#66d9ef">string</span>
	<span style="color:#a6e22e">anycastIPMask</span> <span style="color:#66d9ef">string</span>
	<span style="color:#a6e22e">saltMasterIP</span>  <span style="color:#66d9ef">string</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">saltMasterCloudInitConfig</span>() <span style="color:#66d9ef">string</span> {
	<span style="color:#a6e22e">c</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">New</span>(<span style="color:#e6db74">&#34;focal&#34;</span>)

	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;curl -fsSL https://bootstrap.saltproject.io -o install_salt.sh&#34;</span>)
	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;sh install_salt.sh -P -M -x python3&#34;</span>)

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunTextFile</span>(<span style="color:#e6db74">&#34;/etc/salt/master.d/master.conf&#34;</span>, <span style="color:#e6db74">`autosign_grains_dir: /etc/salt/autosign-grains
</span><span style="color:#e6db74">fileserver_backend:
</span><span style="color:#e6db74">  - roots
</span><span style="color:#e6db74">  - gitfs
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">gitfs_remotes:
</span><span style="color:#e6db74">  - https://gitlab.com/stuh84/salt-anycast-equinix:
</span><span style="color:#e6db74">    - root: states
</span><span style="color:#e6db74">    - base: main
</span><span style="color:#e6db74">    - update_interval: 120
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">pillar_roots:
</span><span style="color:#e6db74">  base:
</span><span style="color:#e6db74">    - /srv/salt/pillars
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">ext_pillar:
</span><span style="color:#e6db74">  - git:
</span><span style="color:#e6db74">    - main https://gitlab.com/stuh84/salt-anycast-equinix:
</span><span style="color:#e6db74">      - root: pillars
</span><span style="color:#e6db74">      - env: base`</span>, <span style="color:#ae81ff">0644</span>)

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunTextFile</span>(<span style="color:#e6db74">&#34;/etc/salt/minion.d/minion.conf&#34;</span>, <span style="color:#e6db74">`autosign_grains:
</span><span style="color:#e6db74">- role
</span><span style="color:#e6db74">startup_states: highstate
</span><span style="color:#e6db74">grains:
</span><span style="color:#e6db74">  role: master`</span>, <span style="color:#ae81ff">0644</span>)

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;mkdir -p /etc/salt/autosign-grains&#34;</span>)

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;echo -e \&#34;master\nbgp\n\&#34; &gt; /etc/salt/autosign-grains/role&#34;</span>)

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;PRIVATE_IP=$(curl -s https://metadata.platformequinix.com/metadata | jq -r &#39;.network.addresses | map(select(.public==false)) | first | .address&#39;)&#34;</span>)

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;mkdir -p /srv/salt&#34;</span>)

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;echo interface: ${PRIVATE_IP} &gt; /etc/salt/master.d/private-interface.conf&#34;</span>)
	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;echo master: ${PRIVATE_IP} &gt; /etc/salt/minion.d/master.conf&#34;</span>)
	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;systemctl daemon-reload&#34;</span>)
	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;systemctl enable salt-master.service&#34;</span>)
	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;systemctl restart --no-block salt-master.service&#34;</span>)
	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;systemctl enable salt-minion.service&#34;</span>)
	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;systemctl restart --no-block salt-minion.service&#34;</span>)

	<span style="color:#a6e22e">script</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">RenderScript</span>()

	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}

	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">script</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">bgpCloudInitConfig</span>(<span style="color:#a6e22e">config</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">BgpNodeConfig</span>) <span style="color:#66d9ef">string</span> {
	<span style="color:#a6e22e">c</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">New</span>(<span style="color:#e6db74">&#34;focal&#34;</span>)

	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;curl -fsSL https://bootstrap.saltproject.io -o install_salt.sh&#34;</span>)
	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;sh install_salt.sh -P -x python3&#34;</span>)

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;PRIVATE_IP=$(curl -s https://metadata.platformequinix.com/metadata | jq -r &#39;.network.addresses | map(select(.public==false)) | first | .address&#39;)&#34;</span>)

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;echo master: %s &gt; /etc/salt/minion.d/master.conf&#34;</span>, <span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">saltMasterIP</span>))

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunTextFile</span>(<span style="color:#e6db74">&#34;/etc/salt/grains&#34;</span>,
		<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(
			<span style="color:#e6db74">&#34;anycast:\n  ipv4:\n    - address: %s\n      mask: %s\n&#34;</span>,
			<span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">anycastIP</span>,
			<span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">anycastIPMask</span>,
		), <span style="color:#ae81ff">0400</span>,
	)

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunTextFile</span>(<span style="color:#e6db74">&#34;/etc/salt/minion.d/minion.conf&#34;</span>, <span style="color:#e6db74">`autosign_grains:
</span><span style="color:#e6db74">- role
</span><span style="color:#e6db74">startup_states: highstate`</span>, <span style="color:#ae81ff">0644</span>)

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;echo \&#34;bgp:\n  localip: ${PRIVATE_IP}\nrole: bgp\&#34; &gt;&gt; /etc/salt/grains&#34;</span>)

	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;systemctl daemon-reload&#34;</span>)
	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;systemctl enable salt-minion.service&#34;</span>)
	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AddRunCmd</span>(<span style="color:#e6db74">&#34;systemctl restart --no-block salt-minion.service&#34;</span>)

	<span style="color:#a6e22e">script</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">RenderScript</span>()

	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		panic(<span style="color:#a6e22e">err</span>)
	}

	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">script</span>
}
</code></pre></div><h2 id="provisioning">Provisioning</h2>
<p>Now that we have created our Pulumi code and our Salt configuration files, we can run Pulumi and watch everything create!</p>
<p>Below is an Asciinema output of my terminal when running <code>pulumi up</code>, so you can see the resources being created: -</p>
<script src="https://asciinema.org/a/414056.js" id="asciicast-414056" async></script> 
<p>We can also take a look at what the generated <code>cloud-init</code> script files look like too: -</p>
<h3 id="salt-master">salt-master</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/bin/bash
</span><span style="color:#75715e"></span>set -e
test -n <span style="color:#e6db74">&#34;</span>$JUJU_PROGRESS_FD<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">||</span> <span style="color:#f92672">(</span>exec <span style="color:#f92672">{</span>JUJU_PROGRESS_FD<span style="color:#f92672">}</span>&gt;&amp;2<span style="color:#f92672">)</span> 2&gt;/dev/null <span style="color:#f92672">&amp;&amp;</span> exec <span style="color:#f92672">{</span>JUJU_PROGRESS_FD<span style="color:#f92672">}</span>&gt;&amp;<span style="color:#ae81ff">2</span> <span style="color:#f92672">||</span> JUJU_PROGRESS_FD<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span>
<span style="color:#f92672">(</span>

<span style="color:#66d9ef">function</span> package_manager_loop <span style="color:#f92672">{</span>
    local rc<span style="color:#f92672">=</span>
    <span style="color:#66d9ef">while</span> true; <span style="color:#66d9ef">do</span>
        <span style="color:#66d9ef">if</span> <span style="color:#f92672">(</span>$*<span style="color:#f92672">)</span>; <span style="color:#66d9ef">then</span>
                <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>
        <span style="color:#66d9ef">else</span>
                rc<span style="color:#f92672">=</span>$?
        <span style="color:#66d9ef">fi</span>
        <span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> $rc -eq <span style="color:#ae81ff">100</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
                sleep 10s
                <span style="color:#66d9ef">continue</span>
        <span style="color:#66d9ef">fi</span>
        <span style="color:#66d9ef">return</span> $rc
    <span style="color:#66d9ef">done</span>
<span style="color:#f92672">}</span>

curl -fsSL https://bootstrap.saltproject.io -o install_salt.sh
sh install_salt.sh -P -M -x python3
install -D -m <span style="color:#ae81ff">644</span> /dev/null <span style="color:#e6db74">&#39;/etc/salt/master.d/master.conf&#39;</span>
printf <span style="color:#e6db74">&#39;%s\n&#39;</span> <span style="color:#e6db74">&#39;autosign_grains_dir: /etc/salt/autosign-grains
</span><span style="color:#e6db74">fileserver_backend:
</span><span style="color:#e6db74">  - roots
</span><span style="color:#e6db74">  - gitfs
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">gitfs_remotes:
</span><span style="color:#e6db74">  - https://gitlab.com/stuh84/salt-anycast-equinix:
</span><span style="color:#e6db74">    - root: states
</span><span style="color:#e6db74">    - base: main
</span><span style="color:#e6db74">    - update_interval: 120
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">pillar_roots:
</span><span style="color:#e6db74">  base:
</span><span style="color:#e6db74">    - /srv/salt/pillars
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">ext_pillar:
</span><span style="color:#e6db74">  - git:
</span><span style="color:#e6db74">    - main https://gitlab.com/stuh84/salt-anycast-equinix:
</span><span style="color:#e6db74">      - root: pillars
</span><span style="color:#e6db74">      - env: base&#39;</span> &gt; <span style="color:#e6db74">&#39;/etc/salt/master.d/master.conf&#39;</span>
install -D -m <span style="color:#ae81ff">644</span> /dev/null <span style="color:#e6db74">&#39;/etc/salt/minion.d/minion.conf&#39;</span>
printf <span style="color:#e6db74">&#39;%s\n&#39;</span> <span style="color:#e6db74">&#39;autosign_grains:
</span><span style="color:#e6db74">- role
</span><span style="color:#e6db74">startup_states: highstate
</span><span style="color:#e6db74">grains:
</span><span style="color:#e6db74">  role: master&#39;</span> &gt; <span style="color:#e6db74">&#39;/etc/salt/minion.d/minion.conf&#39;</span>
mkdir -p /etc/salt/autosign-grains
echo -e <span style="color:#e6db74">&#34;master
</span><span style="color:#e6db74">bgp
</span><span style="color:#e6db74">&#34;</span> &gt; /etc/salt/autosign-grains/role
PRIVATE_IP<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>curl -s https://metadata.platformequinix.com/metadata | jq -r <span style="color:#e6db74">&#39;.network.addresses | map(select(.public==false)) | first | .address&#39;</span><span style="color:#66d9ef">)</span>
mkdir -p /srv/salt
echo interface: <span style="color:#e6db74">${</span>PRIVATE_IP<span style="color:#e6db74">}</span> &gt; /etc/salt/master.d/private-interface.conf
echo master: <span style="color:#e6db74">${</span>PRIVATE_IP<span style="color:#e6db74">}</span> &gt; /etc/salt/minion.d/master.conf
systemctl daemon-reload
systemctl enable salt-master.service
systemctl restart --no-block salt-master.service
systemctl enable salt-minion.service
systemctl restart --no-block salt-minion.service
<span style="color:#f92672">)</span>  2&gt;&amp;<span style="color:#ae81ff">1</span>
</code></pre></div><h3 id="bgp-01">bgp-01</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/bin/bash
</span><span style="color:#75715e"></span>set -e
test -n <span style="color:#e6db74">&#34;</span>$JUJU_PROGRESS_FD<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">||</span> <span style="color:#f92672">(</span>exec <span style="color:#f92672">{</span>JUJU_PROGRESS_FD<span style="color:#f92672">}</span>&gt;&amp;2<span style="color:#f92672">)</span> 2&gt;/dev/null <span style="color:#f92672">&amp;&amp;</span> exec <span style="color:#f92672">{</span>JUJU_PROGRESS_FD<span style="color:#f92672">}</span>&gt;&amp;<span style="color:#ae81ff">2</span> <span style="color:#f92672">||</span> JUJU_PROGRESS_FD<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span>
<span style="color:#f92672">(</span>

<span style="color:#66d9ef">function</span> package_manager_loop <span style="color:#f92672">{</span>
    local rc<span style="color:#f92672">=</span>
    <span style="color:#66d9ef">while</span> true; <span style="color:#66d9ef">do</span>
        <span style="color:#66d9ef">if</span> <span style="color:#f92672">(</span>$*<span style="color:#f92672">)</span>; <span style="color:#66d9ef">then</span>
                <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>
        <span style="color:#66d9ef">else</span>
                rc<span style="color:#f92672">=</span>$?
        <span style="color:#66d9ef">fi</span>
        <span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> $rc -eq <span style="color:#ae81ff">100</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
                sleep 10s
                <span style="color:#66d9ef">continue</span>
        <span style="color:#66d9ef">fi</span>
        <span style="color:#66d9ef">return</span> $rc
    <span style="color:#66d9ef">done</span>
<span style="color:#f92672">}</span>

curl -fsSL https://bootstrap.saltproject.io -o install_salt.sh
sh install_salt.sh -P -x python3
PRIVATE_IP<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>curl -s https://metadata.platformequinix.com/metadata | jq -r <span style="color:#e6db74">&#39;.network.addresses | map(select(.public==false)) | first | .address&#39;</span><span style="color:#66d9ef">)</span>
echo master: 10.12.60.1 &gt; /etc/salt/minion.d/master.conf
install -D -m <span style="color:#ae81ff">400</span> /dev/null <span style="color:#e6db74">&#39;/etc/salt/grains&#39;</span>
printf <span style="color:#e6db74">&#39;%s\n&#39;</span> <span style="color:#e6db74">&#39;anycast:
</span><span style="color:#e6db74">  ipv4:
</span><span style="color:#e6db74">    - address: 147.75.32.0
</span><span style="color:#e6db74">      mask: 255.255.255.255
</span><span style="color:#e6db74">&#39;</span> &gt; <span style="color:#e6db74">&#39;/etc/salt/grains&#39;</span>
install -D -m <span style="color:#ae81ff">644</span> /dev/null <span style="color:#e6db74">&#39;/etc/salt/minion.d/minion.conf&#39;</span>
printf <span style="color:#e6db74">&#39;%s\n&#39;</span> <span style="color:#e6db74">&#39;autosign_grains:
</span><span style="color:#e6db74">- role
</span><span style="color:#e6db74">startup_states: highstate&#39;</span> &gt; <span style="color:#e6db74">&#39;/etc/salt/minion.d/minion.conf&#39;</span>
echo <span style="color:#e6db74">&#34;bgp:
</span><span style="color:#e6db74">  localip: </span><span style="color:#e6db74">${</span>PRIVATE_IP<span style="color:#e6db74">}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">role: bgp&#34;</span> &gt;&gt; /etc/salt/grains
systemctl daemon-reload
systemctl enable salt-minion.service
systemctl restart --no-block salt-minion.service
<span style="color:#f92672">)</span>  2&gt;&amp;<span style="color:#ae81ff">1</span>
</code></pre></div><h3 id="bgp-02">bgp-02</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/bin/bash
</span><span style="color:#75715e"></span>set -e
test -n <span style="color:#e6db74">&#34;</span>$JUJU_PROGRESS_FD<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">||</span> <span style="color:#f92672">(</span>exec <span style="color:#f92672">{</span>JUJU_PROGRESS_FD<span style="color:#f92672">}</span>&gt;&amp;2<span style="color:#f92672">)</span> 2&gt;/dev/null <span style="color:#f92672">&amp;&amp;</span> exec <span style="color:#f92672">{</span>JUJU_PROGRESS_FD<span style="color:#f92672">}</span>&gt;&amp;<span style="color:#ae81ff">2</span> <span style="color:#f92672">||</span> JUJU_PROGRESS_FD<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span>
<span style="color:#f92672">(</span>

<span style="color:#66d9ef">function</span> package_manager_loop <span style="color:#f92672">{</span>
    local rc<span style="color:#f92672">=</span>
    <span style="color:#66d9ef">while</span> true; <span style="color:#66d9ef">do</span>
        <span style="color:#66d9ef">if</span> <span style="color:#f92672">(</span>$*<span style="color:#f92672">)</span>; <span style="color:#66d9ef">then</span>
                <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>
        <span style="color:#66d9ef">else</span>
                rc<span style="color:#f92672">=</span>$?
        <span style="color:#66d9ef">fi</span>
        <span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> $rc -eq <span style="color:#ae81ff">100</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
                sleep 10s
                <span style="color:#66d9ef">continue</span>
        <span style="color:#66d9ef">fi</span>
        <span style="color:#66d9ef">return</span> $rc
    <span style="color:#66d9ef">done</span>
<span style="color:#f92672">}</span>

curl -fsSL https://bootstrap.saltproject.io -o install_salt.sh
sh install_salt.sh -P -x python3
PRIVATE_IP<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>curl -s https://metadata.platformequinix.com/metadata | jq -r <span style="color:#e6db74">&#39;.network.addresses | map(select(.public==false)) | first | .address&#39;</span><span style="color:#66d9ef">)</span>
echo master: 10.12.60.1 &gt; /etc/salt/minion.d/master.conf
install -D -m <span style="color:#ae81ff">400</span> /dev/null <span style="color:#e6db74">&#39;/etc/salt/grains&#39;</span>
printf <span style="color:#e6db74">&#39;%s\n&#39;</span> <span style="color:#e6db74">&#39;anycast:
</span><span style="color:#e6db74">  ipv4:
</span><span style="color:#e6db74">    - address: 147.75.32.0
</span><span style="color:#e6db74">      mask: 255.255.255.255
</span><span style="color:#e6db74">&#39;</span> &gt; <span style="color:#e6db74">&#39;/etc/salt/grains&#39;</span>
install -D -m <span style="color:#ae81ff">644</span> /dev/null <span style="color:#e6db74">&#39;/etc/salt/minion.d/minion.conf&#39;</span>
printf <span style="color:#e6db74">&#39;%s\n&#39;</span> <span style="color:#e6db74">&#39;autosign_grains:
</span><span style="color:#e6db74">- role
</span><span style="color:#e6db74">startup_states: highstate&#39;</span> &gt; <span style="color:#e6db74">&#39;/etc/salt/minion.d/minion.conf&#39;</span>
echo <span style="color:#e6db74">&#34;bgp:
</span><span style="color:#e6db74">  localip: </span><span style="color:#e6db74">${</span>PRIVATE_IP<span style="color:#e6db74">}</span><span style="color:#e6db74">
</span><span style="color:#e6db74">role: bgp&#34;</span> &gt;&gt; /etc/salt/grains
systemctl daemon-reload
systemctl enable salt-minion.service
systemctl restart --no-block salt-minion.service
<span style="color:#f92672">)</span>  2&gt;&amp;<span style="color:#ae81ff">1</span>
</code></pre></div><h2 id="does-it-work">Does it work?</h2>
<p>If it didn&rsquo;t work after creating all of the Pulumi code and Saltstack configuration, it would be disappointing wouldn&rsquo;t it?</p>
<p>We shall verify functionality by testing first if we can reach the anycast IP in a browser. Then, we&rsquo;ll stop the <code>nginx</code> service on the &ldquo;active&rdquo; node (i.e. whichever node the requests to the anycast IP are being forwarded to), and see if it fails over to the other node.</p>
<h3 id="can-we-reach-it">Can we reach it?</h3>
<p>First, lets check that <code>nginx</code> is available on the anycast IP of <code>147.75.32.0</code>: -</p>
<p><img src="/img/pulumi-anycast/pre-service-takedown.png" alt="Anycast IP - bgp-01 nginx"></p>
<p>There it is! Other than running the <code>pulumi up</code> command, we have done nothing else to the provisioned servers. This proves that not only were the servers started, but that they sourced our Salt configuration correctly, discovered the correct grains for configuring ExaBGP and the anycast IP interface, and that the states ran with no intervention.</p>
<h3 id="taking-the-service-down-on-the-active-node">Taking the service down on the &ldquo;active&rdquo; node</h3>
<p>We will now take down <code>nginx</code> on <code>bgp-01</code> to make sure that ExaBGP will stop advertising the route when <code>nginx</code> isn&rsquo;t available.</p>
<p>We can do this from the <code>salt-master</code>. While we could login to <code>bgp-01</code> directly to take down the service, it means we don&rsquo;t then have to login to <code>bgp-02</code> to take down services if we need to: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ssh root@145.40.96.7
Welcome to Ubuntu 20.04.2 LTS <span style="color:#f92672">(</span>GNU/Linux 5.4.0-71-generic x86_64<span style="color:#f92672">)</span>

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Fri <span style="color:#ae81ff">14</span> May <span style="color:#ae81ff">2021</span> 02:53:24 PM UTC

  System load:            0.0
  Usage of /:             0.5% of 438.11GB
  Memory usage:           3%
  Swap usage:             0%
  Temperature:            39.0 C
  Processes:              <span style="color:#ae81ff">286</span>
  Users logged in:        <span style="color:#ae81ff">0</span>
  IPv4 address <span style="color:#66d9ef">for</span> bond0: 145.40.96.7
  IPv6 address <span style="color:#66d9ef">for</span> bond0: 2604:1380:4601:5200::1

 * Pure upstream Kubernetes 1.21, smallest, simplest cluster ops!

     https://microk8s.io/

<span style="color:#ae81ff">46</span> updates can be installed immediately.
<span style="color:#ae81ff">13</span> of these updates are security updates.
To see these additional updates run: apt list --upgradable

The programs included with the Ubuntu system are free software;
the exact distribution terms <span style="color:#66d9ef">for</span> each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

root@salt-master:~# salt-key -L
Accepted Keys:
bgp-01
bgp-02
salt-master
Denied Keys:
Unaccepted Keys:
Rejected Keys:

root@salt-master:~# salt <span style="color:#e6db74">&#39;bgp-01&#39;</span> service.stop <span style="color:#e6db74">&#39;nginx&#39;</span>
bgp-01:
    True
</code></pre></div><p>Do we see requests to the anycast IP go to <code>bgp-02</code>?</p>
<p><img src="/img/pulumi-anycast/post-service-takedown.png" alt="Anycast IP - bgp-02 nginx"></p>
<p>Yes we do! Success.</p>
<h3 id="bring-the-service-back-up">Bring the service back up</h3>
<p>We will now bring the service back up on <code>bgp-01</code>, and see if traffic moves back over: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">root@salt-master:~# salt <span style="color:#e6db74">&#39;bgp-01&#39;</span> service.start <span style="color:#e6db74">&#39;nginx&#39;</span>
bgp-01:
    True
</code></pre></div><p>Does the anycast traffic go back to <code>bgp-01</code>?</p>
<p><img src="/img/pulumi-anycast/after-service-up.png" alt="Anycast IP - bgp-01 nginx service resumption"></p>
<p>Yes it does!</p>
<p>There is no guarantee that traffic <strong>should</strong> go back to <code>bgp-01</code>, instead being dependent upon the Equinix network topology as to whether <code>bgp-01</code> or <code>bgp-02</code> is a preferred path. In some cases, the paths may be evaluated as equal, at which point the <strong>oldest</strong> route is preferred. If we withdraw the route from <code>bgp-01</code> only, <code>bgp-02</code> will now have the oldest route, even when <code>bgp-01</code> reannounces the route.</p>
<h3 id="exabgp-logs">ExaBGP logs</h3>
<p>Lets take a look at some of the ExaBGP log messages that show the routes being announced and withdrawn: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">May 14 14:53:59 bgp-01 exabgp[15320]: 14:53:59 | 15320  | api             | route added to neighbor 169.254.255.1 local-ip 10.12.60.3 local-as 65000 peer-as 65530 router-id 10.12.60.3 family-allowed in-open, neighbor 169.254.255.2 local-ip 10.12.60.3 local-as 65000 peer-as 65530 router-id 10.12.60.3 family-allowed in-open : 147.75.32.0/32 next-hop 10.12.60.3
May 14 14:54:04 bgp-01 exabgp[15320]: 14:54:04 | 15320  | api             | route added to neighbor 169.254.255.1 local-ip 10.12.60.3 local-as 65000 peer-as 65530 router-id 10.12.60.3 family-allowed in-open, neighbor 169.254.255.2 local-ip 10.12.60.3 local-as 65000 peer-as 65530 router-id 10.12.60.3 family-allowed in-open : 147.75.32.0/32 next-hop 10.12.60.3
May 14 14:54:09 bgp-01 exabgp[15320]: 14:54:09 | 15320  | api             | route removed from neighbor 169.254.255.1 local-ip 10.12.60.3 local-as 65000 peer-as 65530 router-id 10.12.60.3 family-allowed in-open, neighbor 169.254.255.2 local-ip 10.12.60.3 local-as 65000 peer-as 65530 router-id 10.12.60.3 family-allowed in-open : 147.75.32.0/32 next-hop 10.12.60.3
May 14 14:54:14 bgp-01 exabgp[15320]: 14:54:14 | 15320  | api             | route removed from neighbor 169.254.255.1 local-ip 10.12.60.3 local-as 65000 peer-as 65530 router-id 10.12.60.3 family-allowed in-open, neighbor 169.254.255.2 local-ip 10.12.60.3 local-as 65000 peer-as 65530 router-id 10.12.60.3 family-allowed in-open : 147.75.32.0/32 next-hop 10.12.60.3
May 14 14:54:19 bgp-01 exabgp[15320]: 14:54:19 | 15320  | api             | route removed from neighbor 169.254.255.1 local-ip 10.12.60.3 local-as 65000 peer-as 65530 router-id 10.12.60.3 family-allowed in-open, neighbor 169.254.255.2 local-ip 10.12.60.3 local-as 65000 peer-as 65530 router-id 10.12.60.3 family-allowed in-open : 147.75.32.0/32 next-hop 10.12.60.3
May 14 14:54:24 bgp-01 exabgp[15320]: 14:54:24 | 15320  | api             | route added to neighbor 169.254.255.1 local-ip 10.12.60.3 local-as 65000 peer-as 65530 router-id 10.12.60.3 family-allowed in-open, neighbor 169.254.255.2 local-ip 10.12.60.3 local-as 65000 peer-as 65530 router-id 10.12.60.3 family-allowed in-open : 147.75.32.0/32 next-hop 10.12.60.3
May 14 14:54:29 bgp-01 exabgp[15320]: 14:54:29 | 15320  | api             | route added to neighbor 169.254.255.1 local-ip 10.12.60.3 local-as 65000 peer-as 65530 router-id 10.12.60.3 family-allowed in-open, neighbor 169.254.255.2 local-ip 10.12.60.3 local-as 65000 peer-as 65530 router-id 10.12.60.3 family-allowed in-open : 147.75.32.0/32 next-hop 10.12.60.3
</code></pre></div><p>We can see that at 14:54:09, the <code>nginx-check</code> script failed to reach <code>nginx</code> on <code>bgp-01</code>, and told ExaBGP to withdraw the anycast route. ExaBGP then generated a BGP withdrawal for the anycast route to both Equinix peers.</p>
<p>15 seconds later, we started <code>nginx</code> again, and we stared advertising the route again.</p>
<h2 id="summary">Summary</h2>
<p>Conditional route advertisement with ExaBGP is exceedingly powerful, and allows us to build highly available services without necessarily requiring clustering protocols (e.g. <code>corosync</code>) or something like VRRP (which relies on Layer 2 connectivity between peers).</p>
<p>Being able to use Pulumi and Saltstack to configure this as well means that you can quite easily scale up how many anycast nodes you required. Change the BGP node creation to be inside of a <code>for</code> loop, and you can create as many nodes as you have money to pay for!</p>
<p>I want to thank <a href="https://twitter.com/rawkode">David McKay/Rawkode</a> again, in part for the inspiration (through the Tinkerbell testing infrastructure) and planting some seeds in my brain with the <a href="https://www.youtube.com/watch?v=ku786TprTD8&amp;list=PLz0t90fOInA43P-6LS5iAbvLpOvZK7K2M&amp;index=2">Metal Monday</a> and <a href="https://www.youtube.com/watch?v=ZsYU7LLnm7E&amp;list=PLz0t90fOInA5kxMPWhvR7Og1Y4fcz-3xO&amp;index=3">Rawkode Live series</a>.</p>
]]></content>
        </item>
        
        <item>
            <title>Pulumi: Deploying infrastructure on other providers</title>
            <link>https://yetiops.net/posts/pulumi-more-providers/</link>
            <pubDate>Mon, 12 Apr 2021 21:50:42 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/pulumi-more-providers/</guid>
            <description>In a previous post, I covered using Pulumi for Infrastructure-as-Code as an alternative to Terraform. The post focussed on building an AWS EC2 (a virtual machine in AWS&amp;rsquo;s cloud) to demonstrate the differences between not only Terraform and Pulumi, but also between using Python, Go or Typescript with Pulumi.
In this post, I am going to cover building machines on four different providers, all using Go. As mentioned in the previous post, Go is my favourite language to write in, so defining infrastructure using it is an enjoyable experience.</description>
            <content type="html"><![CDATA[<p>In a <a href="/posts/pulumi-iac-terraform">previous post</a>, I covered using <a href="https://www.pulumi.com/">Pulumi</a> for Infrastructure-as-Code as an alternative to <a href="https://terraform.io">Terraform</a>. The post focussed on building an AWS EC2 (a virtual machine in AWS&rsquo;s cloud) to demonstrate the differences between not only Terraform and Pulumi, but also between using Python, Go or Typescript with Pulumi.</p>
<p>In this post, I am going to cover building machines on four different providers, all using Go. As mentioned in the previous post, Go is my favourite language to write in, so defining infrastructure using it is an enjoyable  experience. However the approach used could be applied to any of the other supported languages too (Python, Typescript, Javascript or DotNet).</p>
<h2 id="providers">Providers</h2>
<p>The providers we are going to build infrastructure on are: -</p>
<ul>
<li><a href="https://digitalocean.com">Digital Ocean</a> - As covered in <a href="/posts/prometheus-service-discovery-digitalocean/">this post</a> using Terraform</li>
<li><a href="https://www.hetzner.com/cloud">Hetzner Cloud</a> - As covered in <a href="/posts/prometheus-service-discovery-hetzner-hcloud/">this post</a> using Terraform
<ul>
<li>We also are now using the new Hetzner Firewalls feature (which wasn&rsquo;t available when writing the previous post)</li>
</ul>
</li>
<li><a href="https://metal.equinix.com/">Equinix Metal</a> - Formerly known as Packet, providing bare-metal as a service with a very Cloud-like experience</li>
<li><a href="https://www.linode.com/">Linode</a> - Linux-focussed VPSs, Kubernetes Clusters, Object Storage and more</li>
</ul>
<p>I chose these for a variety of reasons, but mostly to show the breadth of support that Pulumi has for a number of providers, as well as making it clear that you don&rsquo;t need to be running complex cloud infrastructure to make use of Pulumi.</p>
<h2 id="digital-ocean">Digital Ocean</h2>
<p><em>Pulumi Documentation</em>: <a href="https://www.pulumi.com/docs/intro/cloud-providers/digitalocean/">here</a></p>
<p>If you don&rsquo;t have an API key with Digital Ocean already, you can follow the instructions <a href="/posts/prometheus-service-discovery-digitalocean/#digital-ocean">here</a> to create a project and generate a key for use with Pulumi. You can then choose whether you store your token in the Pulumi configuration, or exposing it as an environment variable that Pulumi will use.</p>
<p>Run the following to create a stack and project for the code: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ pulumi new digitalocean-go --name <span style="color:#66d9ef">do</span>-go
</code></pre></div><p>This will ask for a name of the stack. I chose <code>staging</code>, but this is entirely up to you. To add your API token to the configuration, run the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ pulumi config set digitalocean:token XXXXXXXXXXXXXX --secret
</code></pre></div><p>This will be stored in your configuration as an encrypted secret. I prefer to use environment variables, exporting it like so: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ export DIGITALOCEAN_TOKEN<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;XXXXXXXXXXXXXX&#34;</span>
</code></pre></div><p>In addition, we&rsquo;ll set our <code>common_name</code> variable, to be used across our resources: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ pulumi config set common_name yetiops-prom
</code></pre></div><p>Now we can build our infrastructure.</p>
<h3 id="the-code">The code</h3>
<p>Like in the previous Digital Ocean <a href="/posts/prometheus-service-discovery-digitalocean/#define-the-infrastructure---droplets">post</a>, we will build the following: -</p>
<ul>
<li>A Digital Ocean Droplet (VPS) running Ubuntu 20.04, with the Prometheus <code>node_exporter</code> installed (using <code>cloud-config</code>)</li>
<li>An SSH key that will be added to the VPS</li>
<li>Some tags assigned to the instance</li>
<li>A firewall to limit what can and cannot communicate with our instance</li>
</ul>
<p>First, we reference our <code>common_name</code> variable to be used by the resources: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">		<span style="color:#a6e22e">conf</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;&#34;</span>)
		<span style="color:#a6e22e">commonName</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">conf</span>.<span style="color:#a6e22e">Require</span>(<span style="color:#e6db74">&#34;common_name&#34;</span>)
</code></pre></div><p>After this, we will define our <code>cloudconfig</code> configuration as we did for our AWS EC2 in the <a href="/posts/pulumi-iac-terraform/#pulumi-using-go">previous Pulumi post</a>.</p>
<p>I have changed the approach slightly, in that our template is now stored in a separate file (rather than a multi-line variable). The location is also defined using another configuration variable, set with <code>pulumi config set cloud_init_path &quot;files/ubuntu.tpl&quot;</code>. Now we can update the configuration and location without requiring code changes in future.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">        <span style="color:#a6e22e">cloudInitPath</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">conf</span>.<span style="color:#a6e22e">Require</span>(<span style="color:#e6db74">&#34;cloud_init_path&#34;</span>)

		<span style="color:#a6e22e">cloudInitScript</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">cloudInitPath</span>)

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">cloudInitContents</span> <span style="color:#f92672">:=</span> string(<span style="color:#a6e22e">cloudInitScript</span>)

		<span style="color:#a6e22e">b64encEnable</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">false</span>
		<span style="color:#a6e22e">gzipEnable</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">false</span>
		<span style="color:#a6e22e">contentType</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;text/cloud-config&#34;</span>
		<span style="color:#a6e22e">fileName</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;init.cfg&#34;</span>
		<span style="color:#a6e22e">cloudconfig</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">LookupConfig</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">LookupConfigArgs</span>{
			<span style="color:#a6e22e">Base64Encode</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">b64encEnable</span>,
			<span style="color:#a6e22e">Gzip</span>:         <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">gzipEnable</span>,
			<span style="color:#a6e22e">Parts</span>: []<span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">GetConfigPart</span>{
				<span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">GetConfigPart</span>{
					<span style="color:#a6e22e">Content</span>:     <span style="color:#a6e22e">cloudInitContents</span>,
					<span style="color:#a6e22e">ContentType</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">contentType</span>,
					<span style="color:#a6e22e">Filename</span>:    <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">fileName</span>,
				},
			},
		}, <span style="color:#66d9ef">nil</span>)

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}
</code></pre></div><p>If we decide we want to install more packages, add users, or use any other feature of <code>cloudinit</code>, we update the template file instead.</p>
<p>Next we define an SSH key that we will use to login to this instance: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">		<span style="color:#a6e22e">user</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">Current</span>()

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">sshkey_path</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%v/.ssh/id_rsa.pub&#34;</span>, <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">HomeDir</span>)

		<span style="color:#a6e22e">sshkey_file</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">sshkey_path</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">sshkey_contents</span> <span style="color:#f92672">:=</span> string(<span style="color:#a6e22e">sshkey_file</span>)

		<span style="color:#a6e22e">sshkey</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">NewSshKey</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">SshKeyArgs</span>{
			<span style="color:#a6e22e">Name</span>:      <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">commonName</span>),
			<span style="color:#a6e22e">PublicKey</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">sshkey_contents</span>),
		})
</code></pre></div><p>This is very similar to what was defined in our AWS post, with just a couple of field names changed.</p>
<p>Many of the Pulumi providers use very similar definitions, fields and arguments, meaning that we can reuse a lot of code across different providers. It also means that we could create reusable functions/packages that can be imported into the stacks, with only the Pulumi-specific code in our <code>main.go</code> files. This cuts down on duplication, and allow us to make changes to a function in one place, meaning all dependent Pulumi stacks would also benefit from the changes.</p>
<p>We use the same function we defined before to get our IP address: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">getMyIp</span>() (<span style="color:#66d9ef">string</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#e6db74">&#34;https://ifconfig.co&#34;</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">err</span>
	}

	<span style="color:#a6e22e">body</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadAll</span>(<span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Body</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">err</span>
	}

	<span style="color:#a6e22e">MyIp</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">TrimSuffix</span>(string(<span style="color:#a6e22e">body</span>), <span style="color:#e6db74">&#34;\n&#34;</span>)

	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">MyIp</span>, <span style="color:#66d9ef">nil</span>
}

[<span style="color:#f92672">...</span>]
		<span style="color:#a6e22e">myIp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">getMyIp</span>()
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">myIpCidr</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%v/32&#34;</span>, <span style="color:#a6e22e">myIp</span>)
</code></pre></div><p>This retrieves our public IPv4 address, allowing us to limit what machine(s) will be able to login to the Droplet.</p>
<p>Next, we define tags that we want to apply to the Droplet: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">		<span style="color:#a6e22e">promtag</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">NewTag</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;prometheus&#34;</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">TagArgs</span>{
			<span style="color:#a6e22e">Name</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;prometheus&#34;</span>),
		})

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">nodeex_tag</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">NewTag</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;node_exporter&#34;</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">TagArgs</span>{
			<span style="color:#a6e22e">Name</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;node_exporter&#34;</span>),
		})

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}
</code></pre></div><p>Now that these are defined, we can configure our Droplet: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">		<span style="color:#a6e22e">droplet</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">NewDroplet</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">DropletArgs</span>{
			<span style="color:#a6e22e">Name</span>:     <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">commonName</span>),
			<span style="color:#a6e22e">Image</span>:    <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;ubuntu-20-04-x64&#34;</span>),
			<span style="color:#a6e22e">Region</span>:   <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;fra1&#34;</span>),
			<span style="color:#a6e22e">Size</span>:     <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;s-1vcpu-1gb&#34;</span>),
			<span style="color:#a6e22e">UserData</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">cloudconfig</span>.<span style="color:#a6e22e">Rendered</span>),
			<span style="color:#a6e22e">SshKeys</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
				<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringOutput</span>(<span style="color:#a6e22e">sshkey</span>.<span style="color:#a6e22e">Fingerprint</span>),
			},
			<span style="color:#a6e22e">Tags</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
				<span style="color:#a6e22e">promtag</span>.<span style="color:#a6e22e">ID</span>(),
				<span style="color:#a6e22e">nodeex_tag</span>.<span style="color:#a6e22e">ID</span>(),
			},
		})
</code></pre></div><p>This is very similar to how we would define the EC2 in the previous Pulumi <a href="/posts/pulumi-iac-terraform/#pulumi-using-go">post</a>. The main difference is that Digital Ocean have a validated set of images that you choose, rather than a vast array of AMIs that practically anyone can create. We also must supply tags as resources rather than just <strong>key-value</strong> pairs like in AWS.</p>
<p>Finally, we define our firewall: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">		<span style="color:#a6e22e">droplet_id</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%v&#34;</span>, <span style="color:#a6e22e">droplet</span>.<span style="color:#a6e22e">ID</span>())

		<span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">NewFirewall</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">FirewallArgs</span>{
			<span style="color:#a6e22e">Name</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">commonName</span>),
			<span style="color:#a6e22e">DropletIds</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">IntArray</span>{
				<span style="color:#a6e22e">droplet_id</span>.<span style="color:#a6e22e">ApplyInt</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">id</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">int</span> {
					<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">idInt</span> <span style="color:#66d9ef">int</span>
					<span style="color:#a6e22e">idInt</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strconv</span>.<span style="color:#a6e22e">Atoi</span>(<span style="color:#a6e22e">id</span>)
					<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
						<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">err</span>)
						<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">idInt</span>
					}
					<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">idInt</span>
				}),
			},
			<span style="color:#a6e22e">InboundRules</span>: <span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">FirewallInboundRuleArray</span>{
				<span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">FirewallInboundRuleArgs</span>{
					<span style="color:#a6e22e">Protocol</span>:  <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;tcp&#34;</span>),
					<span style="color:#a6e22e">PortRange</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;22&#34;</span>),
					<span style="color:#a6e22e">SourceAddresses</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
						<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">myIpCidr</span>),
					},
				},
				<span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">FirewallInboundRuleArgs</span>{
					<span style="color:#a6e22e">Protocol</span>:  <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;tcp&#34;</span>),
					<span style="color:#a6e22e">PortRange</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;9100&#34;</span>),
					<span style="color:#a6e22e">SourceAddresses</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
						<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">myIpCidr</span>),
					},
				},
			},
			<span style="color:#a6e22e">OutboundRules</span>: <span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">FirewallOutboundRuleArray</span>{
				<span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">FirewallOutboundRuleArgs</span>{
					<span style="color:#a6e22e">Protocol</span>:  <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;tcp&#34;</span>),
					<span style="color:#a6e22e">PortRange</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;1-65535&#34;</span>),
					<span style="color:#a6e22e">DestinationAddresses</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
						<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>),
						<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;::/0&#34;</span>),
					},
				},
				<span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">FirewallOutboundRuleArgs</span>{
					<span style="color:#a6e22e">Protocol</span>:  <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;udp&#34;</span>),
					<span style="color:#a6e22e">PortRange</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;1-65535&#34;</span>),
					<span style="color:#a6e22e">DestinationAddresses</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
						<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>),
						<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;::/0&#34;</span>),
					},
				},
				<span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">FirewallOutboundRuleArgs</span>{
					<span style="color:#a6e22e">Protocol</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;icmp&#34;</span>),
					<span style="color:#a6e22e">DestinationAddresses</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
						<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>),
						<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;::/0&#34;</span>),
					},
				},
			},
		})
</code></pre></div><p>There a few parts to explain here. We create this after the Droplet because Digital Ocean Firewalls require a list of Droplets they apply to, rather than telling the Droplet what firewalls apply to it. This is a subtle difference from AWS, Azure or other providers, but worth taking into account.</p>
<p>Also, the Digital Ocean provider returns the ID of the Droplet as a Go type of <code>pulumi.IDOutput</code>, but we need to supply this to the <code>NewFirewall</code> function as an integer. To translate between the two, we need to first turn the <code>IDOutput</code> into a <code>StringOutput</code> using: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#a6e22e">droplet_id</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%v&#34;</span>, <span style="color:#a6e22e">droplet</span>.<span style="color:#a6e22e">ID</span>())
</code></pre></div><p>After this, we need to convert this into an <code>IntOutput</code>. This is achieve using Pulumi&rsquo;s <a href="https://www.pulumi.com/docs/intro/concepts/inputs-outputs/#apply">Apply</a> functionality. The apply function will take the <code>StringOutput</code> value and turn it into another form of <code>Output</code> (in this case, <code>IntOutput</code>). The reason that this doesn&rsquo;t return a standard Go <code>Int</code> type is that it also maintains dependencies and information associated with the original <code>Output</code> value (including when it will be computed and what it refers to). Without this, the value may be computed before it is available, as well as losing any history/attributes of the original <code>Output</code> value.</p>
<p>There is a lot to process here, and it has taken a few attempts at different functions for me to find the right way to do this. Once you realise the motivation behind using the <code>Apply</code> functions rather than just raw values though, it can help you understand how to build them</p>
<p>The code for this is: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#a6e22e">DropletIds</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">IntArray</span>{
    <span style="color:#a6e22e">droplet_id</span>.<span style="color:#a6e22e">ApplyInt</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">id</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">int</span> {
        <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">idInt</span> <span style="color:#66d9ef">int</span>
        <span style="color:#a6e22e">idInt</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strconv</span>.<span style="color:#a6e22e">Atoi</span>(<span style="color:#a6e22e">id</span>)
        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
            <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">err</span>)
            <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">idInt</span>
        }
        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">idInt</span>
    }),
</code></pre></div><p>The <code>pulumi.IntArray</code> declaration is expecting an <code>Array</code> of Pulumi-typed Integers (e.g. <code>pulumi.Int</code>, <code>pulumi.IntOutput</code>). Supplying only <code>droplet_id</code> will as it is of type <code>pulumi.StringOutput</code>. Instead we take the <code>droplet_id</code> variable, and use the Pulumi <code>ApplyInt</code> function against it. This will return a <code>pulumi.IntOutput</code> type as long as the the function running returns an Integer.</p>
<p>The function itself has an input of <code>id string</code> (i.e. the value of droplet_id), and returns an <code>int</code> (Integer). We use the <code>strconv</code> package to convert the <code>String</code> to an <code>Int</code>. We need to define the <code>var idInt int</code> variable as we must return an integer, even if the conversion failed. Defining the integer <code>idInt</code> means that the return value is always an <code>Int</code>, it just may be a zero-value <code>Int</code>.</p>
<p>So long as the string conversion succeeded, we return the ID of the droplet as an integer, satisfying the requirement in the <code>pulumi.IntArray</code>.</p>
<p>Once you have created a few of these <code>Apply</code> functions, they become much easier to understand. The first few times though, you may need to play around with it, and even try and make it fail to see what is happening.</p>
<p>In regards to the firewalls and rules themselves, currently you cannot create an &ldquo;any protocol&rdquo; rule. Instead, we must define three separate rules for TCP, UDP and ICMP. Also, if you specify TCP or UDP, a range of ports is also required, even if this is all ports. The rules can apply to both both IPv4 and IPv6, which can reduce some duplication of code.</p>
<h3 id="running-pulumi">Running Pulumi</h3>
<p>We can now run <code>pulumi up</code> and see our Droplet build: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ Previewing update <span style="color:#f92672">(</span>staging<span style="color:#f92672">)</span>

View Live: https://app.pulumi.com/yetiops/do-go/staging/previews/59056b53-d392-4474-9c17-b9c1529de066

     Type                            Name           Plan
 +   pulumi:pulumi:Stack             <span style="color:#66d9ef">do</span>-go-staging  create
 +   ├─ digitalocean:index:SshKey    yetiops-prom   create
 +   ├─ digitalocean:index:Tag       prometheus     create
 +   ├─ digitalocean:index:Tag       node_exporter  create
 +   ├─ digitalocean:index:Droplet   yetiops-prom   create
 +   └─ digitalocean:index:Firewall  yetiops-prom   create

Resources:
    + <span style="color:#ae81ff">6</span> to create

Do you want to perform this update? yes
Updating <span style="color:#f92672">(</span>staging<span style="color:#f92672">)</span>

View Live: https://app.pulumi.com/yetiops/do-go/staging/updates/5

     Type                            Name           Status
 +   pulumi:pulumi:Stack             <span style="color:#66d9ef">do</span>-go-staging  created
 +   ├─ digitalocean:index:Tag       node_exporter  created
 +   ├─ digitalocean:index:SshKey    yetiops-prom   created
 +   ├─ digitalocean:index:Tag       prometheus     created
 +   ├─ digitalocean:index:Droplet   yetiops-prom   created
 +   └─ digitalocean:index:Firewall  yetiops-prom   created

Outputs:
    publicIp: <span style="color:#e6db74">&#34;68.183.219.18&#34;</span>

Resources:
    + <span style="color:#ae81ff">6</span> created

Duration: 57s
</code></pre></div><p>We can also see this in the Digital Ocean console: -</p>
<p><img src="/img/pulumi/droplet.png" alt="Digital Ocean Droplet in the Digital Ocean Console"></p>
<p>Lets login to it and see what has been deployed: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ssh -i ~/.ssh/id_rsa root@68.183.219.18
Welcome to Ubuntu 20.04.1 LTS <span style="color:#f92672">(</span>GNU/Linux 5.4.0-51-generic x86_64<span style="color:#f92672">)</span>

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

 System information disabled due to load higher than 1.0

<span style="color:#ae81ff">136</span> updates can be installed immediately.
<span style="color:#ae81ff">64</span> of these updates are security updates.
To see these additional updates run: apt list --upgradable


The programs included with the Ubuntu system are free software;
the exact distribution terms <span style="color:#66d9ef">for</span> each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

root@yetiops-prom:~# ss -tlunp
Netid               State                Recv-Q                Send-Q                               Local Address:Port                               Peer Address:Port               Process
udp                 UNCONN               <span style="color:#ae81ff">0</span>                     <span style="color:#ae81ff">0</span>                                    127.0.0.53%lo:53                                      0.0.0.0:*                   users:<span style="color:#f92672">((</span><span style="color:#e6db74">&#34;systemd-resolve&#34;</span>,pid<span style="color:#f92672">=</span>492,fd<span style="color:#f92672">=</span>12<span style="color:#f92672">))</span>
tcp                 LISTEN               <span style="color:#ae81ff">0</span>                     <span style="color:#ae81ff">4096</span>                                 127.0.0.53%lo:53                                      0.0.0.0:*                   users:<span style="color:#f92672">((</span><span style="color:#e6db74">&#34;systemd-resolve&#34;</span>,pid<span style="color:#f92672">=</span>492,fd<span style="color:#f92672">=</span>13<span style="color:#f92672">))</span>
tcp                 LISTEN               <span style="color:#ae81ff">0</span>                     <span style="color:#ae81ff">128</span>                                        0.0.0.0:22                                      0.0.0.0:*                   users:<span style="color:#f92672">((</span><span style="color:#e6db74">&#34;sshd&#34;</span>,pid<span style="color:#f92672">=</span>776,fd<span style="color:#f92672">=</span>3<span style="color:#f92672">))</span>
tcp                 LISTEN               <span style="color:#ae81ff">0</span>                     <span style="color:#ae81ff">4096</span>                                             *:9100                                          *:*                   users:<span style="color:#f92672">((</span><span style="color:#e6db74">&#34;prometheus-node&#34;</span>,pid<span style="color:#f92672">=</span>2277,fd<span style="color:#f92672">=</span>3<span style="color:#f92672">))</span>
tcp                 LISTEN               <span style="color:#ae81ff">0</span>                     <span style="color:#ae81ff">128</span>                                           <span style="color:#f92672">[</span>::<span style="color:#f92672">]</span>:22                                         <span style="color:#f92672">[</span>::<span style="color:#f92672">]</span>:*                   users:<span style="color:#f92672">((</span><span style="color:#e6db74">&#34;sshd&#34;</span>,pid<span style="color:#f92672">=</span>776,fd<span style="color:#f92672">=</span>4<span style="color:#f92672">))</span>

root@yetiops-prom:~# curl localhost:9100/metrics | grep -i yetiops
node_uname_info<span style="color:#f92672">{</span>domainname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(none)&#34;</span>,machine<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;x86_64&#34;</span>,nodename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;yetiops-prom&#34;</span>,release<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;5.4.0-51-generic&#34;</span>,sysname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Linux&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;#56-Ubuntu SMP Mon Oct 5 14:28:49 UTC 2020&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>
</code></pre></div><p>We&rsquo;re in, the <code>node_exporter</code> is installed, and all is looking good. Note that unlike in AWS, the SSH keys we create apply to the <code>root</code> account, rather than the <code>ubuntu</code> account. In fact, no <code>ubuntu</code> account exists: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">root@yetiops-prom:~# groups ubuntu
groups: ‘ubuntu’: no such user
root@yetiops-prom:~#
</code></pre></div><h3 id="all-the-code">All the code</h3>
<p>The below is the full <code>main.go</code> file with our code in: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;io/ioutil&#34;</span>
	<span style="color:#e6db74">&#34;net/http&#34;</span>
	<span style="color:#e6db74">&#34;os/user&#34;</span>
	<span style="color:#e6db74">&#34;strconv&#34;</span>
	<span style="color:#e6db74">&#34;strings&#34;</span>

	<span style="color:#e6db74">&#34;github.com/pulumi/pulumi-cloudinit/sdk/go/cloudinit&#34;</span>
	<span style="color:#e6db74">&#34;github.com/pulumi/pulumi-digitalocean/sdk/v3/go/digitalocean&#34;</span>
	<span style="color:#e6db74">&#34;github.com/pulumi/pulumi/sdk/v2/go/pulumi&#34;</span>
	<span style="color:#e6db74">&#34;github.com/pulumi/pulumi/sdk/v2/go/pulumi/config&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">getMyIp</span>() (<span style="color:#66d9ef">string</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#e6db74">&#34;https://ifconfig.co&#34;</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">err</span>
	}

	<span style="color:#a6e22e">body</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadAll</span>(<span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Body</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">err</span>
	}

	<span style="color:#a6e22e">MyIp</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">TrimSuffix</span>(string(<span style="color:#a6e22e">body</span>), <span style="color:#e6db74">&#34;\n&#34;</span>)

	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">MyIp</span>, <span style="color:#66d9ef">nil</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Context</span>) <span style="color:#66d9ef">error</span> {
		<span style="color:#a6e22e">conf</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;&#34;</span>)
		<span style="color:#a6e22e">commonName</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">conf</span>.<span style="color:#a6e22e">Require</span>(<span style="color:#e6db74">&#34;common_name&#34;</span>)
		<span style="color:#a6e22e">cloudInitPath</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">conf</span>.<span style="color:#a6e22e">Require</span>(<span style="color:#e6db74">&#34;cloud_init_path&#34;</span>)

		<span style="color:#a6e22e">cloudInitScript</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">cloudInitPath</span>)

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">cloudInitContents</span> <span style="color:#f92672">:=</span> string(<span style="color:#a6e22e">cloudInitScript</span>)

		<span style="color:#a6e22e">b64encEnable</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">false</span>
		<span style="color:#a6e22e">gzipEnable</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">false</span>
		<span style="color:#a6e22e">contentType</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;text/cloud-config&#34;</span>
		<span style="color:#a6e22e">fileName</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;init.cfg&#34;</span>
		<span style="color:#a6e22e">cloudconfig</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">LookupConfig</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">LookupConfigArgs</span>{
			<span style="color:#a6e22e">Base64Encode</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">b64encEnable</span>,
			<span style="color:#a6e22e">Gzip</span>:         <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">gzipEnable</span>,
			<span style="color:#a6e22e">Parts</span>: []<span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">GetConfigPart</span>{
				<span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">GetConfigPart</span>{
					<span style="color:#a6e22e">Content</span>:     <span style="color:#a6e22e">cloudInitContents</span>,
					<span style="color:#a6e22e">ContentType</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">contentType</span>,
					<span style="color:#a6e22e">Filename</span>:    <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">fileName</span>,
				},
			},
		}, <span style="color:#66d9ef">nil</span>)

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">user</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">Current</span>()

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">sshkey_path</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%v/.ssh/id_rsa.pub&#34;</span>, <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">HomeDir</span>)

		<span style="color:#a6e22e">sshkey_file</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">sshkey_path</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">sshkey_contents</span> <span style="color:#f92672">:=</span> string(<span style="color:#a6e22e">sshkey_file</span>)

		<span style="color:#a6e22e">sshkey</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">NewSshKey</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">SshKeyArgs</span>{
			<span style="color:#a6e22e">Name</span>:      <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">commonName</span>),
			<span style="color:#a6e22e">PublicKey</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">sshkey_contents</span>),
		})

		<span style="color:#a6e22e">myIp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">getMyIp</span>()
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">myIpCidr</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%v/32&#34;</span>, <span style="color:#a6e22e">myIp</span>)

		<span style="color:#a6e22e">promtag</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">NewTag</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;prometheus&#34;</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">TagArgs</span>{
			<span style="color:#a6e22e">Name</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;prometheus&#34;</span>),
		})

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">nodeex_tag</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">NewTag</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;node_exporter&#34;</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">TagArgs</span>{
			<span style="color:#a6e22e">Name</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;node_exporter&#34;</span>),
		})

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">droplet</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">NewDroplet</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">DropletArgs</span>{
			<span style="color:#a6e22e">Name</span>:     <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">commonName</span>),
			<span style="color:#a6e22e">Image</span>:    <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;ubuntu-20-04-x64&#34;</span>),
			<span style="color:#a6e22e">Region</span>:   <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;fra1&#34;</span>),
			<span style="color:#a6e22e">Size</span>:     <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;s-1vcpu-1gb&#34;</span>),
			<span style="color:#a6e22e">UserData</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">cloudconfig</span>.<span style="color:#a6e22e">Rendered</span>),
			<span style="color:#a6e22e">SshKeys</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
				<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringOutput</span>(<span style="color:#a6e22e">sshkey</span>.<span style="color:#a6e22e">Fingerprint</span>),
			},
			<span style="color:#a6e22e">Tags</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
				<span style="color:#a6e22e">promtag</span>.<span style="color:#a6e22e">ID</span>(),
				<span style="color:#a6e22e">nodeex_tag</span>.<span style="color:#a6e22e">ID</span>(),
			},
		})

		<span style="color:#a6e22e">droplet_id</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%v&#34;</span>, <span style="color:#a6e22e">droplet</span>.<span style="color:#a6e22e">ID</span>())

		<span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">NewFirewall</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">FirewallArgs</span>{
			<span style="color:#a6e22e">Name</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">commonName</span>),
			<span style="color:#a6e22e">DropletIds</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">IntArray</span>{
				<span style="color:#a6e22e">droplet_id</span>.<span style="color:#a6e22e">ApplyInt</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">id</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">int</span> {
					<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">idInt</span> <span style="color:#66d9ef">int</span>
					<span style="color:#a6e22e">idInt</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strconv</span>.<span style="color:#a6e22e">Atoi</span>(<span style="color:#a6e22e">id</span>)
					<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
						<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">err</span>)
						<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">idInt</span>
					}
					<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">idInt</span>
				}),
			},
			<span style="color:#a6e22e">InboundRules</span>: <span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">FirewallInboundRuleArray</span>{
				<span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">FirewallInboundRuleArgs</span>{
					<span style="color:#a6e22e">Protocol</span>:  <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;tcp&#34;</span>),
					<span style="color:#a6e22e">PortRange</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;22&#34;</span>),
					<span style="color:#a6e22e">SourceAddresses</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
						<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">myIpCidr</span>),
					},
				},
				<span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">FirewallInboundRuleArgs</span>{
					<span style="color:#a6e22e">Protocol</span>:  <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;tcp&#34;</span>),
					<span style="color:#a6e22e">PortRange</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;9100&#34;</span>),
					<span style="color:#a6e22e">SourceAddresses</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
						<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">myIpCidr</span>),
					},
				},
			},
			<span style="color:#a6e22e">OutboundRules</span>: <span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">FirewallOutboundRuleArray</span>{
				<span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">FirewallOutboundRuleArgs</span>{
					<span style="color:#a6e22e">Protocol</span>:  <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;tcp&#34;</span>),
					<span style="color:#a6e22e">PortRange</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;1-65535&#34;</span>),
					<span style="color:#a6e22e">DestinationAddresses</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
						<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>),
						<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;::/0&#34;</span>),
					},
				},
				<span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">FirewallOutboundRuleArgs</span>{
					<span style="color:#a6e22e">Protocol</span>:  <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;udp&#34;</span>),
					<span style="color:#a6e22e">PortRange</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;1-65535&#34;</span>),
					<span style="color:#a6e22e">DestinationAddresses</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
						<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>),
						<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;::/0&#34;</span>),
					},
				},
				<span style="color:#a6e22e">digitalocean</span>.<span style="color:#a6e22e">FirewallOutboundRuleArgs</span>{
					<span style="color:#a6e22e">Protocol</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;icmp&#34;</span>),
					<span style="color:#a6e22e">DestinationAddresses</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
						<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>),
						<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;::/0&#34;</span>),
					},
				},
			},
		})

		<span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Export</span>(<span style="color:#e6db74">&#34;publicIp&#34;</span>, <span style="color:#a6e22e">droplet</span>.<span style="color:#a6e22e">Ipv4Address</span>)

		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
	})
}
</code></pre></div><h2 id="hetzner">Hetzner</h2>
<p>I have covered Hetzner&rsquo;s Cloud offering <a href="/posts/prometheus-service-discovery-hetzner-hcloud/">in the past</a>, around the time that Prometheus Service Discovery supported them as a provider. As with Digital Ocean, if you follow all the instructions in that post until you have an API key, you can then choose whether to expose it as an environment variable or as part of your stack&rsquo;s configuration.</p>
<h3 id="project-initialization">Project Initialization</h3>
<p>To create the stack, the Pulumi CLI does not have any existing examples/skeleton structure to create a project. The process is a little more manual. Start with your directory of choice, and then run the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ pulumi stack init
Please enter your desired stack name.
To create a stack in an organization, use the format &lt;org-name&gt;/&lt;stack-name&gt; <span style="color:#f92672">(</span>e.g. <span style="color:#e6db74">`</span>acmecorp/dev<span style="color:#e6db74">`</span><span style="color:#f92672">)</span>.
stack name: <span style="color:#f92672">(</span>dev<span style="color:#f92672">)</span> yetiops/hetzner-go
</code></pre></div><p>Create a <code>Pulumi.yaml</code> file with the following contents: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">name</span>: hetzner-staging
<span style="color:#66d9ef">runtime</span>: go
<span style="color:#66d9ef">description</span>: Testing Hetzner with Pulumi
</code></pre></div><p>You can change the name (this is the project name, not the stack name) and the description to whatever is relevant to the project. Ensure that the runtime value matches the code you are writing (i.e. if you are writing in Python, change it to <code>python</code> rather than leaving it as <code>go</code>).</p>
<p>Create a basic <code>main.go</code> file with the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
    <span style="color:#e6db74">&#34;fmt&#34;</span>

    <span style="color:#a6e22e">hcloud</span> <span style="color:#e6db74">&#34;github.com/pulumi/pulumi-hcloud/sdk/go/hcloud&#34;</span>
    <span style="color:#e6db74">&#34;github.com/pulumi/pulumi/sdk/v2/go/pulumi&#34;</span>
    <span style="color:#e6db74">&#34;github.com/pulumi/pulumi/sdk/v2/go/pulumi/config&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
    <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;test&#34;</span>)
}
</code></pre></div><p>Finally, run <code>go mod init hetzner-go</code> (change <code>hetzner-go</code> to the directory name if it is different) and run <code>go test</code>. This will bring in all the required modules for creating Hetzner Cloud resources in Pulumi using Go.</p>
<p>If you want to store your Hetzner Cloud token in your configuration rather than as an environment variable, run <code>pulumi config set hcloud:token XXXXXXXXXXXXXX --secret</code> with the API token that you created. Otherwise, ensure you have an environment variable called <code>HCLOUD_TOKEN</code> exported for Pulumi to use.</p>
<p>Finally, we set our <code>common_name</code> variable with <code>pulumi config set common_name yetiops-prom</code> to be used when creating our resources.</p>
<h3 id="the-code-1">The code</h3>
<p>Similar to the Terraform-based post, we will create the following: -</p>
<ul>
<li>A Hetzner Cloud instance (VPS) running Ubuntu 20.04, with the Prometheus node_exporter installed (using cloud-config)</li>
<li>An SSH key for our instance to use</li>
<li>A firewall to limit what can and cannot communicate with our instance</li>
</ul>
<p>As mentioned already, Hetzner now support creation of firewalls for Cloud instances, which were not available when writing the original Hetzner <a href="/posts/prometheus-service-discovery-hetzner-hcloud/">post</a>.</p>
<p>The <code>config</code> and <code>cloudinit</code> sections are identical to what we create in Digital Ocean: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">        <span style="color:#a6e22e">conf</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;&#34;</span>)
        <span style="color:#a6e22e">commonName</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">conf</span>.<span style="color:#a6e22e">Require</span>(<span style="color:#e6db74">&#34;common_name&#34;</span>)
        <span style="color:#a6e22e">cloudInitPath</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">conf</span>.<span style="color:#a6e22e">Require</span>(<span style="color:#e6db74">&#34;cloud_init_path&#34;</span>)
        
        <span style="color:#a6e22e">cloudInitScript</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">cloudInitPath</span>)
        
        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
        	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
        }
        
        <span style="color:#a6e22e">cloudInitContents</span> <span style="color:#f92672">:=</span> string(<span style="color:#a6e22e">cloudInitScript</span>)
        
        <span style="color:#a6e22e">b64encEnable</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">false</span>
        <span style="color:#a6e22e">gzipEnable</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">false</span>
        <span style="color:#a6e22e">contentType</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;text/cloud-config&#34;</span>
        <span style="color:#a6e22e">fileName</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;init.cfg&#34;</span>
        <span style="color:#a6e22e">cloudconfig</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">LookupConfig</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">LookupConfigArgs</span>{
        	<span style="color:#a6e22e">Base64Encode</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">b64encEnable</span>,
        	<span style="color:#a6e22e">Gzip</span>:         <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">gzipEnable</span>,
        	<span style="color:#a6e22e">Parts</span>: []<span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">GetConfigPart</span>{
        		<span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">GetConfigPart</span>{
        			<span style="color:#a6e22e">Content</span>:     <span style="color:#a6e22e">cloudInitContents</span>,
        			<span style="color:#a6e22e">ContentType</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">contentType</span>,
        			<span style="color:#a6e22e">Filename</span>:    <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">fileName</span>,
        		},
        	},
        }, <span style="color:#66d9ef">nil</span>)
        
        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
        	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
        }
</code></pre></div><p>As before, we set the <code>cloud_init_path</code> variable with <code>pulumi config set cloud_init_path &quot;files/ubuntu.tpl&quot;</code> so that we can change the path and contents of the template at a later date if we choose, without changing the code itself.</p>
<p>Defining an SSH key is nearly identical with the Digital Ocean approach: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">		<span style="color:#a6e22e">user</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">Current</span>()

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">sshkey_path</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%v/.ssh/id_rsa.pub&#34;</span>, <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">HomeDir</span>)

		<span style="color:#a6e22e">sshkey_file</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">sshkey_path</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">sshkey_contents</span> <span style="color:#f92672">:=</span> string(<span style="color:#a6e22e">sshkey_file</span>)

		<span style="color:#a6e22e">sshkey</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">hcloud</span>.<span style="color:#a6e22e">NewSshKey</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">hcloud</span>.<span style="color:#a6e22e">SshKeyArgs</span>{
			<span style="color:#a6e22e">Name</span>:      <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">commonName</span>),
			<span style="color:#a6e22e">PublicKey</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">sshkey_contents</span>),
		})
</code></pre></div><p>Other than slightly different names for the <code>hcloud</code> functions, there is no difference. This demonstrates how it would be make sense to define central functions that these files call, with only the provider specific details defined in the Pulumi stacks.</p>
<p>Again, we use our <code>GetMyIp</code> function to retrieve our IP to use in the firewall: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">getMyIp</span>() (<span style="color:#66d9ef">string</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#e6db74">&#34;https://ifconfig.co&#34;</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">err</span>
	}

	<span style="color:#a6e22e">body</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadAll</span>(<span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Body</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">err</span>
	}

	<span style="color:#a6e22e">MyIp</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">TrimSuffix</span>(string(<span style="color:#a6e22e">body</span>), <span style="color:#e6db74">&#34;\n&#34;</span>)

	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">MyIp</span>, <span style="color:#66d9ef">nil</span>
}

[<span style="color:#f92672">...</span>]

		<span style="color:#a6e22e">myIp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">getMyIp</span>()
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">myIpCidr</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%v/32&#34;</span>, <span style="color:#a6e22e">myIp</span>)

		<span style="color:#a6e22e">firewall</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">hcloud</span>.<span style="color:#a6e22e">NewFirewall</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">hcloud</span>.<span style="color:#a6e22e">FirewallArgs</span>{
			<span style="color:#a6e22e">Name</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">commonName</span>),
			<span style="color:#a6e22e">Rules</span>: <span style="color:#a6e22e">hcloud</span>.<span style="color:#a6e22e">FirewallRuleArray</span>{
				<span style="color:#a6e22e">hcloud</span>.<span style="color:#a6e22e">FirewallRuleArgs</span>{
					<span style="color:#a6e22e">Direction</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;in&#34;</span>),
					<span style="color:#a6e22e">Port</span>:      <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;22&#34;</span>),
					<span style="color:#a6e22e">Protocol</span>:  <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;tcp&#34;</span>),
					<span style="color:#a6e22e">SourceIps</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
						<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">myIpCidr</span>),
					},
				},
				<span style="color:#a6e22e">hcloud</span>.<span style="color:#a6e22e">FirewallRuleArgs</span>{
					<span style="color:#a6e22e">Direction</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;in&#34;</span>),
					<span style="color:#a6e22e">Port</span>:      <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;9100&#34;</span>),
					<span style="color:#a6e22e">Protocol</span>:  <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;tcp&#34;</span>),
					<span style="color:#a6e22e">SourceIps</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
						<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">myIpCidr</span>),
					},
				},
			},
		})
</code></pre></div><p>One point to note here is that the rules currently only support the <code>in</code> direction, meaning no outbound rules can be created. This will presumably available as an option in the future, with the Hetzner Firewalls still being classed as a <strong>beta</strong> feature.</p>
<p>Finally, we define our instance: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">        <span style="color:#a6e22e">firewall_id</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%v&#34;</span>, <span style="color:#a6e22e">firewall</span>.<span style="color:#a6e22e">ID</span>())

        <span style="color:#a6e22e">srv</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">hcloud</span>.<span style="color:#a6e22e">NewServer</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">hcloud</span>.<span style="color:#a6e22e">ServerArgs</span>{
        	<span style="color:#a6e22e">Name</span>:       <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">commonName</span>),
        	<span style="color:#a6e22e">Image</span>:      <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;ubuntu-20.04&#34;</span>),
        	<span style="color:#a6e22e">ServerType</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;cx11&#34;</span>),
        	<span style="color:#a6e22e">Labels</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Map</span>{
        		<span style="color:#e6db74">&#34;prometheus&#34;</span>:    <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;true&#34;</span>),
        		<span style="color:#e6db74">&#34;node_exporter&#34;</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;true&#34;</span>),
        	},
        	<span style="color:#a6e22e">UserData</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">cloudconfig</span>.<span style="color:#a6e22e">Rendered</span>),
        	<span style="color:#a6e22e">SshKeys</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
        		<span style="color:#a6e22e">sshkey</span>.<span style="color:#a6e22e">Name</span>,
        	},
        	<span style="color:#a6e22e">FirewallIds</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">IntArray</span>{
        		<span style="color:#a6e22e">firewall_id</span>.<span style="color:#a6e22e">ApplyInt</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">id</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">int</span> {
        			<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">idInt</span> <span style="color:#66d9ef">int</span>
        			<span style="color:#a6e22e">idInt</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strconv</span>.<span style="color:#a6e22e">Atoi</span>(<span style="color:#a6e22e">id</span>)
        			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
        				<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">err</span>)
        				<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">idInt</span>
        			}
        			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">idInt</span>
        		}),
        	},
        })
        
        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
        	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
        }
</code></pre></div><p>As with the Digital Ocean Droplet exposing it&rsquo;s ID as a type of <code>pulumi.IDOutput</code>, but needing an integer for the <code>FirewallIds</code> array, we use an <code>Apply</code> function to achieve this. We turn the ID into a <code>pulumi.StringOutput</code> type, and then use an <code>ApplyInt</code> function to turn it into a <code>pulumi.IntOutput</code> type.</p>
<h3 id="running-pulumi-1">Running Pulumi</h3>
<p>We can now run <code>pulumi</code> up and see our instance build: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ pulumi up
Previewing update <span style="color:#f92672">(</span>staging<span style="color:#f92672">)</span>

View Live: https://app.pulumi.com/yetiops/hetzner-staging/staging/previews/c72a380e-4be2-47ba-966f-06f5dccfbf32

     Type                      Name                     Plan
 +   pulumi:pulumi:Stack       hetzner-staging-staging  create
 +   ├─ hcloud:index:SshKey    yetiops-prom             create
 +   ├─ hcloud:index:Firewall  yetiops-prom             create
 +   └─ hcloud:index:Server    yetiops-prom             create

Resources:
    + <span style="color:#ae81ff">4</span> to create

Do you want to perform this update? yes
Updating <span style="color:#f92672">(</span>staging<span style="color:#f92672">)</span>

View Live: https://app.pulumi.com/yetiops/hetzner-staging/staging/updates/15

     Type                      Name                     Status
 +   pulumi:pulumi:Stack       hetzner-staging-staging  created
 +   ├─ hcloud:index:Firewall  yetiops-prom             created
 +   ├─ hcloud:index:SshKey    yetiops-prom             created
 +   └─ hcloud:index:Server    yetiops-prom             created

Outputs:
    publicIP: <span style="color:#e6db74">&#34;95.217.17.28&#34;</span>

Resources:
    + <span style="color:#ae81ff">4</span> created

Duration: 17s
</code></pre></div><p>We can also see this in the Hetzner console: -</p>
<p><img src="/img/pulumi/hetzner.png" alt="Hetzner Instance in the Hetzner Cloud Console"></p>
<p>Now to login and see what has been deployed: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ssh -i ~/.ssh/id_rsa root@95.217.17.28
Welcome to Ubuntu 20.04.2 LTS <span style="color:#f92672">(</span>GNU/Linux 5.4.0-66-generic x86_64<span style="color:#f92672">)</span>

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

root@yetiops-prom:~# ss -tlunp
Netid               State                Recv-Q               Send-Q                                 Local Address:Port                              Peer Address:Port               Process
udp                 UNCONN               <span style="color:#ae81ff">0</span>                    <span style="color:#ae81ff">0</span>                                      127.0.0.53%lo:53                                     0.0.0.0:*                   users:<span style="color:#f92672">((</span><span style="color:#e6db74">&#34;systemd-resolve&#34;</span>,pid<span style="color:#f92672">=</span>402,fd<span style="color:#f92672">=</span>12<span style="color:#f92672">))</span>
udp                 UNCONN               <span style="color:#ae81ff">0</span>                    <span style="color:#ae81ff">0</span>                                  95.217.17.28%eth0:68                                     0.0.0.0:*                   users:<span style="color:#f92672">((</span><span style="color:#e6db74">&#34;systemd-network&#34;</span>,pid<span style="color:#f92672">=</span>400,fd<span style="color:#f92672">=</span>19<span style="color:#f92672">))</span>
tcp                 LISTEN               <span style="color:#ae81ff">0</span>                    <span style="color:#ae81ff">4096</span>                                   127.0.0.53%lo:53                                     0.0.0.0:*                   users:<span style="color:#f92672">((</span><span style="color:#e6db74">&#34;systemd-resolve&#34;</span>,pid<span style="color:#f92672">=</span>402,fd<span style="color:#f92672">=</span>13<span style="color:#f92672">))</span>
tcp                 LISTEN               <span style="color:#ae81ff">0</span>                    <span style="color:#ae81ff">128</span>                                          0.0.0.0:22                                     0.0.0.0:*                   users:<span style="color:#f92672">((</span><span style="color:#e6db74">&#34;sshd&#34;</span>,pid<span style="color:#f92672">=</span>576,fd<span style="color:#f92672">=</span>3<span style="color:#f92672">))</span>
tcp                 LISTEN               <span style="color:#ae81ff">0</span>                    <span style="color:#ae81ff">4096</span>                                               *:9100                                         *:*                   users:<span style="color:#f92672">((</span><span style="color:#e6db74">&#34;prometheus-node&#34;</span>,pid<span style="color:#f92672">=</span>1199,fd<span style="color:#f92672">=</span>3<span style="color:#f92672">))</span>
tcp                 LISTEN               <span style="color:#ae81ff">0</span>                    <span style="color:#ae81ff">128</span>                                             <span style="color:#f92672">[</span>::<span style="color:#f92672">]</span>:22                                        <span style="color:#f92672">[</span>::<span style="color:#f92672">]</span>:*                   users:<span style="color:#f92672">((</span><span style="color:#e6db74">&#34;sshd&#34;</span>,pid<span style="color:#f92672">=</span>576,fd<span style="color:#f92672">=</span>4<span style="color:#f92672">))</span>

root@yetiops-prom:~# curl localhost:9100/metrics | grep -i yeti
node_uname_info<span style="color:#f92672">{</span>domainname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(none)&#34;</span>,machine<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;x86_64&#34;</span>,nodename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;yetiops-prom&#34;</span>,release<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;5.4.0-66-generic&#34;</span>,sysname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Linux&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;#74-Ubuntu SMP Wed Jan 27 22:54:38 UTC 2021&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>
</code></pre></div><p>As with the Digital Ocean Droplet, we login as <code>root</code>, and the <code>ubuntu</code> user does not exist. All looks good!</p>
<h3 id="all-the-code-1">All the code</h3>
<p>The below is the full <code>main.go</code> file with our code in: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;io/ioutil&#34;</span>
	<span style="color:#e6db74">&#34;net/http&#34;</span>
	<span style="color:#e6db74">&#34;os/user&#34;</span>
	<span style="color:#e6db74">&#34;strconv&#34;</span>
	<span style="color:#e6db74">&#34;strings&#34;</span>

	<span style="color:#e6db74">&#34;github.com/pulumi/pulumi-cloudinit/sdk/go/cloudinit&#34;</span>
	<span style="color:#a6e22e">hcloud</span> <span style="color:#e6db74">&#34;github.com/pulumi/pulumi-hcloud/sdk/go/hcloud&#34;</span>
	<span style="color:#e6db74">&#34;github.com/pulumi/pulumi/sdk/v2/go/pulumi&#34;</span>
	<span style="color:#e6db74">&#34;github.com/pulumi/pulumi/sdk/v2/go/pulumi/config&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">getMyIp</span>() (<span style="color:#66d9ef">string</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#e6db74">&#34;https://ifconfig.co&#34;</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">err</span>
	}

	<span style="color:#a6e22e">body</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadAll</span>(<span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Body</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">err</span>
	}

	<span style="color:#a6e22e">MyIp</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">TrimSuffix</span>(string(<span style="color:#a6e22e">body</span>), <span style="color:#e6db74">&#34;\n&#34;</span>)

	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">MyIp</span>, <span style="color:#66d9ef">nil</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Context</span>) <span style="color:#66d9ef">error</span> {

		<span style="color:#a6e22e">conf</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;&#34;</span>)
		<span style="color:#a6e22e">commonName</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">conf</span>.<span style="color:#a6e22e">Require</span>(<span style="color:#e6db74">&#34;common_name&#34;</span>)
		<span style="color:#a6e22e">cloudInitPath</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">conf</span>.<span style="color:#a6e22e">Require</span>(<span style="color:#e6db74">&#34;cloud_init_path&#34;</span>)

		<span style="color:#a6e22e">cloudInitScript</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">cloudInitPath</span>)

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">cloudInitContents</span> <span style="color:#f92672">:=</span> string(<span style="color:#a6e22e">cloudInitScript</span>)

		<span style="color:#a6e22e">b64encEnable</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">false</span>
		<span style="color:#a6e22e">gzipEnable</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">false</span>
		<span style="color:#a6e22e">contentType</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;text/cloud-config&#34;</span>
		<span style="color:#a6e22e">fileName</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;init.cfg&#34;</span>
		<span style="color:#a6e22e">cloudconfig</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">LookupConfig</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">LookupConfigArgs</span>{
			<span style="color:#a6e22e">Base64Encode</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">b64encEnable</span>,
			<span style="color:#a6e22e">Gzip</span>:         <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">gzipEnable</span>,
			<span style="color:#a6e22e">Parts</span>: []<span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">GetConfigPart</span>{
				<span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">GetConfigPart</span>{
					<span style="color:#a6e22e">Content</span>:     <span style="color:#a6e22e">cloudInitContents</span>,
					<span style="color:#a6e22e">ContentType</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">contentType</span>,
					<span style="color:#a6e22e">Filename</span>:    <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">fileName</span>,
				},
			},
		}, <span style="color:#66d9ef">nil</span>)

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">user</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">Current</span>()

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">sshkey_path</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%v/.ssh/id_rsa.pub&#34;</span>, <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">HomeDir</span>)

		<span style="color:#a6e22e">sshkey_file</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">sshkey_path</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">sshkey_contents</span> <span style="color:#f92672">:=</span> string(<span style="color:#a6e22e">sshkey_file</span>)

		<span style="color:#a6e22e">sshkey</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">hcloud</span>.<span style="color:#a6e22e">NewSshKey</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">hcloud</span>.<span style="color:#a6e22e">SshKeyArgs</span>{
			<span style="color:#a6e22e">Name</span>:      <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">commonName</span>),
			<span style="color:#a6e22e">PublicKey</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">sshkey_contents</span>),
		})

		<span style="color:#a6e22e">myIp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">getMyIp</span>()
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">myIpCidr</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%v/32&#34;</span>, <span style="color:#a6e22e">myIp</span>)

		<span style="color:#a6e22e">firewall</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">hcloud</span>.<span style="color:#a6e22e">NewFirewall</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">hcloud</span>.<span style="color:#a6e22e">FirewallArgs</span>{
			<span style="color:#a6e22e">Name</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">commonName</span>),
			<span style="color:#a6e22e">Rules</span>: <span style="color:#a6e22e">hcloud</span>.<span style="color:#a6e22e">FirewallRuleArray</span>{
				<span style="color:#a6e22e">hcloud</span>.<span style="color:#a6e22e">FirewallRuleArgs</span>{
					<span style="color:#a6e22e">Direction</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;in&#34;</span>),
					<span style="color:#a6e22e">Port</span>:      <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;22&#34;</span>),
					<span style="color:#a6e22e">Protocol</span>:  <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;tcp&#34;</span>),
					<span style="color:#a6e22e">SourceIps</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
						<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">myIpCidr</span>),
					},
				},
				<span style="color:#a6e22e">hcloud</span>.<span style="color:#a6e22e">FirewallRuleArgs</span>{
					<span style="color:#a6e22e">Direction</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;in&#34;</span>),
					<span style="color:#a6e22e">Port</span>:      <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;9100&#34;</span>),
					<span style="color:#a6e22e">Protocol</span>:  <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;tcp&#34;</span>),
					<span style="color:#a6e22e">SourceIps</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
						<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">myIpCidr</span>),
					},
				},
			},
		})

		<span style="color:#a6e22e">firewall_id</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%v&#34;</span>, <span style="color:#a6e22e">firewall</span>.<span style="color:#a6e22e">ID</span>())

		<span style="color:#a6e22e">srv</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">hcloud</span>.<span style="color:#a6e22e">NewServer</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">hcloud</span>.<span style="color:#a6e22e">ServerArgs</span>{
			<span style="color:#a6e22e">Name</span>:       <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">commonName</span>),
			<span style="color:#a6e22e">Image</span>:      <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;ubuntu-20.04&#34;</span>),
			<span style="color:#a6e22e">ServerType</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;cx11&#34;</span>),
			<span style="color:#a6e22e">Labels</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Map</span>{
				<span style="color:#e6db74">&#34;prometheus&#34;</span>:    <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;true&#34;</span>),
				<span style="color:#e6db74">&#34;node_exporter&#34;</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;true&#34;</span>),
			},
			<span style="color:#a6e22e">UserData</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">cloudconfig</span>.<span style="color:#a6e22e">Rendered</span>),
			<span style="color:#a6e22e">SshKeys</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
				<span style="color:#a6e22e">sshkey</span>.<span style="color:#a6e22e">Name</span>,
			},
			<span style="color:#a6e22e">FirewallIds</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">IntArray</span>{
				<span style="color:#a6e22e">firewall_id</span>.<span style="color:#a6e22e">ApplyInt</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">id</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">int</span> {
					<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">idInt</span> <span style="color:#66d9ef">int</span>
					<span style="color:#a6e22e">idInt</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strconv</span>.<span style="color:#a6e22e">Atoi</span>(<span style="color:#a6e22e">id</span>)
					<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
						<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">err</span>)
						<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">idInt</span>
					}
					<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">idInt</span>
				}),
			},
		})

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Export</span>(<span style="color:#e6db74">&#34;publicIP&#34;</span>, <span style="color:#a6e22e">srv</span>.<span style="color:#a6e22e">Ipv4Address</span>)

		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
	})
}
</code></pre></div><h2 id="equinix-metal">Equinix Metal</h2>
<p>Equinix Metal, previously known as Packet, take a different approach to a lot of other providers. Most providers predominantly provide virtual machines as their base unit of compute. Some do offer bare metal as an option, but this is more of an option than as standard.</p>
<p>Equinix Metal instead offer bare metal as their base unit of compute. This means if you deploy a server instance, you will be provisioned a dedicated bare metal server. The cost is reflective of this, meaning you are usually talking dollars per hour rather than cents per hour, but you are not sharing compute resources (even at a hypervisor level) with other users.</p>
<h3 id="signing-up">Signing up</h3>
<p>To sign up for an account with Equinix Metal, go to the <a href="https://console.equinix.com/signup">Registration Page</a>, supply all the required details, and you will be able to start an account. You will be prompted to create an <strong>Organization</strong> (this doesn&rsquo;t have to be a registered business) and then you will be prompted to create a project (effectively a grouping of resources for an application/project/purpose).</p>
<p><img src="/img/pulumi/em-signup.png" alt="Equinix Metal - Create a project"></p>
<p>You can create a project here, or you can define this within Pulumi</p>
<h3 id="generate-an-api-key">Generate an API key</h3>
<p>Like the other providers, you will need an API key to use Pulumi with Equinix Metal. Once you have created your account, go to the menu at the top right and select <strong>API Keys</strong>: -</p>
<p><img src="/img/pulumi/em-api.png" alt="Equinix Metal Console Account Dropdown"></p>
<p>In here, create an API key and name it (use a name that denotes what it is being used for, e.g. <code>pulumi-api-key</code>).</p>
<p>Now we can create our stack and use this API key.</p>
<h3 id="creating-the-stack-and-project">Creating the stack and project</h3>
<p>To create the stack and project, create a folder and run <code>pulumi new equinix-metal-go --name basic-metal</code>. Once this is done, you can define your API key as configuration using: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ pulumi config set equinix-metal:authToken XXXXXXXXXXXXXX --secret
</code></pre></div><p>Alternatively, you can expose it as an environment variable using <code>export PACKET_AUTH_TOKEN=XXXXXXXXXXXXXX</code>.</p>
<p>We then run <code>pulumi config set common_name yetiops-prom</code> to use across our resources, and now we can create some code!</p>
<h3 id="the-code-2">The code</h3>
<p>With Equinix Metal, we are going to create the following: -</p>
<ul>
<li>A project to create our resources in</li>
<li>A bare metal server running Ubuntu 20.04 with the Prometheus <code>node_exporter</code> installed</li>
<li>An SSH key to login to the server with</li>
</ul>
<p>Rather than creating a server directly though, we will use what a Spot Market request. Like AWS, Equinix Metal provide a Spot Market that makes available spare capacity (unused servers) at a cheaper price, with the understanding that if an <strong>On Demand</strong> (i.e. a standard server creation) or <strong>Reserved</strong> (i.e. a server that will be dedicated to a customer) request is made that requires this capacity, your usage of that server will be terminated to allow Equinix Metal to fulfil this request.</p>
<p>A practical application of this approach is using servers for jobs that are stateless/ephemeral (e.g. batch jobs that just need workers to churn through data, rather than a 24/7 available service). If your worker jobs are not time sensitive, then this allows a lot more compute capacity at a lower price, with the understanding that it is not guaranteed.</p>
<p>For our purposes, it just means we can create a server to demonstrate Pulumi without it costing too much!</p>
<p>Equinix Metal does not currently offer a firewall-like product that would be in front of your server(s), so you will need to deploy host-based firewalling. You can build BGP networks and Layer 2 networks though, so it is perfectly possible to build a Linux/FreeBSD-based firewall to sit in front of your instances, and have them all connect as if they are in a switched network behind the firewall(s).</p>
<p>As with the other providers, we define the <code>config</code> elements and <code>cloudinit</code> resource in the same way: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">		<span style="color:#a6e22e">conf</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;&#34;</span>)
		<span style="color:#a6e22e">commonName</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">conf</span>.<span style="color:#a6e22e">Require</span>(<span style="color:#e6db74">&#34;common_name&#34;</span>)
		<span style="color:#a6e22e">cloudInitPath</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">conf</span>.<span style="color:#a6e22e">Require</span>(<span style="color:#e6db74">&#34;cloud_init_path&#34;</span>)

		<span style="color:#a6e22e">cloudInitScript</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">cloudInitPath</span>)

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">cloudInitContents</span> <span style="color:#f92672">:=</span> string(<span style="color:#a6e22e">cloudInitScript</span>)

		<span style="color:#a6e22e">b64encEnable</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">false</span>
		<span style="color:#a6e22e">gzipEnable</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">false</span>
		<span style="color:#a6e22e">contentType</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;text/cloud-config&#34;</span>
		<span style="color:#a6e22e">fileName</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;init.cfg&#34;</span>
		<span style="color:#a6e22e">cloudconfig</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">LookupConfig</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">LookupConfigArgs</span>{
			<span style="color:#a6e22e">Base64Encode</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">b64encEnable</span>,
			<span style="color:#a6e22e">Gzip</span>:         <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">gzipEnable</span>,
			<span style="color:#a6e22e">Parts</span>: []<span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">GetConfigPart</span>{
				<span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">GetConfigPart</span>{
					<span style="color:#a6e22e">Content</span>:     <span style="color:#a6e22e">cloudInitContents</span>,
					<span style="color:#a6e22e">ContentType</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">contentType</span>,
					<span style="color:#a6e22e">Filename</span>:    <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">fileName</span>,
				},
			},
		}, <span style="color:#66d9ef">nil</span>)

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}
</code></pre></div><p>As noted previously, the <code>cloud_init_path</code> configuration allows us to make changes to the file externally without updating the code itself.</p>
<p>After this, we create an SSH key resource, in much the same way as Digital Ocean and Hetzner: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">        <span style="color:#a6e22e">user</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">Current</span>()
        
        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
        	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
        }
        
        <span style="color:#a6e22e">sshkey_path</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%v/.ssh/id_rsa.pub&#34;</span>, <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">HomeDir</span>)
        
        <span style="color:#a6e22e">sshkey_file</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">sshkey_path</span>)
        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
        	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
        }
        
        <span style="color:#a6e22e">sshkey_contents</span> <span style="color:#f92672">:=</span> string(<span style="color:#a6e22e">sshkey_file</span>)
        
        <span style="color:#a6e22e">sshkey</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">NewSshKey</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">SshKeyArgs</span>{
        	<span style="color:#a6e22e">Name</span>:      <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">commonName</span>),
        	<span style="color:#a6e22e">PublicKey</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">sshkey_contents</span>),
        })
</code></pre></div><p>After this, we define our project: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">		<span style="color:#a6e22e">project</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">NewProject</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;my-project&#34;</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">ProjectArgs</span>{
			<span style="color:#a6e22e">Name</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;yetiops-blog&#34;</span>),
		})
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}
</code></pre></div><p>This project is the grouping of our resources (similar to an Azure Resource Group, or a GCP Project).</p>
<p>Finally, we create the Spot Request. This requests a server based upon some instance parameters. We also specify the maximum cost we want to pay per server, as well as how many of them we want to provision: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">		<span style="color:#a6e22e">project_id</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%v&#34;</span>, <span style="color:#a6e22e">project</span>.<span style="color:#a6e22e">ID</span>())

		<span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">NewSpotMarketRequest</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">SpotMarketRequestArgs</span>{
			<span style="color:#a6e22e">ProjectId</span>: <span style="color:#a6e22e">project_id</span>,
			<span style="color:#a6e22e">MaxBidPrice</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Float64</span>(<span style="color:#ae81ff">0.20</span>),
			<span style="color:#a6e22e">Facilities</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
				<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;fr2&#34;</span>),
			},
			<span style="color:#a6e22e">DevicesMin</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Int</span>(<span style="color:#ae81ff">1</span>),
			<span style="color:#a6e22e">DevicesMax</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Int</span>(<span style="color:#ae81ff">1</span>),
			<span style="color:#a6e22e">InstanceParameters</span>: <span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">SpotMarketRequestInstanceParametersArgs</span>{
				<span style="color:#a6e22e">Hostname</span>:        <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">commonName</span>),
				<span style="color:#a6e22e">BillingCycle</span>:    <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;hourly&#34;</span>),
				<span style="color:#a6e22e">OperatingSystem</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;ubuntu_20_04&#34;</span>),
				<span style="color:#a6e22e">Plan</span>:            <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;c3.small.x86&#34;</span>),
				<span style="color:#a6e22e">UserSshKeys</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
					<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringOutput</span>(<span style="color:#a6e22e">sshkey</span>.<span style="color:#a6e22e">OwnerId</span>),
				},
				<span style="color:#a6e22e">Userdata</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">cloudconfig</span>.<span style="color:#a6e22e">Rendered</span>),
			},
		})
</code></pre></div><p>Similar to before, we need to turn the Project ID into a <code>StringOutput</code> rather than an <code>IDOutput</code>. We don&rsquo;t need to use Apply functions here, instead the <code>pulumi.Sprintf</code> function provides a variable of type <code>StringOutput</code> that we can use directly.</p>
<p>Besides this, the main parts to we configure are setting a <code>MaxBidPrice</code> (i.e. the most we want to pay for a server), what facility (similar to regions) we want to deploy our instance(s) in, and what our maximum and minimum amount of devices is (in this case, both 1). This provides us with a bare metal server to manage.</p>
<h3 id="running-pulumi-2">Running Pulumi</h3>
<p>We can now run <code>pulumi up</code> and get ourselves a bare metal server: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ pulumi up
Previewing update <span style="color:#f92672">(</span>staging<span style="color:#f92672">)</span>

View Live: https://app.pulumi.com/yetiops/basic-metal/staging/previews/347cd557-8dd7-4188-9cf8-9eb5a78f3fd6

     Type                                      Name                 Plan
 +   pulumi:pulumi:Stack                       basic-metal-staging  create
 +   ├─ equinix-metal:index:SshKey             yetiops-prom         create
 +   ├─ equinix-metal:index:Project            my-project           create
 +   └─ equinix-metal:index:SpotMarketRequest  yetiops-prom         create

Resources:
    + <span style="color:#ae81ff">4</span> to create

Do you want to perform this update? yes
Updating <span style="color:#f92672">(</span>staging<span style="color:#f92672">)</span>

View Live: https://app.pulumi.com/yetiops/basic-metal/staging/updates/11

     Type                                      Name                 Status      Info
 +   pulumi:pulumi:Stack                       basic-metal-staging  created     <span style="color:#ae81ff">7</span> messages
 +   ├─ equinix-metal:index:SshKey             yetiops-prom         created
 +   ├─ equinix-metal:index:Project            my-project           created
 +   └─ equinix-metal:index:SpotMarketRequest  yetiops-prom         created

Diagnostics:
  pulumi:pulumi:Stack <span style="color:#f92672">(</span>basic-metal-staging<span style="color:#f92672">)</span>:
    2021/04/12 12:47:12 <span style="color:#f92672">[</span>DEBUG<span style="color:#f92672">]</span> POST https://api.equinix.com/metal/v1/ssh-keys
    2021/04/12 12:47:13 <span style="color:#f92672">[</span>DEBUG<span style="color:#f92672">]</span> POST https://api.equinix.com/metal/v1/projects
    2021/04/12 12:47:14 <span style="color:#f92672">[</span>DEBUG<span style="color:#f92672">]</span> GET https://api.equinix.com/metal/v1/projects/079c6885-ad49-4141-9977-6988ef97f2e2?
    2021/04/12 12:47:14 <span style="color:#f92672">[</span>DEBUG<span style="color:#f92672">]</span> GET https://api.equinix.com/metal/v1/ssh-keys/7fed9543-61c8-4bb3-81c9-bdd29512e96e?
    2021/04/12 12:47:14 <span style="color:#f92672">[</span>DEBUG<span style="color:#f92672">]</span> GET https://api.equinix.com/metal/v1/projects/079c6885-ad49-4141-9977-6988ef97f2e2/bgp-config?
    2021/04/12 12:47:16 <span style="color:#f92672">[</span>DEBUG<span style="color:#f92672">]</span> POST https://api.equinix.com/metal/v1/projects/079c6885-ad49-4141-9977-6988ef97f2e2/spot-market-requests?include<span style="color:#f92672">=</span>devices,project,plan
    2021/04/12 12:47:17 <span style="color:#f92672">[</span>DEBUG<span style="color:#f92672">]</span> GET https://api.equinix.com/metal/v1/spot-market-requests/1ba255af-2e97-4a62-8cdc-bd018c8883d9?include<span style="color:#f92672">=</span>project%2Cdevices%2Cfacilities

Outputs:
    projectName: <span style="color:#e6db74">&#34;yetiops-blog&#34;</span>

Resources:
    + <span style="color:#ae81ff">4</span> created

Duration: 10s
</code></pre></div><p>We can also see this in the Equinix Metal console: -</p>
<p><img src="/img/pulumi/em-server.png" alt="Equinix Metal Server - Equinix Metal Console"></p>
<p>Lets login and see if it works as expected: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ssh -i ~/.ssh/id_rsa root@145.40.93.99
Welcome to Ubuntu 20.04.1 LTS <span style="color:#f92672">(</span>GNU/Linux 5.4.0-52-generic x86_64<span style="color:#f92672">)</span>

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

This system has been minimized by removing packages and content that are
not required on a system that users <span style="color:#66d9ef">do</span> not log into.

To restore this content, you can run the <span style="color:#e6db74">&#39;unminimize&#39;</span> command.

The programs included with the Ubuntu system are free software;
the exact distribution terms <span style="color:#66d9ef">for</span> each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

root@yetiops-prom:~#
root@yetiops-prom:~#
root@yetiops-prom:~#
root@yetiops-prom:~#
root@yetiops-prom:~# ss -tlunp
Netid             State              Recv-Q             Send-Q                                              Local Address:Port                           Peer Address:Port             Process
udp               UNCONN             <span style="color:#ae81ff">0</span>                  <span style="color:#ae81ff">0</span>                                                   127.0.0.53%lo:53                                  0.0.0.0:*                 users:<span style="color:#f92672">((</span><span style="color:#e6db74">&#34;systemd-resolve&#34;</span>,pid<span style="color:#f92672">=</span>897,fd<span style="color:#f92672">=</span>12<span style="color:#f92672">))</span>
udp               UNCONN             <span style="color:#ae81ff">0</span>                  <span style="color:#ae81ff">0</span>                                                    10.25.19.129:123                                 0.0.0.0:*                 users:<span style="color:#f92672">((</span><span style="color:#e6db74">&#34;ntpd&#34;</span>,pid<span style="color:#f92672">=</span>1119,fd<span style="color:#f92672">=</span>20<span style="color:#f92672">))</span>
udp               UNCONN             <span style="color:#ae81ff">0</span>                  <span style="color:#ae81ff">0</span>                                                    145.40.93.99:123                                 0.0.0.0:*                 users:<span style="color:#f92672">((</span><span style="color:#e6db74">&#34;ntpd&#34;</span>,pid<span style="color:#f92672">=</span>1119,fd<span style="color:#f92672">=</span>19<span style="color:#f92672">))</span>
udp               UNCONN             <span style="color:#ae81ff">0</span>                  <span style="color:#ae81ff">0</span>                                                       127.0.0.1:123                                 0.0.0.0:*                 users:<span style="color:#f92672">((</span><span style="color:#e6db74">&#34;ntpd&#34;</span>,pid<span style="color:#f92672">=</span>1119,fd<span style="color:#f92672">=</span>18<span style="color:#f92672">))</span>
udp               UNCONN             <span style="color:#ae81ff">0</span>                  <span style="color:#ae81ff">0</span>                                                         0.0.0.0:123                                 0.0.0.0:*                 users:<span style="color:#f92672">((</span><span style="color:#e6db74">&#34;ntpd&#34;</span>,pid<span style="color:#f92672">=</span>1119,fd<span style="color:#f92672">=</span>17<span style="color:#f92672">))</span>
udp               UNCONN             <span style="color:#ae81ff">0</span>                  <span style="color:#ae81ff">0</span>                                <span style="color:#f92672">[</span>fe80::63f:72ff:fed4:749c<span style="color:#f92672">]</span>%bond0:123                                    <span style="color:#f92672">[</span>::<span style="color:#f92672">]</span>:*                 users:<span style="color:#f92672">((</span><span style="color:#e6db74">&#34;ntpd&#34;</span>,pid<span style="color:#f92672">=</span>1119,fd<span style="color:#f92672">=</span>23<span style="color:#f92672">))</span>
udp               UNCONN             <span style="color:#ae81ff">0</span>                  <span style="color:#ae81ff">0</span>                                        <span style="color:#f92672">[</span>2604:1380:4091:1400::1<span style="color:#f92672">]</span>:123                                    <span style="color:#f92672">[</span>::<span style="color:#f92672">]</span>:*                 users:<span style="color:#f92672">((</span><span style="color:#e6db74">&#34;ntpd&#34;</span>,pid<span style="color:#f92672">=</span>1119,fd<span style="color:#f92672">=</span>22<span style="color:#f92672">))</span>
udp               UNCONN             <span style="color:#ae81ff">0</span>                  <span style="color:#ae81ff">0</span>                                                           <span style="color:#f92672">[</span>::1<span style="color:#f92672">]</span>:123                                    <span style="color:#f92672">[</span>::<span style="color:#f92672">]</span>:*                 users:<span style="color:#f92672">((</span><span style="color:#e6db74">&#34;ntpd&#34;</span>,pid<span style="color:#f92672">=</span>1119,fd<span style="color:#f92672">=</span>21<span style="color:#f92672">))</span>
udp               UNCONN             <span style="color:#ae81ff">0</span>                  <span style="color:#ae81ff">0</span>                                                            <span style="color:#f92672">[</span>::<span style="color:#f92672">]</span>:123                                    <span style="color:#f92672">[</span>::<span style="color:#f92672">]</span>:*                 users:<span style="color:#f92672">((</span><span style="color:#e6db74">&#34;ntpd&#34;</span>,pid<span style="color:#f92672">=</span>1119,fd<span style="color:#f92672">=</span>16<span style="color:#f92672">))</span>
tcp               LISTEN             <span style="color:#ae81ff">0</span>                  <span style="color:#ae81ff">4096</span>                                                127.0.0.53%lo:53                                  0.0.0.0:*                 users:<span style="color:#f92672">((</span><span style="color:#e6db74">&#34;systemd-resolve&#34;</span>,pid<span style="color:#f92672">=</span>897,fd<span style="color:#f92672">=</span>13<span style="color:#f92672">))</span>
tcp               LISTEN             <span style="color:#ae81ff">0</span>                  <span style="color:#ae81ff">128</span>                                                       0.0.0.0:22                                  0.0.0.0:*                 users:<span style="color:#f92672">((</span><span style="color:#e6db74">&#34;sshd&#34;</span>,pid<span style="color:#f92672">=</span>1197,fd<span style="color:#f92672">=</span>3<span style="color:#f92672">))</span>
tcp               LISTEN             <span style="color:#ae81ff">0</span>                  <span style="color:#ae81ff">4096</span>                                                            *:9100                                      *:*                 users:<span style="color:#f92672">((</span><span style="color:#e6db74">&#34;prometheus-node&#34;</span>,pid<span style="color:#f92672">=</span>1753,fd<span style="color:#f92672">=</span>3<span style="color:#f92672">))</span>
tcp               LISTEN             <span style="color:#ae81ff">0</span>                  <span style="color:#ae81ff">128</span>                                                          <span style="color:#f92672">[</span>::<span style="color:#f92672">]</span>:22                                     <span style="color:#f92672">[</span>::<span style="color:#f92672">]</span>:*                 users:<span style="color:#f92672">((</span><span style="color:#e6db74">&#34;sshd&#34;</span>,pid<span style="color:#f92672">=</span>1197,fd<span style="color:#f92672">=</span>4<span style="color:#f92672">))</span>

root@yetiops-prom:~# curl localhost:9100/metrics | grep -i yeti
node_uname_info<span style="color:#f92672">{</span>domainname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(none)&#34;</span>,machine<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;x86_64&#34;</span>,nodename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;yetiops-prom&#34;</span>,release<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;5.4.0-52-generic&#34;</span>,sysname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Linux&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;#57-Ubuntu SMP Thu Oct 15 10:57:00 UTC 2020&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>

root@yetiops-prom:~# cat /proc/cpuinfo  | grep -i processor
processor	: <span style="color:#ae81ff">0</span>
processor	: <span style="color:#ae81ff">1</span>
processor	: <span style="color:#ae81ff">2</span>
processor	: <span style="color:#ae81ff">3</span>
processor	: <span style="color:#ae81ff">4</span>
processor	: <span style="color:#ae81ff">5</span>
processor	: <span style="color:#ae81ff">6</span>
processor	: <span style="color:#ae81ff">7</span>
processor	: <span style="color:#ae81ff">8</span>
processor	: <span style="color:#ae81ff">9</span>
processor	: <span style="color:#ae81ff">10</span>
processor	: <span style="color:#ae81ff">11</span>
processor	: <span style="color:#ae81ff">12</span>
processor	: <span style="color:#ae81ff">13</span>
processor	: <span style="color:#ae81ff">14</span>
processor	: <span style="color:#ae81ff">15</span>

root@yetiops-prom:~# free -mh
              total        used        free      shared  buff/cache   available
Mem:           31Gi       598Mi        30Gi        17Mi       256Mi        30Gi
Swap:         1.9Gi          0B       1.9Gi
</code></pre></div><p>The joy of bare metal is that we now have 16 cores to play with and 32Gi of memory, and we don&rsquo;t have to share it with anyone else!</p>
<h3 id="all-the-code-2">All the code</h3>
<p>The below is the full <code>main.go</code> file with our code in: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;io/ioutil&#34;</span>
	<span style="color:#e6db74">&#34;os/user&#34;</span>

	<span style="color:#e6db74">&#34;github.com/pulumi/pulumi-cloudinit/sdk/go/cloudinit&#34;</span>
	<span style="color:#a6e22e">metal</span> <span style="color:#e6db74">&#34;github.com/pulumi/pulumi-equinix-metal/sdk/go/equinix&#34;</span>
	<span style="color:#e6db74">&#34;github.com/pulumi/pulumi/sdk/v2/go/pulumi&#34;</span>
	<span style="color:#e6db74">&#34;github.com/pulumi/pulumi/sdk/v2/go/pulumi/config&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Context</span>) <span style="color:#66d9ef">error</span> {
		<span style="color:#a6e22e">conf</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;&#34;</span>)
		<span style="color:#a6e22e">commonName</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">conf</span>.<span style="color:#a6e22e">Require</span>(<span style="color:#e6db74">&#34;common_name&#34;</span>)
		<span style="color:#a6e22e">cloudInitPath</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">conf</span>.<span style="color:#a6e22e">Require</span>(<span style="color:#e6db74">&#34;cloud_init_path&#34;</span>)

		<span style="color:#a6e22e">cloudInitScript</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">cloudInitPath</span>)

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">cloudInitContents</span> <span style="color:#f92672">:=</span> string(<span style="color:#a6e22e">cloudInitScript</span>)

		<span style="color:#a6e22e">b64encEnable</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">false</span>
		<span style="color:#a6e22e">gzipEnable</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">false</span>
		<span style="color:#a6e22e">contentType</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;text/cloud-config&#34;</span>
		<span style="color:#a6e22e">fileName</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;init.cfg&#34;</span>
		<span style="color:#a6e22e">cloudconfig</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">LookupConfig</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">LookupConfigArgs</span>{
			<span style="color:#a6e22e">Base64Encode</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">b64encEnable</span>,
			<span style="color:#a6e22e">Gzip</span>:         <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">gzipEnable</span>,
			<span style="color:#a6e22e">Parts</span>: []<span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">GetConfigPart</span>{
				<span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">GetConfigPart</span>{
					<span style="color:#a6e22e">Content</span>:     <span style="color:#a6e22e">cloudInitContents</span>,
					<span style="color:#a6e22e">ContentType</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">contentType</span>,
					<span style="color:#a6e22e">Filename</span>:    <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">fileName</span>,
				},
			},
		}, <span style="color:#66d9ef">nil</span>)

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">user</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">Current</span>()

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">sshkey_path</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%v/.ssh/id_rsa.pub&#34;</span>, <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">HomeDir</span>)

		<span style="color:#a6e22e">sshkey_file</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">sshkey_path</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">sshkey_contents</span> <span style="color:#f92672">:=</span> string(<span style="color:#a6e22e">sshkey_file</span>)

		<span style="color:#a6e22e">sshkey</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">NewSshKey</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">SshKeyArgs</span>{
			<span style="color:#a6e22e">Name</span>:      <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">commonName</span>),
			<span style="color:#a6e22e">PublicKey</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">sshkey_contents</span>),
		})

		<span style="color:#a6e22e">project</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">NewProject</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;my-project&#34;</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">ProjectArgs</span>{
			<span style="color:#a6e22e">Name</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;yetiops-blog&#34;</span>),
		})
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">project_id</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%v&#34;</span>, <span style="color:#a6e22e">project</span>.<span style="color:#a6e22e">ID</span>())

		<span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">NewSpotMarketRequest</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">SpotMarketRequestArgs</span>{
			<span style="color:#75715e">//			ProjectId: project_id.ApplyString(func(id string) string {
</span><span style="color:#75715e"></span>			<span style="color:#75715e">//				return id
</span><span style="color:#75715e"></span>			<span style="color:#75715e">//			}),
</span><span style="color:#75715e"></span>			<span style="color:#a6e22e">ProjectId</span>:   <span style="color:#a6e22e">project_id</span>,
			<span style="color:#a6e22e">MaxBidPrice</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Float64</span>(<span style="color:#ae81ff">0.20</span>),
			<span style="color:#a6e22e">Facilities</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
				<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;fr2&#34;</span>),
			},
			<span style="color:#a6e22e">DevicesMin</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Int</span>(<span style="color:#ae81ff">1</span>),
			<span style="color:#a6e22e">DevicesMax</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Int</span>(<span style="color:#ae81ff">1</span>),
			<span style="color:#a6e22e">InstanceParameters</span>: <span style="color:#a6e22e">metal</span>.<span style="color:#a6e22e">SpotMarketRequestInstanceParametersArgs</span>{
				<span style="color:#a6e22e">Hostname</span>:        <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">commonName</span>),
				<span style="color:#a6e22e">BillingCycle</span>:    <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;hourly&#34;</span>),
				<span style="color:#a6e22e">OperatingSystem</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;ubuntu_20_04&#34;</span>),
				<span style="color:#a6e22e">Plan</span>:            <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;c3.small.x86&#34;</span>),
				<span style="color:#a6e22e">UserSshKeys</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
					<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringOutput</span>(<span style="color:#a6e22e">sshkey</span>.<span style="color:#a6e22e">OwnerId</span>),
				},
				<span style="color:#a6e22e">Userdata</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">cloudconfig</span>.<span style="color:#a6e22e">Rendered</span>),
			},
		})

		<span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Export</span>(<span style="color:#e6db74">&#34;projectName&#34;</span>, <span style="color:#a6e22e">project</span>.<span style="color:#a6e22e">Name</span>)
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
	})
}
</code></pre></div><h2 id="linode">Linode</h2>
<p>Linode are a provider who have been around since the early 2000s, focussing primarily on Linux instances and the supporting infrastructure for them. This includes LoadBalancers (<strong>NodeBalancers</strong> in Linode terminology), Firewalls, Object Storage and more.</p>
<p>They also offer the Linode Kubernetes Engine which provides a managed Kubernetes cluster that can be sized to your requirements. To see one in action, you can watch/listen to the <a href="https://changelog.com">Changelog Podcast</a> performing updates/inducing some chaos engineering on their cluster: -</p>

<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe src="https://www.youtube.com/embed/umkcN8Kz2es" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allowfullscreen title="YouTube Video"></iframe>
</div>

<p>The full podcast that this video is from is available <a href="https://www.changelog.com/podcast/419">here</a>.</p>
<p>So far we have shown multiple providers (AWS, Digital Ocean, Hetzner, Equinix Metal) but all building some form of machine (virtual or dedicated) running the Prometheus <code>node_exporter</code>. This time we are going to build an LKE Kubernetes cluster and run a basic Deployment on it. We won&rsquo;t expose the Pods publicly, instead we are more concerned with the interaction between the Linode and Kubernetes Pulumi providers.</p>
<h3 id="signing-up-1">Signing up</h3>
<p>First, you need to sign up with Linode. Go to their <a href="https://login.linode.com/signup">Sign Up page</a> and provide your details (name, email, billing information). You&rsquo;ll then be presented with the Linode console: -</p>
<p><img src="/img/pulumi/linode-console.png" alt="Linode Console"></p>
<h3 id="generate-an-api-key-1">Generate an API key</h3>
<p>To create a Linode API key, go to the top right of the interface, click on the dropdown over your account and choose <strong>API Tokens</strong>: -</p>
<p><img src="/img/pulumi/linode-account.png" alt="Linode Console - Account Dropdown"></p>
<p>You can then create a <strong>Personal Access Token</strong> for use with Pulumi.</p>
<h3 id="create-the-stack-and-project">Create the stack and project</h3>
<p>You can now create your project using <code>pulumi new linode-go --name lke-go</code>. Either set the API token as part of your configuration with <code>pulumi config set linode:token XXXXXXXXXXXXXX --secret</code> or export it as <code>export LINODE_TOKEN=XXXXXXXXXXXXXX</code>.</p>
<p>As with all the other providers, we&rsquo;ll also add <code>pulumi config set common_name yetiops-prom</code> for use across our resources.</p>
<h3 id="the-code-3">The code</h3>
<p>As noted, rather than creating a VPS, this time we&rsquo;re going to: -</p>
<ul>
<li>Create an LKE Kubernetes Cluster, running Kubernetes v1.20
<ul>
<li>This will have 3 nodes of size <code>g6-standard-1</code> (2Gb memory, 1 vCPU)</li>
</ul>
</li>
<li>We will also add a generic NGINX deployment with a single replica, using the <code>kubeconfig</code> exported from the LKE cluster</li>
</ul>
<p>First, we define our configuration values: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">		<span style="color:#a6e22e">conf</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;&#34;</span>)
		<span style="color:#a6e22e">commonName</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">conf</span>.<span style="color:#a6e22e">Require</span>(<span style="color:#e6db74">&#34;common_name&#34;</span>)
</code></pre></div><p>This is the same as the other providers, except we don&rsquo;t provide any <code>cloudinit</code> or similar because Linode takes care of the Kubernetes-specific installation/application settings.</p>
<p>Next, we build the LKE cluster: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">		<span style="color:#a6e22e">cluster</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">linode</span>.<span style="color:#a6e22e">NewLkeCluster</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">linode</span>.<span style="color:#a6e22e">LkeClusterArgs</span>{
			<span style="color:#a6e22e">K8sVersion</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;1.20&#34;</span>),
			<span style="color:#a6e22e">Label</span>:      <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">commonName</span>),
			<span style="color:#a6e22e">Pools</span>: <span style="color:#a6e22e">linode</span>.<span style="color:#a6e22e">LkeClusterPoolArray</span>{
				<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">linode</span>.<span style="color:#a6e22e">LkeClusterPoolArgs</span>{
					<span style="color:#a6e22e">Count</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Int</span>(<span style="color:#ae81ff">3</span>),
					<span style="color:#a6e22e">Type</span>:  <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;g6-standard-1&#34;</span>),
				},
			},
			<span style="color:#a6e22e">Region</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;eu-west&#34;</span>),
			<span style="color:#a6e22e">Tags</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
				<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;staging&#34;</span>),
				<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;lke-yetiops&#34;</span>),
			},
		})

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}
</code></pre></div><p>One of the benefits of using a managed Kubernetes cluster is that there is very little configuration required to set a cluster up. In addition we add a couple of tags, specify that the cluster will be built in the <code>eu-west</code> region, and then build it.</p>
<p>The next section takes the generated <code>kubeconfig</code> from the cluster, and makes it available for use with the Pulumi Kubernetes provider: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">		<span style="color:#a6e22e">lke_config</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cluster</span>.<span style="color:#a6e22e">Kubeconfig</span>.<span style="color:#a6e22e">ApplyString</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">kconf</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">string</span> {
			<span style="color:#a6e22e">output</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">b64</span>.<span style="color:#a6e22e">StdEncoding</span>.<span style="color:#a6e22e">DecodeString</span>(<span style="color:#a6e22e">kconf</span>)
			<span style="color:#66d9ef">return</span> string(<span style="color:#a6e22e">output</span>)
		})

		<span style="color:#a6e22e">kubeProvider</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">kubernetes</span>.<span style="color:#a6e22e">NewProvider</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">kubernetes</span>.<span style="color:#a6e22e">ProviderArgs</span>{
			<span style="color:#a6e22e">Kubeconfig</span>: <span style="color:#a6e22e">lke_config</span>,
		}, <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">DependsOn</span>([]<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Resource</span>{<span style="color:#a6e22e">cluster</span>}))

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}
</code></pre></div><p>There are two parts to this. The first is another <code>Apply</code> function. This is because the <code>kubeconfig</code> output from the cluster is in <code>base64</code> format, which needs turning into a string for the Pulumi Kubernetes provider to use. There are other ways of generating a usable <code>kubeconfig</code> (a good example is <a href="https://www.pulumi.com/blog/deploy-kubernetes-and-apps-with-go/#cloud--kubernetes-with-go">here</a>), but in this case the LKE cluster output is already in the correct format (once decoded).</p>
<p>After that, we create a Kubernetes provider, which has a depends upon the cluster being created. The <code>DependsOn</code> function is similar to Terraform&rsquo;s <code>depends_on</code>, in that Pulumi will not try to create this resource until the resource it depends upon is created first.</p>
<p>For every resource we want to add to the Kubernetes cluster, we&rsquo;ll reference this provider so that it uses the correct configuration. Without this, Pulumi follow the defaults that <code>kubectl</code> and similar tools use, either the configuration referenced in a <code>$KUBE_CONFIG</code> environment variable, or the <code>~/.kube/config</code> file, if one exists.</p>
<p>Next, we create an app label resource which can be used to add additional labels to anything we create: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">		<span style="color:#a6e22e">appLabels</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringMap</span>{
			<span style="color:#e6db74">&#34;app&#34;</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;nginx&#34;</span>),
		}
</code></pre></div><p>Any labels added to this will be present in all of your other resources that reference it.</p>
<p>Next, we create a namespace: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">        <span style="color:#a6e22e">namespace</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">corev1</span>.<span style="color:#a6e22e">NewNamespace</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;app-ns&#34;</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">corev1</span>.<span style="color:#a6e22e">NamespaceArgs</span>{
            <span style="color:#a6e22e">Metadata</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metav1</span>.<span style="color:#a6e22e">ObjectMetaArgs</span>{
                <span style="color:#a6e22e">Name</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">commonName</span>),
            },
        }, <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Provider</span>(<span style="color:#a6e22e">kubeProvider</span>))

        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
            <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
        }
</code></pre></div><p>As you can see, we have attached our provider here, which means that it will use the provider specific to our LKE cluster.</p>
<p>Finally, we create our NGINX deployment, using the provider, namespace and labels we created, and deploy it into our cluster: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">		<span style="color:#a6e22e">deployment</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">appsv1</span>.<span style="color:#a6e22e">NewDeployment</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;app-dep&#34;</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">appsv1</span>.<span style="color:#a6e22e">DeploymentArgs</span>{
			<span style="color:#a6e22e">Metadata</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metav1</span>.<span style="color:#a6e22e">ObjectMetaArgs</span>{
				<span style="color:#a6e22e">Namespace</span>: <span style="color:#a6e22e">namespace</span>.<span style="color:#a6e22e">Metadata</span>.<span style="color:#a6e22e">Elem</span>().<span style="color:#a6e22e">Name</span>(),
			},
			<span style="color:#a6e22e">Spec</span>: <span style="color:#a6e22e">appsv1</span>.<span style="color:#a6e22e">DeploymentSpecArgs</span>{
				<span style="color:#a6e22e">Selector</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metav1</span>.<span style="color:#a6e22e">LabelSelectorArgs</span>{
					<span style="color:#a6e22e">MatchLabels</span>: <span style="color:#a6e22e">appLabels</span>,
				},
				<span style="color:#a6e22e">Replicas</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Int</span>(<span style="color:#ae81ff">1</span>),
				<span style="color:#a6e22e">Template</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">corev1</span>.<span style="color:#a6e22e">PodTemplateSpecArgs</span>{
					<span style="color:#a6e22e">Metadata</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metav1</span>.<span style="color:#a6e22e">ObjectMetaArgs</span>{
						<span style="color:#a6e22e">Labels</span>: <span style="color:#a6e22e">appLabels</span>,
					},
					<span style="color:#a6e22e">Spec</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">corev1</span>.<span style="color:#a6e22e">PodSpecArgs</span>{
						<span style="color:#a6e22e">Containers</span>: <span style="color:#a6e22e">corev1</span>.<span style="color:#a6e22e">ContainerArray</span>{
							<span style="color:#a6e22e">corev1</span>.<span style="color:#a6e22e">ContainerArgs</span>{
								<span style="color:#a6e22e">Name</span>:  <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;nginx&#34;</span>),
								<span style="color:#a6e22e">Image</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;nginx&#34;</span>),
							}},
					},
				},
			},
		}, <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Provider</span>(<span style="color:#a6e22e">kubeProvider</span>))
</code></pre></div><p>If you aren&rsquo;t familiar with Kubernetes, this will run a single NGINX container in our cluster. This Deployment will manage rollouts, and also if the container dies at any point it will get replaced with an another running NGINX. We specify how many containers we want to run using Replicas.</p>
<p>We also reference our <code>appLabels</code> and <code>namespace</code>, meaning the application will be labelled correctly and will also be deployed into the namespace we created.</p>
<p>Finally, we export a couple of values: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">        <span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Export</span>(<span style="color:#e6db74">&#34;name&#34;</span>, <span style="color:#a6e22e">deployment</span>.<span style="color:#a6e22e">Metadata</span>.<span style="color:#a6e22e">Elem</span>().<span style="color:#a6e22e">Name</span>())
        <span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Export</span>(<span style="color:#e6db74">&#34;kubeConfig&#34;</span>, <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Unsecret</span>(<span style="color:#a6e22e">lke_config</span>))
</code></pre></div><p>The first is just the name of the deployment. The second is our <code>kubeconfig</code>. We need to use the <code>pulumi.Unsecret</code> function because by default the <code>kubeconfig</code> output is a secret. This means that it won&rsquo;t be exposed into <code>stdout</code>, meaning it wouldn&rsquo;t be seen in in logs for the process running Pulumi (e.g. a CI/CD system). In our case though, we want to output it unencrypted it so that we can inspect the cluster ourselves.</p>
<h3 id="running-pulumi-3">Running Pulumi</h3>
<p>Now we can run Pulumi, see if we get an LKE cluster, and if our application is deployed: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">Previewing update <span style="color:#f92672">(</span>staging<span style="color:#f92672">)</span>

View Live: https://app.pulumi.com/yetiops/lke-go/staging/previews/0d68a411-ca47-49e0-a63a-e54cd1c278bd

     Type                              Name            Plan
 +   pulumi:pulumi:Stack               lke-go-staging  create
 +   ├─ linode:index:LkeCluster        yetiops-prom    create
 +   ├─ pulumi:providers:kubernetes    yetiops-prom    create
 +   ├─ kubernetes:core/v1:Namespace   app-ns          create
 +   └─ kubernetes:apps/v1:Deployment  app-dep         create

Resources:
    + <span style="color:#ae81ff">5</span> to create

Do you want to perform this update? yes
Updating <span style="color:#f92672">(</span>staging<span style="color:#f92672">)</span>

View Live: https://app.pulumi.com/yetiops/lke-go/staging/updates/10

     Type                              Name            Status
 +   pulumi:pulumi:Stack               lke-go-staging  created
 +   ├─ linode:index:LkeCluster        yetiops-prom    created
 +   ├─ pulumi:providers:kubernetes    yetiops-prom    created
 +   ├─ kubernetes:core/v1:Namespace   app-ns          created
 +   └─ kubernetes:apps/v1:Deployment  app-dep         created

Outputs:
    kubeConfig: <span style="color:#e6db74">&#34;### KUBECONFIG OUTPUT ###&#34;</span>
    name      : <span style="color:#e6db74">&#34;app-dep-nvxmwgxw&#34;</span>

Resources:
    + <span style="color:#ae81ff">5</span> created

Duration: 4m4s
</code></pre></div><p>We can see that our cluster was created in the Linode console: -</p>
<p><img src="/img/pulumi/lke-cluster.png" alt="LKE Cluster in Linode Console"></p>
<p>If we take the output of <code>kubeConfig</code> and put it into our <code>~/.kube/config</code> file, we can also check the cluster itself: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ kubectl get nodes
NAME                          STATUS   ROLES    AGE     VERSION
lke24018-30565-607487089a4e   Ready    &lt;none&gt;   3m12s   v1.20.5
lke24018-30565-60748708f09e   Ready    &lt;none&gt;   3m11s   v1.20.5
lke24018-30565-607487094690   Ready    &lt;none&gt;   3m9s    v1.20.5

$ kubectl get nodes -o wide
NAME                          STATUS   ROLES    AGE     VERSION   INTERNAL-IP       EXTERNAL-IP      OS-IMAGE                       KERNEL-VERSION         CONTAINER-RUNTIME
lke24018-30565-607487089a4e   Ready    &lt;none&gt;   3m27s   v1.20.5   192.168.147.230   178.79.134.242   Debian GNU/Linux <span style="color:#ae81ff">9</span> <span style="color:#f92672">(</span>stretch<span style="color:#f92672">)</span>   5.10.0-5-cloud-amd64   docker://19.3.15
lke24018-30565-60748708f09e   Ready    &lt;none&gt;   3m26s   v1.20.5   192.168.149.192   178.79.158.241   Debian GNU/Linux <span style="color:#ae81ff">9</span> <span style="color:#f92672">(</span>stretch<span style="color:#f92672">)</span>   5.10.0-5-cloud-amd64   docker://19.3.15
lke24018-30565-607487094690   Ready    &lt;none&gt;   3m24s   v1.20.5   192.168.164.66    178.79.157.243   Debian GNU/Linux <span style="color:#ae81ff">9</span> <span style="color:#f92672">(</span>stretch<span style="color:#f92672">)</span>   5.10.0-5-cloud-amd64   docker://19.3.15

$ kubectl get deployments -A
NAMESPACE      NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
kube-system    calico-kube-controllers   1/1     <span style="color:#ae81ff">1</span>            <span style="color:#ae81ff">1</span>           6m15s
kube-system    coredns                   2/2     <span style="color:#ae81ff">2</span>            <span style="color:#ae81ff">2</span>           6m13s
yetiops-prom   app-dep-nvxmwgxw          1/1     <span style="color:#ae81ff">1</span>            <span style="color:#ae81ff">1</span>           5m43s

$ kubectl describe deployments app-dep-nvxmwgxw -n yetiops-prom
Name:                   app-dep-nvxmwgxw
Namespace:              yetiops-prom
CreationTimestamp:      Mon, <span style="color:#ae81ff">12</span> Apr <span style="color:#ae81ff">2021</span> 18:45:54 +0100
Labels:                 app.kubernetes.io/managed-by<span style="color:#f92672">=</span>pulumi
Annotations:            deployment.kubernetes.io/revision: <span style="color:#ae81ff">1</span>
                        pulumi.com/autonamed: true
Selector:               app<span style="color:#f92672">=</span>nginx
Replicas:               <span style="color:#ae81ff">1</span> desired | <span style="color:#ae81ff">1</span> updated | <span style="color:#ae81ff">1</span> total | <span style="color:#ae81ff">1</span> available | <span style="color:#ae81ff">0</span> unavailable
StrategyType:           RollingUpdate
MinReadySeconds:        <span style="color:#ae81ff">0</span>
RollingUpdateStrategy:  25% max unavailable, 25% max surge
Pod Template:
  Labels:  app<span style="color:#f92672">=</span>nginx
  Containers:
   nginx:
    Image:        nginx
    Port:         &lt;none&gt;
    Host Port:    &lt;none&gt;
    Environment:  &lt;none&gt;
    Mounts:       &lt;none&gt;
  Volumes:        &lt;none&gt;
Conditions:
  Type           Status  Reason
  ----           ------  ------
  Available      True    MinimumReplicasAvailable
  Progressing    True    NewReplicaSetAvailable
OldReplicaSets:  &lt;none&gt;
NewReplicaSet:   app-dep-nvxmwgxw-6799fc88d8 <span style="color:#f92672">(</span>1/1 replicas created<span style="color:#f92672">)</span>
Events:
  Type    Reason             Age   From                   Message
  ----    ------             ----  ----                   -------
  Normal  ScalingReplicaSet  6m9s  deployment-controller  Scaled up replica set app-dep-nvxmwgxw-6799fc88d8 to <span style="color:#ae81ff">1</span>

$ kubectl get pods -A
NAMESPACE      NAME                                      READY   STATUS    RESTARTS   AGE
kube-system    calico-kube-controllers-b9575d85c-m5zvm   1/1     Running   <span style="color:#ae81ff">1</span>          5m19s
kube-system    calico-node-7mrgk                         1/1     Running   <span style="color:#ae81ff">0</span>          3m49s
kube-system    calico-node-r58mj                         1/1     Running   <span style="color:#ae81ff">0</span>          3m51s
kube-system    calico-node-skxz5                         1/1     Running   <span style="color:#ae81ff">0</span>          3m52s
kube-system    coredns-5f8bfcb47-9xqmb                   1/1     Running   <span style="color:#ae81ff">0</span>          5m19s
kube-system    coredns-5f8bfcb47-sgrdp                   1/1     Running   <span style="color:#ae81ff">0</span>          5m19s
kube-system    csi-linode-controller-0                   4/4     Running   <span style="color:#ae81ff">0</span>          5m19s
kube-system    csi-linode-node-8wxk4                     2/2     Running   <span style="color:#ae81ff">0</span>          3m8s
kube-system    csi-linode-node-jmbtr                     2/2     Running   <span style="color:#ae81ff">0</span>          3m10s
kube-system    csi-linode-node-p2l7v                     2/2     Running   <span style="color:#ae81ff">0</span>          2m58s
kube-system    kube-proxy-5v8v8                          1/1     Running   <span style="color:#ae81ff">0</span>          3m51s
kube-system    kube-proxy-nbwrx                          1/1     Running   <span style="color:#ae81ff">0</span>          3m49s
kube-system    kube-proxy-q547g                          1/1     Running   <span style="color:#ae81ff">0</span>          3m52s
yetiops-prom   app-dep-nvxmwgxw-6799fc88d8-n2x2w         1/1     Running   <span style="color:#ae81ff">0</span>          5m3s
</code></pre></div><p>Lets <strong>exec</strong> into the container as well: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ kubectl exec -n yetiops-prom -it app-dep-nvxmwgxw-6799fc88d8-n2x2w -- /bin/bash

root@app-dep-nvxmwgxw-6799fc88d8-n2x2w:/# cat /etc/os-release
PRETTY_NAME<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Debian GNU/Linux 10 (buster)&#34;</span>
NAME<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Debian GNU/Linux&#34;</span>
VERSION_ID<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;10&#34;</span>
VERSION<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;10 (buster)&#34;</span>
VERSION_CODENAME<span style="color:#f92672">=</span>buster
ID<span style="color:#f92672">=</span>debian
HOME_URL<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://www.debian.org/&#34;</span>
SUPPORT_URL<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://www.debian.org/support&#34;</span>
BUG_REPORT_URL<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://bugs.debian.org/&#34;</span>

root@app-dep-nvxmwgxw-6799fc88d8-n2x2w:/# curl localhost:80
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;title&gt;Welcome to nginx!&lt;/title&gt;
&lt;style&gt;
    body <span style="color:#f92672">{</span>
        width: 35em;
        margin: <span style="color:#ae81ff">0</span> auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    <span style="color:#f92672">}</span>
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;Welcome to nginx!&lt;/h1&gt;
&lt;p&gt;If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.&lt;/p&gt;

&lt;p&gt;For online documentation and support please refer to
&lt;a href<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;http://nginx.org/&#34;</span>&gt;nginx.org&lt;/a&gt;.&lt;br/&gt;
Commercial support is available at
&lt;a href<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;http://nginx.com/&#34;</span>&gt;nginx.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thank you <span style="color:#66d9ef">for</span> using nginx.&lt;/em&gt;&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;

root@app-dep-nvxmwgxw-6799fc88d8-n2x2w:/# curl -k https://kubernetes.default.svc:443
<span style="color:#f92672">{</span>
  <span style="color:#e6db74">&#34;kind&#34;</span>: <span style="color:#e6db74">&#34;Status&#34;</span>,
  <span style="color:#e6db74">&#34;apiVersion&#34;</span>: <span style="color:#e6db74">&#34;v1&#34;</span>,
  <span style="color:#e6db74">&#34;metadata&#34;</span>: <span style="color:#f92672">{</span>

  <span style="color:#f92672">}</span>,
  <span style="color:#e6db74">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;Failure&#34;</span>,
  <span style="color:#e6db74">&#34;message&#34;</span>: <span style="color:#e6db74">&#34;forbidden: User \&#34;system:anonymous\&#34; cannot get path \&#34;/\&#34;&#34;</span>,
  <span style="color:#e6db74">&#34;reason&#34;</span>: <span style="color:#e6db74">&#34;Forbidden&#34;</span>,
  <span style="color:#e6db74">&#34;details&#34;</span>: <span style="color:#f92672">{</span>

  <span style="color:#f92672">}</span>,
  <span style="color:#e6db74">&#34;code&#34;</span>: <span style="color:#ae81ff">403</span>
</code></pre></div><p>All looking good!</p>
<h3 id="all-the-code-3">All the code</h3>
<p>The following is the full <code>main.go</code> file with all of our code: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#a6e22e">b64</span> <span style="color:#e6db74">&#34;encoding/base64&#34;</span>

	<span style="color:#e6db74">&#34;github.com/pulumi/pulumi-kubernetes/sdk/v2/go/kubernetes&#34;</span>
	<span style="color:#a6e22e">appsv1</span> <span style="color:#e6db74">&#34;github.com/pulumi/pulumi-kubernetes/sdk/v2/go/kubernetes/apps/v1&#34;</span>
	<span style="color:#a6e22e">corev1</span> <span style="color:#e6db74">&#34;github.com/pulumi/pulumi-kubernetes/sdk/v2/go/kubernetes/core/v1&#34;</span>
	<span style="color:#a6e22e">metav1</span> <span style="color:#e6db74">&#34;github.com/pulumi/pulumi-kubernetes/sdk/v2/go/kubernetes/meta/v1&#34;</span>
	<span style="color:#e6db74">&#34;github.com/pulumi/pulumi-linode/sdk/v2/go/linode&#34;</span>
	<span style="color:#e6db74">&#34;github.com/pulumi/pulumi/sdk/v2/go/pulumi&#34;</span>
	<span style="color:#e6db74">&#34;github.com/pulumi/pulumi/sdk/v2/go/pulumi/config&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Context</span>) <span style="color:#66d9ef">error</span> {
		<span style="color:#a6e22e">conf</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;&#34;</span>)
		<span style="color:#a6e22e">commonName</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">conf</span>.<span style="color:#a6e22e">Require</span>(<span style="color:#e6db74">&#34;common_name&#34;</span>)

		<span style="color:#a6e22e">cluster</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">linode</span>.<span style="color:#a6e22e">NewLkeCluster</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">linode</span>.<span style="color:#a6e22e">LkeClusterArgs</span>{
			<span style="color:#a6e22e">K8sVersion</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;1.20&#34;</span>),
			<span style="color:#a6e22e">Label</span>:      <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">commonName</span>),
			<span style="color:#a6e22e">Pools</span>: <span style="color:#a6e22e">linode</span>.<span style="color:#a6e22e">LkeClusterPoolArray</span>{
				<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">linode</span>.<span style="color:#a6e22e">LkeClusterPoolArgs</span>{
					<span style="color:#a6e22e">Count</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Int</span>(<span style="color:#ae81ff">3</span>),
					<span style="color:#a6e22e">Type</span>:  <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;g6-standard-1&#34;</span>),
				},
			},
			<span style="color:#a6e22e">Region</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;eu-west&#34;</span>),
			<span style="color:#a6e22e">Tags</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
				<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;staging&#34;</span>),
				<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;lke-yetiops&#34;</span>),
			},
		})

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">lke_config</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cluster</span>.<span style="color:#a6e22e">Kubeconfig</span>.<span style="color:#a6e22e">ApplyString</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">kconf</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">string</span> {
			<span style="color:#a6e22e">output</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">b64</span>.<span style="color:#a6e22e">StdEncoding</span>.<span style="color:#a6e22e">DecodeString</span>(<span style="color:#a6e22e">kconf</span>)
			<span style="color:#66d9ef">return</span> string(<span style="color:#a6e22e">output</span>)
		})

		<span style="color:#a6e22e">kubeProvider</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">kubernetes</span>.<span style="color:#a6e22e">NewProvider</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">kubernetes</span>.<span style="color:#a6e22e">ProviderArgs</span>{
			<span style="color:#a6e22e">Kubeconfig</span>: <span style="color:#a6e22e">lke_config</span>,
		}, <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">DependsOn</span>([]<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Resource</span>{<span style="color:#a6e22e">cluster</span>}))

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">appLabels</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringMap</span>{
			<span style="color:#e6db74">&#34;app&#34;</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;nginx&#34;</span>),
		}

		<span style="color:#a6e22e">namespace</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">corev1</span>.<span style="color:#a6e22e">NewNamespace</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;app-ns&#34;</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">corev1</span>.<span style="color:#a6e22e">NamespaceArgs</span>{
			<span style="color:#a6e22e">Metadata</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metav1</span>.<span style="color:#a6e22e">ObjectMetaArgs</span>{
				<span style="color:#a6e22e">Name</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">commonName</span>),
			},
		}, <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Provider</span>(<span style="color:#a6e22e">kubeProvider</span>))

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">deployment</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">appsv1</span>.<span style="color:#a6e22e">NewDeployment</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;app-dep&#34;</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">appsv1</span>.<span style="color:#a6e22e">DeploymentArgs</span>{
			<span style="color:#a6e22e">Metadata</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metav1</span>.<span style="color:#a6e22e">ObjectMetaArgs</span>{
				<span style="color:#a6e22e">Namespace</span>: <span style="color:#a6e22e">namespace</span>.<span style="color:#a6e22e">Metadata</span>.<span style="color:#a6e22e">Elem</span>().<span style="color:#a6e22e">Name</span>(),
			},
			<span style="color:#a6e22e">Spec</span>: <span style="color:#a6e22e">appsv1</span>.<span style="color:#a6e22e">DeploymentSpecArgs</span>{
				<span style="color:#a6e22e">Selector</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metav1</span>.<span style="color:#a6e22e">LabelSelectorArgs</span>{
					<span style="color:#a6e22e">MatchLabels</span>: <span style="color:#a6e22e">appLabels</span>,
				},
				<span style="color:#a6e22e">Replicas</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Int</span>(<span style="color:#ae81ff">1</span>),
				<span style="color:#a6e22e">Template</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">corev1</span>.<span style="color:#a6e22e">PodTemplateSpecArgs</span>{
					<span style="color:#a6e22e">Metadata</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metav1</span>.<span style="color:#a6e22e">ObjectMetaArgs</span>{
						<span style="color:#a6e22e">Labels</span>: <span style="color:#a6e22e">appLabels</span>,
					},
					<span style="color:#a6e22e">Spec</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">corev1</span>.<span style="color:#a6e22e">PodSpecArgs</span>{
						<span style="color:#a6e22e">Containers</span>: <span style="color:#a6e22e">corev1</span>.<span style="color:#a6e22e">ContainerArray</span>{
							<span style="color:#a6e22e">corev1</span>.<span style="color:#a6e22e">ContainerArgs</span>{
								<span style="color:#a6e22e">Name</span>:  <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;nginx&#34;</span>),
								<span style="color:#a6e22e">Image</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;nginx&#34;</span>),
							}},
					},
				},
			},
		}, <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Provider</span>(<span style="color:#a6e22e">kubeProvider</span>))

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Export</span>(<span style="color:#e6db74">&#34;name&#34;</span>, <span style="color:#a6e22e">deployment</span>.<span style="color:#a6e22e">Metadata</span>.<span style="color:#a6e22e">Elem</span>().<span style="color:#a6e22e">Name</span>())
		<span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Export</span>(<span style="color:#e6db74">&#34;kubeConfig&#34;</span>, <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Unsecret</span>(<span style="color:#a6e22e">lke_config</span>))

		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
	})
}
</code></pre></div><h2 id="summary">Summary</h2>
<p>In the previous post we discovered that Pulumi is a very powerful Infrastructure-as-Code tool. From being able to choose from any one of five (at the time of speaking) languages to define your infrastructure, to defining your own functions as part of the code, Pulumi offers a great deal of flexibility when creating and codifying your infrastructure.</p>
<p>Now we also can see that isn&rsquo;t limited to just the big few cloud providers. Even with some of the smaller or more esoteric providers, there is still a way to define your infrastructure with Pulumi.</p>
<p>What this also means is that if you want to use another provider for either cost reasons, a different approach (e.g. Equinix Metal with their bare metal offering) or other reasons, you don&rsquo;t have to throw out all of your tooling to use it. Using tools like Pulumi and Terraform offer a migration path without a complete refactor.</p>
]]></content>
        </item>
        
        <item>
            <title>Pulumi: Infrastructure as Code meets programming</title>
            <link>https://yetiops.net/posts/pulumi-iac-terraform/</link>
            <pubDate>Wed, 07 Apr 2021 17:37:54 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/pulumi-iac-terraform/</guid>
            <description>Infrastructure as Code is the practice of managing your infrastructure (in a cloud provider, on premises, databases, monitoring and more) in a declarative manner. This means that rather than creating resources manually through the provider/server console, or manually setting up individual services on bare metal/virtualised servers, the infrastructure itself is managed using some form of code.
This has many advantages in that: -
 It is repeatable - If your infrastructure is defined in code, then it is easy to rebuild it, or make a copy of it It is documented - The code declares the infrastructure, with all the steps required to build it It is consistent - You can define modules which build infrastructure in the same way (e.</description>
            <content type="html"><![CDATA[<p>Infrastructure as Code is the practice of managing your infrastructure (in a cloud provider, on premises, databases, monitoring and more) in a declarative manner. This means that rather than creating resources manually through the provider/server console, or manually setting up individual services on bare metal/virtualised servers, the infrastructure itself is managed using some form of code.</p>
<p>This has many advantages in that: -</p>
<ul>
<li>It is <strong>repeatable</strong> - If your infrastructure is defined in code, then it is easy to rebuild it, or make a copy of it</li>
<li>It is <strong>documented</strong> - The code declares the infrastructure, with all the steps required to build it</li>
<li>It is <strong>consistent</strong> - You can define modules which build infrastructure in the same way (e.g. required tags, required security group rules, minimum access policies etc), without needing a checklist or relying on an engineer&rsquo;s memory</li>
<li>Changes are <strong>trackable</strong> - If you use version control for your Infrastructure Code, you can see what was changed
<ul>
<li>Commit messages will also help you determine why as well</li>
</ul>
</li>
<li>It is <strong>testable</strong> - You can build deployment pipelines that will test the code, ensuring it is secure and does what is intended</li>
</ul>
<p>There are a number of other benefits too, but the above are more than enough to make using IaC (<strong>I</strong>nfrastructure-<strong>a</strong>s-<strong>C</strong>ode) a worthwhile endeavour.</p>
<h2 id="tooling">Tooling</h2>
<p>In terms of IaC tooling, there are quite a few available.</p>
<p>Configuration management tools like <a href="https://www.ansible.com">Ansible</a>, <a href="https://docs.saltproject.io/en/latest">Saltstack</a>, <a href="https://puppet.com/">Puppet</a> and <a href="https://www.chef.io/products/chef-infra">Chef</a> are used to deploy applications and define how they are configured. This can go a long way to making your builds and applications more consistent. However when you need to manage the underlying infrastructure itself, they are not always as well suited. These tools are examples of &ldquo;imperative&rdquo; configuration. They can define how to create the infrastructure and all the steps to get there, but they rarely define the final state.</p>
<p>It is perfectly possible to create an AWS EC2 instance with Ansible for example, but checking whether there is already an instance that matches your <em>desired state</em> is can be very complex. It not only has to build it, but it has to know whether it has built one before, how many have been built already, and whether they are configured correctly.</p>
<p>The better option for this is a &ldquo;declarative&rdquo; tool. Rather than telling the tool what to build and how to build it, you define the end state (e.g. &ldquo;I want a Kubernetes cluster in Google Cloud, with GPU-optimized instances&rdquo;) and let the tool take care of how to reach that end state. The most popular tool in this space is Hashicorp&rsquo;s <a href="https://www.terraform.io/">Terraform</a>.</p>
<h2 id="terraform">Terraform</h2>
<p>Terraform has become very popular in the IaC space, especially in managing cloud infrastructure, for a number of reasons.</p>
<p>First, it is a single binary without any other dependencies. This makes it very easy to install. Secondly, it uses a Domain-Specific Language called HCL (Hashicorp Configuration Language). HCL does not have a lot of features that a newcomer needs to learn to become productive, making the barrier to entry quite low.</p>
<p>HCL also supports the concept of modules, meaning you can have a file that contains the &ldquo;standard&rdquo; way you want to build a unit (or units) of infrastructure, with a few parameters to make it unique (e.g. a name, startup scripts etc).</p>
<p>It also maintains a &ldquo;state&rdquo; file. This file contains the last known state of the infrastructure, which allows Terraform to reconcile between what it knows the state to be, with what the new desired state is (i.e. what changes are required based upon updates to the HCL files).</p>
<h3 id="limitations">Limitations</h3>
<p>Over the years since Terraform was introduced, HCL has grown to include features that you would expect in a standard programming language, including <a href="https://www.terraform.io/docs/language/expressions/dynamic-blocks.html">dynamic blocks</a>, <a href="https://www.terraform.io/docs/language/expressions/for.html">for loops</a> and <a href="https://www.terraform.io/docs/language/expressions/operators.html">operators</a>.</p>
<p>These can add complexity to Terraform code, as sometimes the logic is not immediately obvious. Also, there are limitations to these features, meaning that you sometimes work against HCL to achieve your end state, or have some infrastructure lie outside of it.</p>
<p>To give an example, the below is a dynamic block in Terraform: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;global_replicas&#34;</span> {
  type        <span style="color:#f92672">=</span> <span style="color:#66d9ef">list</span>
  default     <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;eu-west-1&#34;, &#34;eu-west-2&#34;</span>]
  description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Add the ability to define global tables (v2 - 2019.11.21) in DynamoDB&#34;</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_dynamodb_table&#34; &#34;table&#34;</span> {
  <span style="color:#66d9ef">dynamic</span> <span style="color:#e6db74">&#34;replica&#34;</span> {
    for_each <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">global_replicas</span>
    <span style="color:#66d9ef">content</span> {
      region_name            <span style="color:#f92672">=</span> <span style="color:#66d9ef">replica</span>.<span style="color:#66d9ef">value</span>.<span style="color:#66d9ef">region_name</span>
    }
  }
</code></pre></div><p>Traditionally in a for loop, you would usually expect to reference a variable as the value. That it also references the field name in the value as well is a potential cause for confusion too. If you did something similar in Python, it would look something like this: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python">global_replicas <span style="color:#f92672">=</span> [
  <span style="color:#e6db74">&#34;eu-west-1&#34;</span>,
  <span style="color:#e6db74">&#34;eu-west-2&#34;</span>
]

<span style="color:#66d9ef">for</span> replica <span style="color:#f92672">in</span> global_replicas:
  region_name <span style="color:#f92672">=</span> replica
</code></pre></div><p>Compared to the Terraform for loop, this doesn&rsquo;t take much to comprehend.</p>
<p>Terraform also has other constraints on what can and can&rsquo;t be dynamically created. Up until the most recent versions of Terraform, resources (i.e. declaring what you want directly) could be created dynamically (i.e. based upon the contents of a list, or a count) but references to modules could not. This added complexity to modules, as they would have to account for multiple resource creations, even if most of the time only 1 resource would be.</p>
<p>In a situation like this, you may find yourself wanting to use a standard programming language. This is where <a href="https://www.pulumi.com/">Pulumi</a> comes in.</p>
<h2 id="what-is-pulumi">What is Pulumi?</h2>
<p>Pulumi is another IaC tool. Instead of using it&rsquo;s own Domain-Specific Language, you can use a number of standard programming languages. Currently Pulumi supports Javascript, Typescript (a variant of Javascript that introduces static typing), Python, Go and DotNet.</p>
<p>Pulumi offers an SDK for your programming language of choice, and is usually split into modules/packages. This also allows you to include only the modules you need to use (e.g. only the AWS EC2 module or only the Azure Blob Storage module).</p>
<p>Pulumi uses very similar concepts to Terraform, in that you declare your infrastructure as code, and the tool (Pulumi in this case) ensures the end result matches what you have declared. It also manages state, so that it knows what your infrastructure previously looked like, and what needs to be added/updated/removed to reach the new desired state.</p>
<p>Pulumi introduces the concepts of <em>Projects</em> and <em>Stacks</em>. A <em>Project</em> defines what runtimes are used (i.e. which language) and also specifies detail about your project (e.g. a description, any other metadata). A <em>Stack</em> sits inside a project. It is an instance/environment that you create your infrastructure inside. It usually denotes the <strong>stage</strong> it is deployed at (e.g. <code>staging</code>, <code>development</code>, <code>production</code> etc).</p>
<p>It uses many of the Terraform <a href="https://www.terraform.io/docs/providers/index.html">providers</a> (i.e. what Terraform uses to communicate with your cloud/infrastructure/application provider of choice). This means Pulumi usually supports the same cloud providers/infrastructure as Terraform, or at least has the capability to.</p>
<h2 id="how-do-i-start-using-it">How do I start using it?</h2>
<p>To start using Pulumi, first you need to install the Pulumi tool itself. Follow the instructions <a href="https://www.pulumi.com/docs/get-started/install/">here</a> based upon your platform of choice. The CLI itself is used to manage projects and stacks, as well as standing up your infrastructure. If you are familiar with Terraform already, the Pulumi CLI serves the same functions as the Terraform binary, and the code you create is analogous to the resources/modules you create in your HCL-based <code>.tf</code> files.</p>
<p>Once Pulumi is installed, create an empty directory, and run <code>pulumi new $provider-$language --name $stack-name</code>. Replace <code>$provider</code> with the infrastructure/cloud you are targeting (e.g. <code>aws</code>, <code>azure</code>, <code>cloudinit</code>), choose one of <code>typescript</code>, <code>javascript</code>, <code>python</code>, <code>go</code> or <code>csharp</code> as the language, and choose a relevant Stack Name (e.g. <code>my-first-stack</code> or <code>squad-dynamodb-staging</code>). You will be greeted with something like the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ pulumi new aws-go --name yetiops-blog-aws-staging
This command will walk you through creating a new Pulumi project.

Enter a value or leave blank to accept the <span style="color:#f92672">(</span>default<span style="color:#f92672">)</span>, and press &lt;ENTER&gt;.
Press ^C at any time to quit.

project description: <span style="color:#f92672">(</span>A minimal AWS Go Pulumi program<span style="color:#f92672">)</span> This is <span style="color:#66d9ef">for</span> the blog
Created project <span style="color:#e6db74">&#39;yetiops-blog-aws-staging&#39;</span>

Please enter your desired stack name.
To create a stack in an organization, use the format &lt;org-name&gt;/&lt;stack-name&gt; <span style="color:#f92672">(</span>e.g. <span style="color:#e6db74">`</span>acmecorp/dev<span style="color:#e6db74">`</span><span style="color:#f92672">)</span>.
stack name: <span style="color:#f92672">(</span>dev<span style="color:#f92672">)</span> yetiops/staging
Created stack <span style="color:#e6db74">&#39;staging&#39;</span>

aws:region: The AWS region to deploy into: <span style="color:#f92672">(</span>us-east-1<span style="color:#f92672">)</span> eu-west-2
Saved config

Installing dependencies...

Finished installing dependencies

Your new project is ready to go! ✨

To perform an initial deployment, run <span style="color:#e6db74">&#39;pulumi up&#39;</span>
</code></pre></div><p>You will then be presented with the following directory structure: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ tree
.
|-- Pulumi.staging.yaml
|-- Pulumi.yaml
|-- go.mod
|-- go.sum
<span style="color:#e6db74">`</span>-- main.go

<span style="color:#ae81ff">0</span> directories, <span style="color:#ae81ff">5</span> files
</code></pre></div><p>Each file represents the following: -</p>
<ul>
<li><code>Pulumi.staging.yaml</code> - This is configuration specific to the stack</li>
<li><code>Pulumi.yaml</code> - This is the project definition, which contains the <code>name</code>, <code>runtime</code> and <code>description</code> of the project
<ul>
<li>As noted, multiple stacks can sit inside a project. You can expect to see one <code>Pulumi.yaml</code>, and multiple <code>Pulumi.$STACK-NAME.yaml</code> files</li>
</ul>
</li>
<li><code>main.go</code> - Define your infrastructure in here, using Go</li>
<li><code>go.mod</code> and <code>go.sum</code> - These are Go module definitions (see <a href="https://blog.golang.org/using-go-modules">here</a> if you are not familiar)</li>
</ul>
<p>The contents of each file look like the below: -</p>
<p><strong>Pulumi.staging.yaml</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">config</span>:
  <span style="color:#66d9ef">aws:region</span>: eu-west<span style="color:#ae81ff">-2</span>
</code></pre></div><p><strong>Pulumi.yaml</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">name</span>: yetiops-blog-aws-staging
<span style="color:#66d9ef">runtime</span>: go
<span style="color:#66d9ef">description</span>: This is for the blog
</code></pre></div><p><strong>main.go</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;github.com/pulumi/pulumi-aws/sdk/v3/go/aws/s3&#34;</span>
	<span style="color:#e6db74">&#34;github.com/pulumi/pulumi/sdk/v2/go/pulumi&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Context</span>) <span style="color:#66d9ef">error</span> {
		<span style="color:#75715e">// Create an AWS resource (S3 Bucket)
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">bucket</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s3</span>.<span style="color:#a6e22e">NewBucket</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;my-bucket&#34;</span>, <span style="color:#66d9ef">nil</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#75715e">// Export the name of the bucket
</span><span style="color:#75715e"></span>		<span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Export</span>(<span style="color:#e6db74">&#34;bucketName&#34;</span>, <span style="color:#a6e22e">bucket</span>.<span style="color:#a6e22e">ID</span>())
		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
	})
}
</code></pre></div><p><strong>go.mod</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#a6e22e">module</span> <span style="color:#a6e22e">yetiops</span><span style="color:#f92672">-</span><span style="color:#a6e22e">blog</span><span style="color:#f92672">-</span><span style="color:#a6e22e">aws</span><span style="color:#f92672">-</span><span style="color:#a6e22e">staging</span>

<span style="color:#66d9ef">go</span> <span style="color:#ae81ff">1.14</span>

<span style="color:#a6e22e">require</span> (
	<span style="color:#a6e22e">github</span>.<span style="color:#a6e22e">com</span><span style="color:#f92672">/</span><span style="color:#a6e22e">pulumi</span><span style="color:#f92672">/</span><span style="color:#a6e22e">pulumi</span><span style="color:#f92672">-</span><span style="color:#a6e22e">aws</span><span style="color:#f92672">/</span><span style="color:#a6e22e">sdk</span><span style="color:#f92672">/</span><span style="color:#a6e22e">v3</span> <span style="color:#a6e22e">v3</span><span style="color:#ae81ff">.26.1</span>
	<span style="color:#a6e22e">github</span>.<span style="color:#a6e22e">com</span><span style="color:#f92672">/</span><span style="color:#a6e22e">pulumi</span><span style="color:#f92672">/</span><span style="color:#a6e22e">pulumi</span><span style="color:#f92672">/</span><span style="color:#a6e22e">sdk</span><span style="color:#f92672">/</span><span style="color:#a6e22e">v2</span> <span style="color:#a6e22e">v2</span><span style="color:#ae81ff">.19.0</span>
)
</code></pre></div><p>I won&rsquo;t include the <code>go.sum</code> file as it is 427 lines long and contains calculated sums of each dependency that the Pulumi modules rely upon.</p>
<p>As you can see, Pulumi already brings in a few base dependencies, and everything you need to start building your infrastructure. We can now run <code>pulumi up</code> to see what will happen: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ pulumi up
Previewing update <span style="color:#f92672">(</span>staging<span style="color:#f92672">)</span>

View Live: https://app.pulumi.com/yetiops/yetiops-blog-aws-staging/staging/previews/e9bdbddd-90fb-4cb3-b6d3-91d0c3af8199

     Type                 Name                              Plan
 +   pulumi:pulumi:Stack  yetiops-blog-aws-staging-staging  create
 +   └─ aws:s3:Bucket     my-bucket                         create

Resources:
    + <span style="color:#ae81ff">2</span> to create

Do you want to perform this update? yes
Updating <span style="color:#f92672">(</span>staging<span style="color:#f92672">)</span>

View Live: https://app.pulumi.com/yetiops/yetiops-blog-aws-staging/staging/updates/1

     Type                 Name                              Status
 +   pulumi:pulumi:Stack  yetiops-blog-aws-staging-staging  created
 +   └─ aws:s3:Bucket     my-bucket                         created

Outputs:
    bucketName: <span style="color:#e6db74">&#34;my-bucket-00d14a7&#34;</span>

Resources:
    + <span style="color:#ae81ff">2</span> created

Duration: 9s
</code></pre></div><p>We can also tear it down again using <code>pulumi destroy</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">Previewing destroy <span style="color:#f92672">(</span>staging<span style="color:#f92672">)</span>

View Live: https://app.pulumi.com/yetiops/yetiops-blog-aws-staging/staging/previews/4a846464-faa9-4d39-8971-0ec65d130d15

     Type                 Name                              Plan
 -   pulumi:pulumi:Stack  yetiops-blog-aws-staging-staging  delete
 -   └─ aws:s3:Bucket     my-bucket                         delete

Outputs:
  - bucketName: <span style="color:#e6db74">&#34;my-bucket-00d14a7&#34;</span>

Resources:
    - <span style="color:#ae81ff">2</span> to delete

Do you want to perform this destroy? yes
Destroying <span style="color:#f92672">(</span>staging<span style="color:#f92672">)</span>

View Live: https://app.pulumi.com/yetiops/yetiops-blog-aws-staging/staging/updates/2

     Type                 Name                              Status
 -   pulumi:pulumi:Stack  yetiops-blog-aws-staging-staging  deleted
 -   └─ aws:s3:Bucket     my-bucket                         deleted

Outputs:
  - bucketName: <span style="color:#e6db74">&#34;my-bucket-00d14a7&#34;</span>

Resources:
    - <span style="color:#ae81ff">2</span> deleted

Duration: 4s

The resources in the stack have been deleted, but the history and configuration associated with the stack are still maintained.
If you want to remove the stack completely, run <span style="color:#e6db74">&#39;pulumi stack rm staging&#39;</span>.
</code></pre></div><p>As noted by the message at the end, the stack still exists, we have just destroyed the resources it created.</p>
<p>Also notice that we have a link to <code>https://app.pulumi.com</code> in our output. This is because Pulumi by default uses the Pulumi Service as it&rsquo;s backend (i.e. Pulumi&rsquo;s hosted backend) for storing state. Using the Pulumi backend is free for individuals. Alternatively you can use an S3-compatible storage system (e.g. AWS S3, Azure Blob Store, Google Cloud Storage, Minio) instead. Using these backends, you can also see the progress of your services being deployed: -</p>
<p><img src="/img/pulumi/pulumi-backend.png" alt="Pulumi Service backend"></p>
<p>Finally, if you want to see information on what will be created or destroyed (depending on whether you choose <code>pulumi up</code> or <code>pulumi destroy</code>), select <code>details</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">Do you want to perform this update?  <span style="color:#f92672">[</span>Use arrows to move, enter to <span style="color:#66d9ef">select</span>, type to filter<span style="color:#f92672">]</span>
  yes
&gt; no
  details

Do you want to perform this update? details
+ pulumi:pulumi:Stack: <span style="color:#f92672">(</span>create<span style="color:#f92672">)</span>
    <span style="color:#f92672">[</span>urn<span style="color:#f92672">=</span>urn:pulumi:staging::yetiops-blog-aws-staging::pulumi:pulumi:Stack::yetiops-blog-aws-staging-staging<span style="color:#f92672">]</span>
    + aws:s3/bucket:Bucket: <span style="color:#f92672">(</span>create<span style="color:#f92672">)</span>
        <span style="color:#f92672">[</span>urn<span style="color:#f92672">=</span>urn:pulumi:staging::yetiops-blog-aws-staging::aws:s3/bucket:Bucket::my-bucket<span style="color:#f92672">]</span>
        acl         : <span style="color:#e6db74">&#34;private&#34;</span>
        bucket      : <span style="color:#e6db74">&#34;my-bucket-7f03e0b&#34;</span>
        forceDestroy: false
</code></pre></div><p>There isn&rsquo;t much for this stack, but in larger stacks you will see more than enough information to determine whether the infrastructure will be built correctly.</p>
<h2 id="building-some-infrastructure">Building some infrastructure</h2>
<p>To show the differences between using Terraform and Pulumi for IaC, I&rsquo;m going to revisit a previous post <a href="https://yetiops.net/posts/prometheus-service-discovery-aws-gcp-azure/">Prometheus Service Discovery with AWS</a> (and other providers, but I&rsquo;ll focus on AWS).</p>
<p>If you follow all the steps in the AWS section until <a href="https://yetiops.net/posts/prometheus-service-discovery-aws-gcp-azure/#configure-terraform---ec2s">Configure EC2s - Terraform</a>, your environment will have all the necessary configuration and credentials that both Terraform and Pulumi require.</p>
<p>Now we will build an Amazon EC2 (a virtual machine for those who are not familiar with AWS) that has the Prometheus <code>node_exporter</code> installed using <a href="https://cloudinit.readthedocs.io/en/latest/topics/examples.html">cloud-config</a>.</p>
<p>Since writing the previous post, Terraform is now on version <code>0.14</code>, using the new <strong>required providers</strong> syntax (allowing providers to be downloaded from the Terraform provider registry). The Terraform code in the following section uses this new syntax. Also, mostly out of intrigue, I decided to build using AWS Graviton2-based instances to see if there are any noticeable differences from a <code>t3.micro</code> instance. It helps that <code>t4g.micro</code> instances are free to trial for a few more months at the time of writing too!</p>
<h3 id="terraform-1">Terraform</h3>
<p>In our Terraform code, first we define our providers. In this case, we put in a requirement for the AWS provider, and say that the default region is <code>eu-west-2</code>.</p>
<p><strong>providers.tf</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">terraform</span> {
  <span style="color:#66d9ef">required_providers</span> {
    aws <span style="color:#f92672">=</span> {
      source  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;hashicorp/aws&#34;</span>
      version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;~&gt; 3.0&#34;</span>
    }
  }
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e"># AWS Provider
</span><span style="color:#75715e"></span><span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;aws&#34;</span> {
  region  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;eu-west-2&#34;</span>
}

</code></pre></div><p>After this, we define our <strong>user-data</strong>. This is what runs on the first boot of the instance, which can include everything from package installs, user creation, configuration files, and even triggering some form of configuration management tool (e.g. Ansible, Chef) to configure the instance.</p>
<p><strong>user_data.tf</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;template_file&#34; &#34;ubuntu&#34;</span> {
  template <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${file(&#34;${path.module}/files/ubuntu.tpl&#34;)}&#34;</span>
}

<span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;template_cloudinit_config&#34; &#34;ubuntu&#34;</span> {
  gzip          <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>
  base64_encode <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>

  <span style="color:#66d9ef">part</span> {
    filename     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;init.cfg&#34;</span>
    content_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;text/cloud-config&#34;</span>
    content      <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">template_file</span>.<span style="color:#66d9ef">ubuntu</span>.<span style="color:#66d9ef">rendered</span>
  }
}
</code></pre></div><p>In the above, we reference a file called <code>ubuntu.tpl</code>. This is a small <strong>cloud-config</strong> file that installs the Prometheus <code>node_expoter</code>.</p>
<p><strong>files/ubuntu.tpl</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e">#cloud-config</span>
<span style="color:#66d9ef">packages</span>:
 - prometheus-node-exporter
</code></pre></div><p>Finally, we have our main set of resources which are: -</p>
<ul>
<li>An AWS EC2 instance, running as a <code>t4g.micro</code> (ARM64/Graviton2-based)
<ul>
<li>This uses the Ubuntu Focal 20.04 ARM64 server image (as reference with the AWS AMI section)</li>
</ul>
</li>
<li>We create an SSH key pair in AWS (referencing an existing key on our file system) so that we can SSH into the instance</li>
<li>This will be created in the default AWS VPC, as we aren&rsquo;t specifying/creating our own</li>
<li>We create an AWS Security Group to lock down what can talk to this instance
<ul>
<li>As I&rsquo;m only creating it for this blog, I only need to open it up to my home IP address</li>
</ul>
</li>
</ul>
<p><strong>aws.tf</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;aws_ami&#34; &#34;ubuntu&#34;</span> {
  most_recent <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>

  <span style="color:#66d9ef">filter</span> {
    name   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;name&#34;</span>
    values <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;ubuntu/images/hvm-ssd/ubuntu-focal-20.04-arm64-server-*&#34;</span>]
  }

  <span style="color:#66d9ef">filter</span> {
    name   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;virtualization-type&#34;</span>
    values <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;hvm&#34;</span>]
  }

  owners <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;099720109477&#34;</span>]<span style="color:#75715e"> # Canonical
</span><span style="color:#75715e"></span>}

<span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;aws_vpc&#34; &#34;default&#34;</span> {
  default <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_key_pair&#34; &#34;yetiops-aws-prom&#34;</span> {
  key_name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span>
  public_key <span style="color:#f92672">=</span> <span style="color:#66d9ef">file</span>(<span style="color:#e6db74">&#34;~/.ssh/id_rsa.pub&#34;</span>)
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_instance&#34; &#34;yetiops-aws-prom&#34;</span> {
  ami           <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">aws_ami</span>.<span style="color:#66d9ef">ubuntu</span>.<span style="color:#66d9ef">id</span>
  instance_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;t4g.micro&#34;</span>
  user_data     <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">template_file</span>.<span style="color:#66d9ef">ubuntu</span>.<span style="color:#66d9ef">template</span>

  key_name <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_key_pair</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">aws</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">key_name</span>

  vpc_security_group_ids <span style="color:#f92672">=</span> [
    <span style="color:#66d9ef">aws_security_group</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">aws</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">id</span>
  ]

  tags <span style="color:#f92672">=</span> {
    Name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span>
    prometheus <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
    node_exporter <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
  }
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_security_group&#34; &#34;yetiops-aws-prom&#34;</span> {
  name        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span>
  description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;AWS Security Group for yetiops-aws-prom&#34;</span>
  vpc_id      <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">aws_vpc</span>.<span style="color:#66d9ef">default</span>.<span style="color:#66d9ef">id</span>

  tags <span style="color:#f92672">=</span> {
    Name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span>
  }
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_security_group_rule&#34; &#34;ingress_ssh_in&#34;</span> {
  type              <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ingress&#34;</span>
  to_port           <span style="color:#f92672">=</span> <span style="color:#ae81ff">22</span>
  protocol          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
  from_port         <span style="color:#f92672">=</span> <span style="color:#ae81ff">22</span>
  cidr_blocks       <span style="color:#f92672">=</span> [
    <span style="color:#e6db74">&#34;$MY-IP/32&#34;</span>
  ]
  security_group_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_security_group</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">aws</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">id</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_security_group_rule&#34; &#34;ingress_node_exporter_in&#34;</span> {
  type              <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ingress&#34;</span>
  to_port           <span style="color:#f92672">=</span> <span style="color:#ae81ff">9100</span>
  protocol          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
  from_port         <span style="color:#f92672">=</span> <span style="color:#ae81ff">9100</span>
  cidr_blocks       <span style="color:#f92672">=</span> [
    <span style="color:#e6db74">&#34;$MY-IP/32&#34;</span>
  ]
  security_group_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_security_group</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">aws</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">id</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_security_group_rule&#34; &#34;egress_allow_all&#34;</span> {
  type              <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;egress&#34;</span>
  to_port           <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
  protocol          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;-1&#34;</span>
  from_port         <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
  cidr_blocks       <span style="color:#f92672">=</span> [
    <span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>
  ]
  security_group_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_security_group</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">aws</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">id</span>
}
</code></pre></div><p>Using the standard Terraform commands, we create and start the instance, SSH into it, and verify if the Prometheus <code>node_exporter</code> is working: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ terraform apply
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  <span style="color:#75715e"># aws_instance.yetiops-aws-prom will be created</span>
  + resource <span style="color:#e6db74">&#34;aws_instance&#34;</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span> <span style="color:#f92672">{</span>
      + ami                          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ami-0368515722c2a5070&#34;</span>
      + arn                          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + associate_public_ip_address  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + availability_zone            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + cpu_core_count               <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + cpu_threads_per_core         <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + get_password_data            <span style="color:#f92672">=</span> false
      + host_id                      <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id                           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + instance_state               <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + instance_type                <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;t4g.micro&#34;</span>
      + ipv6_address_count           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + ipv6_addresses               <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + key_name                     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span>
      + outpost_arn                  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + password_data                <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + placement_group              <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + primary_network_interface_id <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + private_dns                  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + private_ip                   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + public_dns                   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + public_ip                    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + secondary_private_ips        <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + security_groups              <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + source_dest_check            <span style="color:#f92672">=</span> true
      + subnet_id                    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + tags                         <span style="color:#f92672">=</span> <span style="color:#f92672">{</span>
          + <span style="color:#e6db74">&#34;Name&#34;</span>          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span>
          + <span style="color:#e6db74">&#34;node_exporter&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
          + <span style="color:#e6db74">&#34;prometheus&#34;</span>    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
        <span style="color:#f92672">}</span>
      + tenancy                      <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + user_data                    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;2169d8a3e100623d34bf1a7b2f6bd924a8997bfb&#34;</span>
      + vpc_security_group_ids       <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>

      + ebs_block_device <span style="color:#f92672">{</span>
          + delete_on_termination <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + device_name           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + encrypted             <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + iops                  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + kms_key_id            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + snapshot_id           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + tags                  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + throughput            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + volume_id             <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + volume_size           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + volume_type           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
        <span style="color:#f92672">}</span>

      + enclave_options <span style="color:#f92672">{</span>
          + enabled <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
        <span style="color:#f92672">}</span>

      + ephemeral_block_device <span style="color:#f92672">{</span>
          + device_name  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + no_device    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + virtual_name <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
        <span style="color:#f92672">}</span>

      + metadata_options <span style="color:#f92672">{</span>
          + http_endpoint               <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + http_put_response_hop_limit <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + http_tokens                 <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
        <span style="color:#f92672">}</span>

      + network_interface <span style="color:#f92672">{</span>
          + delete_on_termination <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + device_index          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + network_interface_id  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
        <span style="color:#f92672">}</span>

      + root_block_device <span style="color:#f92672">{</span>
          + delete_on_termination <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + device_name           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + encrypted             <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + iops                  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + kms_key_id            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + tags                  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + throughput            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + volume_id             <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + volume_size           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + volume_type           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
        <span style="color:#f92672">}</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># aws_key_pair.yetiops-aws-prom will be created</span>
  + resource <span style="color:#e6db74">&#34;aws_key_pair&#34;</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span> <span style="color:#f92672">{</span>
      + arn         <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + fingerprint <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + key_name    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span>
      + key_pair_id <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + public_key  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCmU363A+W8ifK9zwmsMSRdvw/++FNsmBxDu16VJGvLc9n7hBgH0w/fzlbBIgwuff1hVRMQ09ZtopEJ0MGYEKo/MtvSMfYEHz4FjQWnir4aBBUjRPDo3KEhJXrZb7tBhACZRiOFfF4e6iw0Veli5dmzrqo7fTwnhHEhggJTUCwUxDm4W08k4g97OrKxrN63tKV3GDdPA3Sv3ukpVwMOzjp6rId5d+TjMYVsfYuY5feKqjhiFZi96BNNaJLfVTRib1H1XarNivSi6I5xQYkMgBXVqSqiyLYuAkxojufPZBtmzQnbO9GRyRr7eg/lz4QpKabhS7TO1bQlZrLYcIWMkGaB stuh84@deeppurplewifi.noisepalace.home&#34;</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># aws_security_group.yetiops-aws-prom will be created</span>
  + resource <span style="color:#e6db74">&#34;aws_security_group&#34;</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span> <span style="color:#f92672">{</span>
      + arn                    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + description            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;AWS Security Group for yetiops-aws-prom&#34;</span>
      + egress                 <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id                     <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + ingress                <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + name                   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span>
      + name_prefix            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + owner_id               <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + revoke_rules_on_delete <span style="color:#f92672">=</span> false
      + tags                   <span style="color:#f92672">=</span> <span style="color:#f92672">{</span>
          + <span style="color:#e6db74">&#34;Name&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span>
        <span style="color:#f92672">}</span>
      + vpc_id                 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vpc-a62062ce&#34;</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># aws_security_group_rule.egress_allow_all will be created</span>
  + resource <span style="color:#e6db74">&#34;aws_security_group_rule&#34;</span> <span style="color:#e6db74">&#34;egress_allow_all&#34;</span> <span style="color:#f92672">{</span>
      + cidr_blocks              <span style="color:#f92672">=</span> <span style="color:#f92672">[</span>
          + <span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>,
        <span style="color:#f92672">]</span>
      + from_port                <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
      + id                       <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + protocol                 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;-1&#34;</span>
      + security_group_id        <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + self                     <span style="color:#f92672">=</span> false
      + source_security_group_id <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + to_port                  <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
      + type                     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;egress&#34;</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># aws_security_group_rule.ingress_node_exporter_in will be created</span>
  + resource <span style="color:#e6db74">&#34;aws_security_group_rule&#34;</span> <span style="color:#e6db74">&#34;ingress_node_exporter_in&#34;</span> <span style="color:#f92672">{</span>
      + cidr_blocks              <span style="color:#f92672">=</span> <span style="color:#f92672">[</span>
          + <span style="color:#e6db74">&#34;</span>$MY<span style="color:#e6db74">-IP/32&#34;</span>,
        <span style="color:#f92672">]</span>
      + from_port                <span style="color:#f92672">=</span> <span style="color:#ae81ff">9100</span>
      + id                       <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + protocol                 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
      + security_group_id        <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + self                     <span style="color:#f92672">=</span> false
      + source_security_group_id <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + to_port                  <span style="color:#f92672">=</span> <span style="color:#ae81ff">9100</span>
      + type                     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ingress&#34;</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># aws_security_group_rule.ingress_ssh_in will be created</span>
  + resource <span style="color:#e6db74">&#34;aws_security_group_rule&#34;</span> <span style="color:#e6db74">&#34;ingress_ssh_in&#34;</span> <span style="color:#f92672">{</span>
      + cidr_blocks              <span style="color:#f92672">=</span> <span style="color:#f92672">[</span>
          + <span style="color:#e6db74">&#34;</span>$MY<span style="color:#e6db74">-IP/32&#34;</span>,
        <span style="color:#f92672">]</span>
      + from_port                <span style="color:#f92672">=</span> <span style="color:#ae81ff">22</span>
      + id                       <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + protocol                 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
      + security_group_id        <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + self                     <span style="color:#f92672">=</span> false
      + source_security_group_id <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + to_port                  <span style="color:#f92672">=</span> <span style="color:#ae81ff">22</span>
      + type                     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ingress&#34;</span>
    <span style="color:#f92672">}</span>

Plan: <span style="color:#ae81ff">6</span> to add, <span style="color:#ae81ff">0</span> to change, <span style="color:#ae81ff">0</span> to destroy.


Warning: Interpolation-only expressions are deprecated

  on user_data.tf line 2, in data <span style="color:#e6db74">&#34;template_file&#34;</span> <span style="color:#e6db74">&#34;ubuntu&#34;</span>:
   2:   template <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>file(<span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>path.module<span style="color:#e6db74">}</span><span style="color:#e6db74">/files/ubuntu.tpl&#34;</span>)<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>

Terraform 0.11 and earlier required all non-constant expressions to be
provided via interpolation syntax, but this pattern is now deprecated. To
silence this warning, remove the <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span> sequence from the start and the <span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
sequence from the end of this expression, leaving just the inner expression.

Template interpolation syntax is still used to construct strings from
expressions when the template includes multiple interpolation sequences or a
mixture of literal strings and interpolations. This deprecation applies only
to templates that consist entirely of a single interpolation sequence.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only <span style="color:#e6db74">&#39;yes&#39;</span> will be accepted to approve.

  Enter a value: yes

aws_key_pair.yetiops-aws-prom: Creating...
aws_security_group.yetiops-aws-prom: Creating...
aws_key_pair.yetiops-aws-prom: Creation complete after 0s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>yetiops-aws-prom<span style="color:#f92672">]</span>
aws_security_group.yetiops-aws-prom: Creation complete after 1s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>sg-02ce3d10921cf0138<span style="color:#f92672">]</span>
aws_security_group_rule.egress_allow_all: Creating...
aws_security_group_rule.ingress_node_exporter_in: Creating...
aws_security_group_rule.ingress_ssh_in: Creating...
aws_instance.yetiops-aws-prom: Creating...
aws_security_group_rule.ingress_ssh_in: Creation complete after 0s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>sgrule-235111840<span style="color:#f92672">]</span>
aws_security_group_rule.ingress_node_exporter_in: Creation complete after 1s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>sgrule-1442673345<span style="color:#f92672">]</span>
aws_security_group_rule.egress_allow_all: Creation complete after 1s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>sgrule-816809870<span style="color:#f92672">]</span>
aws_instance.yetiops-aws-prom: Still creating... <span style="color:#f92672">[</span>10s elapsed<span style="color:#f92672">]</span>
aws_instance.yetiops-aws-prom: Creation complete after 12s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>i-07b222c1a42ee0615<span style="color:#f92672">]</span>

Apply complete! Resources: <span style="color:#ae81ff">6</span> added, <span style="color:#ae81ff">0</span> changed, <span style="color:#ae81ff">0</span> destroyed.

$ ssh -i ~/.ssh/id_rsa ubuntu@35.176.83.178

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Sun Apr  <span style="color:#ae81ff">4</span> 18:04:53 UTC <span style="color:#ae81ff">2021</span>

  System load:  0.57              Processes:             <span style="color:#ae81ff">150</span>
  Usage of /:   18.2% of 7.59GB   Users logged in:       <span style="color:#ae81ff">0</span>
  Memory usage: 25%               IPv4 address <span style="color:#66d9ef">for</span> ens5: 172.31.47.158
  Swap usage:   0%

<span style="color:#ae81ff">4</span> updates can be installed immediately.
<span style="color:#ae81ff">3</span> of these updates are security updates.
To see these additional updates run: apt list --upgradable

The programs included with the Ubuntu system are free software;
the exact distribution terms <span style="color:#66d9ef">for</span> each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

To run a command as administrator <span style="color:#f92672">(</span>user <span style="color:#e6db74">&#34;root&#34;</span><span style="color:#f92672">)</span>, use <span style="color:#e6db74">&#34;sudo &lt;command&gt;&#34;</span>.
See <span style="color:#e6db74">&#34;man sudo_root&#34;</span> <span style="color:#66d9ef">for</span> details.

$ ss -tlunp
Netid           State            Recv-Q           Send-Q                            Local Address:Port                       Peer Address:Port           Process
udp             UNCONN           <span style="color:#ae81ff">0</span>                <span style="color:#ae81ff">0</span>                                 127.0.0.53%lo:53                              0.0.0.0:*
udp             UNCONN           <span style="color:#ae81ff">0</span>                <span style="color:#ae81ff">0</span>                            172.31.47.158%ens5:68                              0.0.0.0:*
tcp             LISTEN           <span style="color:#ae81ff">0</span>                <span style="color:#ae81ff">4096</span>                              127.0.0.53%lo:53                              0.0.0.0:*
tcp             LISTEN           <span style="color:#ae81ff">0</span>                <span style="color:#ae81ff">128</span>                                     0.0.0.0:22                              0.0.0.0:*
tcp             LISTEN           <span style="color:#ae81ff">0</span>                <span style="color:#ae81ff">4096</span>                                          *:9100                                  *:*
tcp             LISTEN           <span style="color:#ae81ff">0</span>                <span style="color:#ae81ff">128</span>                                        <span style="color:#f92672">[</span>::<span style="color:#f92672">]</span>:22                                 <span style="color:#f92672">[</span>::<span style="color:#f92672">]</span>:*

$ curl localhost:9100/metrics | grep -i aarch
node_uname_info<span style="color:#f92672">{</span>domainname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(none)&#34;</span>,machine<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;aarch64&#34;</span>,nodename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;ip-172-31-47-158&#34;</span>,release<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;5.4.0-1041-aws&#34;</span>,sysname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Linux&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;#43-Ubuntu SMP Fri Mar 19 22:06:27 UTC 2021&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>
</code></pre></div><p>All looks good, now to recreate this in Pulumi!</p>
<h3 id="pulumi-using-python">Pulumi using Python</h3>
<p>We will go through how to use Pulumi with Python first. Python by nature is a simpler language to read than others, almost reading like how you would describe code.</p>
<p>Python&rsquo;s requirements on variable types (i.e. whether something is a string, integer, float or otherwise) are much looser than Go and Typescript, which also helps in making a smoother transition from Terraform.</p>
<p>First, we run <code>pulumi new aws-python --name basic-ec2-py</code> in a new directory to create the stack, the project and the example files.</p>
<p>After this, we set a variable for us to use throughout the stack. This provides a common name throughout (in my case, <code>yetiops-prom</code>). This isn&rsquo;t a requirement, but it means that I can reference this variable in each of the resources I create, and it will always be the same. It also means that if I want to change it later, I only need to make that change in one place (rather than everywhere the name is referenced in code).</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ pulumi config set common_name yetiops-prom 
$ cat Pulumi.basic-ec2-py.yaml
config:
  aws:region: eu-west-2
  basic-ec2-py:common_name: yetiops-prom
</code></pre></div><h4 id="the-code">The code</h4>
<p>Now we can start writing our code! The first part references our <code>common_name</code> variable, enabling us to use it in our resources: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python">config <span style="color:#f92672">=</span> pulumi<span style="color:#f92672">.</span>Config()
common_name <span style="color:#f92672">=</span> config<span style="color:#f92672">.</span>require(<span style="color:#e6db74">&#34;common_name&#34;</span>)
</code></pre></div><p>If you had another variable (say, <code>cost_tag</code>), you would reference it in the same way (<code>cost_tag = config.require(&quot;cost_tag&quot;)</code>)</p>
<p>From now on, when we use the <code>common_name</code> variable, it will use the value we set with the Pulumi command (i.e. <code>yetiops-prom</code>). If you change it in the <code>Pulumi.$STACK-NAME.yaml</code> file, it will pick up these changes too.</p>
<p>Next we define the AMI (Amazon Machine Image) that we want to use: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python">ami <span style="color:#f92672">=</span> aws<span style="color:#f92672">.</span>get_ami(most_recent<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;true&#34;</span>,
                  owners <span style="color:#f92672">=</span> [
                    <span style="color:#e6db74">&#34;099720109477&#34;</span>
                  ],
                  filters <span style="color:#f92672">=</span> [
                    {
                       <span style="color:#e6db74">&#34;name&#34;</span>:<span style="color:#e6db74">&#34;name&#34;</span>,
                       <span style="color:#e6db74">&#34;values&#34;</span>: [
                          <span style="color:#e6db74">&#34;ubuntu/images/hvm-ssd/ubuntu-focal-20.04-arm64-server-*&#34;</span>
                       ]
                    }
                  ])
</code></pre></div><p>Like in the Terraform code, we use a filter to find the correct image. We also specify the owner (the owner in question being Canonical) and to use the <code>most_recent</code> image.</p>
<p>For those familiar with Python, you&rsquo;ll notice that the <code>filters</code>, <code>owners</code> and <code>most_recent</code> fields are arguments to a function.</p>
<p>Next, we will define our SSH key pair: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python">homedir <span style="color:#f92672">=</span> str(Path<span style="color:#f92672">.</span>home())
sshkey_path <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;{}/.ssh/id_rsa.pub&#34;</span><span style="color:#f92672">.</span>format(
                  homedir
              )
sshkey_file <span style="color:#f92672">=</span> open(sshkey_path, <span style="color:#e6db74">&#39;r&#39;</span>)


sshkey <span style="color:#f92672">=</span> aws<span style="color:#f92672">.</span>ec2<span style="color:#f92672">.</span>KeyPair(common_name,
  public_key <span style="color:#f92672">=</span> sshkey_file<span style="color:#f92672">.</span>read()<span style="color:#f92672">.</span>strip(<span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>)
)
</code></pre></div><p>Here, we use a couple of native Python functions. We discover the current home directory of the user, reading the contents of their SSH key file and strip new lines from the file. We use the <code>aws.ec2.KeyPair</code> function to define our SSH key, providing the <code>common_name</code> variable for naming this resource.</p>
<p>Now we define a security group: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python">  group <span style="color:#f92672">=</span> aws<span style="color:#f92672">.</span>ec2<span style="color:#f92672">.</span>SecurityGroup(common_name,
      description<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Yetiops Access&#34;</span>,
      ingress <span style="color:#f92672">=</span> [
        {
          <span style="color:#e6db74">&#39;protocol&#39;</span>: <span style="color:#e6db74">&#39;tcp&#39;</span>,
          <span style="color:#e6db74">&#39;from_port&#39;</span>: <span style="color:#ae81ff">22</span>,
          <span style="color:#e6db74">&#39;to_port&#39;</span>: <span style="color:#ae81ff">22</span>,
          <span style="color:#e6db74">&#39;cidr_blocks&#39;</span>: [
            myIpCidr
          ]
        },
        {
          <span style="color:#e6db74">&#39;protocol&#39;</span>: <span style="color:#e6db74">&#39;tcp&#39;</span>,
          <span style="color:#e6db74">&#39;from_port&#39;</span>: <span style="color:#ae81ff">9100</span>,
          <span style="color:#e6db74">&#39;to_port&#39;</span>: <span style="color:#ae81ff">9100</span>,
          <span style="color:#e6db74">&#39;cidr_blocks&#39;</span>: [
            myIpCidr
          ]
        }
      ],
      egress <span style="color:#f92672">=</span> [
        {
          <span style="color:#e6db74">&#39;protocol&#39;</span>: <span style="color:#e6db74">&#39;-1&#39;</span>,
          <span style="color:#e6db74">&#39;from_port&#39;</span>: <span style="color:#ae81ff">0</span>,
          <span style="color:#e6db74">&#39;to_port&#39;</span>: <span style="color:#ae81ff">0</span>,
          <span style="color:#e6db74">&#39;cidr_blocks&#39;</span>: [
            <span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>
          ]
        }
      ]
    )
</code></pre></div><p>As with before, this is a Python function with a list of arguments. We define the name of the resource (<code>common_name</code>), a description, and some ingress and egress rules. Also notice that we have a variable called <code>myIpCidr</code>. Below we&rsquo;ll show using our own Python function to feed data into Pulumi: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">GetMyIP</span>():
  request <span style="color:#f92672">=</span> requests<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;https://ifconfig.co/json&#34;</span>)
  request_json <span style="color:#f92672">=</span> request<span style="color:#f92672">.</span>json()
  myIp <span style="color:#f92672">=</span> request_json[<span style="color:#e6db74">&#39;ip&#39;</span>]
  <span style="color:#66d9ef">return</span> myIp

myIpCidr <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;{}/32&#34;</span><span style="color:#f92672">.</span>format(
  GetMyIP()<span style="color:#f92672">.</span>strip(<span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>)
)
</code></pre></div><p>The above function performs an HTTP GET request to <a href="https://ifconfig.co/json">ifconfig.co</a> to retrieve our public IPv4 address. We then return this, strip it of any new lines, and append a <code>/32</code> subnet mask to it. This means that rather than specifying up front what our public IP is, we can calculate it at runtime. This also makes the code more portable as it isn&rsquo;t relying on a certain users IP address.</p>
<p>In the next section, we define our <strong>user-data</strong>. In Pulumi, this uses the <strong>cloudinit</strong> module: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python">  cloudconfig <span style="color:#f92672">=</span> cloudinit<span style="color:#f92672">.</span>get_config(
      base64_encode <span style="color:#f92672">=</span> False,
      gzip <span style="color:#f92672">=</span> False,
      parts <span style="color:#f92672">=</span> [
        cloudinit<span style="color:#f92672">.</span>GetConfigPartArgs(
          content <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;#cloudconfig</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">packages:</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">- prometheus-node-exporter&#34;</span>,
          content_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;text/cloud-config&#34;</span>,
          filename <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;init.cfg&#34;</span>
        )
      ]
    )
</code></pre></div><p>Like with the AWS modules, we use a function from the <strong>cloudinit</strong> module called <code>get_config</code> with a set of arguments. The content is on one line, but could easily reference a file (using a similar technique to what we did in our SSH Key Pair definition) or a multiline variable. If you have quite a complex <code>cloud-config</code> file, trying to fit it all onto one line is probably not the best idea anyway!</p>
<p>When you use <code>pulumi new $provider-$language --new $stack-name</code> command to start a new stack, it will create a Python virtual environment and also install the relevant <a href="https://pypi.org/project/pip/">pip</a> packages for the provider in question. If you want to then use other modules (e.g. the <strong>cloudinit</strong> module), you&rsquo;ll also need to install the packages for them too.</p>
<p>To do this, run the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ cd $PROJECT-DIR &lt;---- replace with the location of your project

$ source venv/bin/activate &lt;---- uses the virtual environment version of Python

$ pip3 install pulumi_cloudinit

$ pip3 freeze &gt; requirements.txt
</code></pre></div><p>The last step ensures that your <code>requirements.txt</code> file is up to date, meaning that if you run this project on a different machine, or somebody else needs to use it, they can just run <code>pip3 install -r requirements.txt</code> to get the relevant packages on their system.</p>
<p>Finally, we define our instance: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python">size <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;t4g.micro&#34;</span>

instance <span style="color:#f92672">=</span> aws<span style="color:#f92672">.</span>ec2<span style="color:#f92672">.</span>Instance(common_name,
  instance_type <span style="color:#f92672">=</span> size,
  vpc_security_group_ids <span style="color:#f92672">=</span> [
    group<span style="color:#f92672">.</span>id
  ],
  ami <span style="color:#f92672">=</span> ami<span style="color:#f92672">.</span>id,
  tags <span style="color:#f92672">=</span> {
    <span style="color:#e6db74">&#34;Name&#34;</span>: common_name,
    <span style="color:#e6db74">&#34;prometheus&#34;</span>: <span style="color:#e6db74">&#34;true&#34;</span>,
    <span style="color:#e6db74">&#34;node_exporter&#34;</span>: <span style="color:#e6db74">&#34;true&#34;</span>
  },
  key_name <span style="color:#f92672">=</span> sshkey<span style="color:#f92672">.</span>key_name,
  user_data <span style="color:#f92672">=</span> cloudconfig<span style="color:#f92672">.</span>rendered
)

pulumi<span style="color:#f92672">.</span>export(<span style="color:#e6db74">&#39;publicIp&#39;</span>, instance<span style="color:#f92672">.</span>public_ip)
pulumi<span style="color:#f92672">.</span>export(<span style="color:#e6db74">&#39;publicHostName&#39;</span>, instance<span style="color:#f92672">.</span>public_dns) 
</code></pre></div><p>Most of the fields refer to the other functions we defined, referencing their <a href="https://www.pulumi.com/docs/intro/concepts/inputs-outputs/">Outputs</a>. For example, rather than typing in the name of the SSH key pair directly, we use <code>sshkey.key_name</code>. Similarly, we reference the user data with <code>cloudconfig.rendered</code>.</p>
<p>Finally, we export some variables that are displayed when we run <code>pulumi up</code>. When the build tasks are complete, it displays the public IP and DNS hostname of the instance. These are like <a href="https://www.terraform.io/docs/language/values/outputs.html">Terraform Outputs</a>.</p>
<h4 id="running-pulumi">Running Pulumi</h4>
<p>Now we can run <code>pulumi up</code> and see what happens: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">Previewing update <span style="color:#f92672">(</span>basic-ec2-py<span style="color:#f92672">)</span>

View Live: https://app.pulumi.com/yetiops/basic-ec2-py/basic-ec2-py/previews/c2a0203c-7ddc-416b-9f4b-365a73fb74fd

     Type                      Name                       Plan       Info
 +   pulumi:pulumi:Stack       basic-ec2-py-basic-ec2-py  create     <span style="color:#ae81ff">1</span> warning
 +   ├─ aws:ec2:KeyPair        yetiops-prom               create
 +   ├─ aws:ec2:SecurityGroup  yetiops-prom               create
 +   └─ aws:ec2:Instance       yetiops-prom               create

Diagnostics:
  pulumi:pulumi:Stack <span style="color:#f92672">(</span>basic-ec2-py-basic-ec2-py<span style="color:#f92672">)</span>:
    warning: get_ami is deprecated: aws.getAmi has been deprecated in favor of aws.ec2.getAmi


Do you want to perform this update? yes
Updating <span style="color:#f92672">(</span>basic-ec2-py<span style="color:#f92672">)</span>

View Live: https://app.pulumi.com/yetiops/basic-ec2-py/basic-ec2-py/updates/5

     Type                      Name                       Status      Info
 +   pulumi:pulumi:Stack       basic-ec2-py-basic-ec2-py  created     <span style="color:#ae81ff">1</span> warning
 +   ├─ aws:ec2:KeyPair        yetiops-prom               created
 +   ├─ aws:ec2:SecurityGroup  yetiops-prom               created
 +   └─ aws:ec2:Instance       yetiops-prom               created

Diagnostics:
  pulumi:pulumi:Stack <span style="color:#f92672">(</span>basic-ec2-py-basic-ec2-py<span style="color:#f92672">)</span>:
    warning: get_ami is deprecated: aws.getAmi has been deprecated in favor of aws.ec2.getAmi

Outputs:
    publicHostName: <span style="color:#e6db74">&#34;ec2-18-134-179-124.eu-west-2.compute.amazonaws.com&#34;</span>
    publicIp      : <span style="color:#e6db74">&#34;18.134.179.124&#34;</span>

Resources:
    + <span style="color:#ae81ff">4</span> created

Duration: 23s

$ ssh -i ~/.ssh/id_rsa ubuntu@18.134.179.124

Welcome to Ubuntu 20.04.2 LTS <span style="color:#f92672">(</span>GNU/Linux 5.4.0-1041-aws aarch64<span style="color:#f92672">)</span>

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Mon Apr  <span style="color:#ae81ff">5</span> 09:34:36 UTC <span style="color:#ae81ff">2021</span>

  System load:  0.23              Processes:             <span style="color:#ae81ff">155</span>
  Usage of /:   17.3% of 7.59GB   Users logged in:       <span style="color:#ae81ff">0</span>
  Memory usage: 28%               IPv4 address <span style="color:#66d9ef">for</span> ens5: 172.31.35.165
  Swap usage:   0%

<span style="color:#ae81ff">0</span> updates can be installed immediately.
<span style="color:#ae81ff">0</span> of these updates are security updates.


The list of available updates is more than a week old.
To check <span style="color:#66d9ef">for</span> new updates run: sudo apt update


The programs included with the Ubuntu system are free software;
the exact distribution terms <span style="color:#66d9ef">for</span> each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

To run a command as administrator <span style="color:#f92672">(</span>user <span style="color:#e6db74">&#34;root&#34;</span><span style="color:#f92672">)</span>, use <span style="color:#e6db74">&#34;sudo &lt;command&gt;&#34;</span>.
See <span style="color:#e6db74">&#34;man sudo_root&#34;</span> <span style="color:#66d9ef">for</span> details.

$ ss -tlunp
Netid             State              Recv-Q             Send-Q                              Local Address:Port                         Peer Address:Port             Process
udp               UNCONN             <span style="color:#ae81ff">0</span>                  <span style="color:#ae81ff">0</span>                                   127.0.0.53%lo:53                                0.0.0.0:*
udp               UNCONN             <span style="color:#ae81ff">0</span>                  <span style="color:#ae81ff">0</span>                              172.31.35.165%ens5:68                                0.0.0.0:*
tcp               LISTEN             <span style="color:#ae81ff">0</span>                  <span style="color:#ae81ff">4096</span>                                127.0.0.53%lo:53                                0.0.0.0:*
tcp               LISTEN             <span style="color:#ae81ff">0</span>                  <span style="color:#ae81ff">128</span>                                       0.0.0.0:22                                0.0.0.0:*
tcp               LISTEN             <span style="color:#ae81ff">0</span>                  <span style="color:#ae81ff">4096</span>                                            *:9100                                    *:*
tcp               LISTEN             <span style="color:#ae81ff">0</span>                  <span style="color:#ae81ff">128</span>                                          <span style="color:#f92672">[</span>::<span style="color:#f92672">]</span>:22                                   <span style="color:#f92672">[</span>::<span style="color:#f92672">]</span>:*

$ curl localhost:9100/metrics | grep -i aarch64
node_uname_info<span style="color:#f92672">{</span>domainname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(none)&#34;</span>,machine<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;aarch64&#34;</span>,nodename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;ip-172-31-35-165&#34;</span>,release<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;5.4.0-1041-aws&#34;</span>,sysname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Linux&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;#43-Ubuntu SMP Fri Mar 19 22:06:27 UTC 2021&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>
</code></pre></div><p>Everything looks good, we can login to the instance, and the <code>node_exporter</code> is installed.</p>
<h4 id="all-the-code">All the code</h4>
<p>The full Python file, including modules we imported, is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#f92672">import</span> pulumi
<span style="color:#f92672">import</span> requests
<span style="color:#f92672">import</span> pulumi_aws <span style="color:#f92672">as</span> aws
<span style="color:#f92672">import</span> pulumi_cloudinit <span style="color:#f92672">as</span> cloudinit
<span style="color:#f92672">from</span> pathlib <span style="color:#f92672">import</span> Path

<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">GetMyIP</span>():
  request <span style="color:#f92672">=</span> requests<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;https://ifconfig.co/json&#34;</span>)
  request_json <span style="color:#f92672">=</span> request<span style="color:#f92672">.</span>json()
  myIp <span style="color:#f92672">=</span> request_json[<span style="color:#e6db74">&#39;ip&#39;</span>]
  <span style="color:#66d9ef">return</span> myIp

config <span style="color:#f92672">=</span> pulumi<span style="color:#f92672">.</span>Config()
common_name <span style="color:#f92672">=</span> config<span style="color:#f92672">.</span>require(<span style="color:#e6db74">&#34;common_name&#34;</span>)

size <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;t4g.micro&#34;</span>

ami <span style="color:#f92672">=</span> aws<span style="color:#f92672">.</span>get_ami(most_recent<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;true&#34;</span>,
                  owners <span style="color:#f92672">=</span> [
                    <span style="color:#e6db74">&#34;099720109477&#34;</span>
                  ],
                  filters <span style="color:#f92672">=</span> [
                    {
                       <span style="color:#e6db74">&#34;name&#34;</span>:<span style="color:#e6db74">&#34;name&#34;</span>,
                       <span style="color:#e6db74">&#34;values&#34;</span>: [
                          <span style="color:#e6db74">&#34;ubuntu/images/hvm-ssd/ubuntu-focal-20.04-arm64-server-*&#34;</span>
                       ]
                    }
                  ])
homedir <span style="color:#f92672">=</span> str(Path<span style="color:#f92672">.</span>home())
sshkey_path <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;{}/.ssh/id_rsa.pub&#34;</span><span style="color:#f92672">.</span>format(
                  homedir
              )
sshkey_file <span style="color:#f92672">=</span> open(sshkey_path, <span style="color:#e6db74">&#39;r&#39;</span>)


sshkey <span style="color:#f92672">=</span> aws<span style="color:#f92672">.</span>ec2<span style="color:#f92672">.</span>KeyPair(common_name,
  public_key <span style="color:#f92672">=</span> sshkey_file<span style="color:#f92672">.</span>read()<span style="color:#f92672">.</span>strip(<span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>)
)

myIpCidr <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;{}/32&#34;</span><span style="color:#f92672">.</span>format(
  GetMyIP()<span style="color:#f92672">.</span>strip(<span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>)
)

group <span style="color:#f92672">=</span> aws<span style="color:#f92672">.</span>ec2<span style="color:#f92672">.</span>SecurityGroup(common_name,
    description<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Yetiops Access&#34;</span>,
    ingress <span style="color:#f92672">=</span> [
      {
        <span style="color:#e6db74">&#39;protocol&#39;</span>: <span style="color:#e6db74">&#39;tcp&#39;</span>,
        <span style="color:#e6db74">&#39;from_port&#39;</span>: <span style="color:#ae81ff">22</span>,
        <span style="color:#e6db74">&#39;to_port&#39;</span>: <span style="color:#ae81ff">22</span>,
        <span style="color:#e6db74">&#39;cidr_blocks&#39;</span>: [
          myIpCidr
        ]
      },
      {
        <span style="color:#e6db74">&#39;protocol&#39;</span>: <span style="color:#e6db74">&#39;tcp&#39;</span>,
        <span style="color:#e6db74">&#39;from_port&#39;</span>: <span style="color:#ae81ff">9100</span>,
        <span style="color:#e6db74">&#39;to_port&#39;</span>: <span style="color:#ae81ff">9100</span>,
        <span style="color:#e6db74">&#39;cidr_blocks&#39;</span>: [
          myIpCidr
        ]
      }
    ],
    egress <span style="color:#f92672">=</span> [
      {
        <span style="color:#e6db74">&#39;protocol&#39;</span>: <span style="color:#e6db74">&#39;-1&#39;</span>,
        <span style="color:#e6db74">&#39;from_port&#39;</span>: <span style="color:#ae81ff">0</span>,
        <span style="color:#e6db74">&#39;to_port&#39;</span>: <span style="color:#ae81ff">0</span>,
        <span style="color:#e6db74">&#39;cidr_blocks&#39;</span>: [
          <span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>
        ]
      }
    ]
  )

cloudconfig <span style="color:#f92672">=</span> cloudinit<span style="color:#f92672">.</span>get_config(
    base64_encode <span style="color:#f92672">=</span> False,
    gzip <span style="color:#f92672">=</span> False,
    parts <span style="color:#f92672">=</span> [
      cloudinit<span style="color:#f92672">.</span>GetConfigPartArgs(
        content <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;#cloudconfig</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">packages:</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">- prometheus-node-exporter&#34;</span>,
        content_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;text/cloud-config&#34;</span>,
        filename <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;init.cfg&#34;</span>
      )
    ]
  )


instance <span style="color:#f92672">=</span> aws<span style="color:#f92672">.</span>ec2<span style="color:#f92672">.</span>Instance(common_name,
  instance_type <span style="color:#f92672">=</span> size,
  vpc_security_group_ids <span style="color:#f92672">=</span> [
    group<span style="color:#f92672">.</span>id
  ],
  ami <span style="color:#f92672">=</span> ami<span style="color:#f92672">.</span>id,
  tags <span style="color:#f92672">=</span> {
    <span style="color:#e6db74">&#34;Name&#34;</span>: common_name,
    <span style="color:#e6db74">&#34;prometheus&#34;</span>: <span style="color:#e6db74">&#34;true&#34;</span>,
    <span style="color:#e6db74">&#34;node_exporter&#34;</span>: <span style="color:#e6db74">&#34;true&#34;</span>
  },
  key_name <span style="color:#f92672">=</span> sshkey<span style="color:#f92672">.</span>key_name,
  user_data <span style="color:#f92672">=</span> cloudconfig<span style="color:#f92672">.</span>rendered
)

pulumi<span style="color:#f92672">.</span>export(<span style="color:#e6db74">&#39;publicIp&#39;</span>, instance<span style="color:#f92672">.</span>public_ip)
pulumi<span style="color:#f92672">.</span>export(<span style="color:#e6db74">&#39;publicHostName&#39;</span>, instance<span style="color:#f92672">.</span>public_dns)
</code></pre></div><p>Now we can do the same, but in Go.</p>
<h3 id="pulumi-using-go">Pulumi using Go</h3>
<p>Pulumi hasn&rsquo;t supported Go for as long as it has Typescript, Javascript and Python, so you may find the odd example missing in their documentation (<a href="https://www.pulumi.com/docs/reference/pkg/digitalocean/firewall/">here</a> is a good example). However once you have worked with a few of the providers, you can usually infer what you need to do based upon the fields/inputs mentioned in the provider documentation.</p>
<h4 id="why-go">Why Go?</h4>
<p>Why would you want to use Go over Python? There are a couple of reasons.</p>
<p>First, because Go uses <a href="https://en.wikipedia.org/wiki/Type_system#Static_type_checking">static typing</a> for variables, you can&rsquo;t use something like an integer in place of a string without converting it to a string first. This makes you more aware of what values you are passing between functions, and can help avoid cases of a variable being interpreted incorrectly and creating something wildly different from what you intended.</p>
<p>Also, you have to be explicit in how you handle errors in Go. This in turn makes you think about the functions you are using, and what you expect them to return.</p>
<p>Mostly though, it is down to personal preference. I prefer writing in Go, due to the reasons already mentioned. While not relevant to Pulumi, I also like that Go can create a single binary of your application that is easily distributable. With Python you usually ship a <code>requirements.txt</code> file that a user/operator must supply to <code>pip</code> to install all the required modules before the application can run.</p>
<p>For me, if I prefer writing other applications in Go, then also writing my infrastructure code in Go makes a lot of sense too.</p>
<h4 id="the-code-1">The code</h4>
<p>As before, we need to run <code>pulumi new aws-go --name basic-ec2-go</code> to instantiate our stack and project. We also run <code>pulumi config set common_name yetiops-prom</code> to set our <code>common_name</code> variable.</p>
<p>After this, we can start building our infrastructure. We first define the AMI: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">		<span style="color:#a6e22e">mostRecent</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">true</span>
		<span style="color:#a6e22e">ami</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">aws</span>.<span style="color:#a6e22e">GetAmi</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">aws</span>.<span style="color:#a6e22e">GetAmiArgs</span>{
			<span style="color:#a6e22e">Filters</span>: []<span style="color:#a6e22e">aws</span>.<span style="color:#a6e22e">GetAmiFilter</span>{
				{
					<span style="color:#a6e22e">Name</span>:   <span style="color:#e6db74">&#34;name&#34;</span>,
					<span style="color:#a6e22e">Values</span>: []<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">&#34;ubuntu/images/hvm-ssd/ubuntu-focal-20.04-arm64-server-*&#34;</span>},
				},
			},
			<span style="color:#a6e22e">Owners</span>:     []<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">&#34;099720109477&#34;</span>},
			<span style="color:#a6e22e">MostRecent</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">mostRecent</span>,
		})

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}
</code></pre></div><p>You may notice that we define some variables (like <code>mostRecent</code>) and then refer to them using the <code>&amp;</code> symbol (ampersand) as a prefix. This is known as a <strong>pointer</strong>. In Go, a pointer is the variables location in memory. In Python, if you make a change to a variable later in the code, you are usually working on a copy of the variable, not the original. A pointer always works on the originally defined variable itself. If a change is made to the variable later in the code, it will also change everywhere it is used.</p>
<p>In all Pulumi functions in Go, we supply the <strong>context</strong> (the <code>ctx</code> variable). Context provides information to the function about whether Pulumi has completed/cancelled the operation, and can also pass in values from Pulumi SDK/CLI/API when required.</p>
<p>Like in the Python version, we are defining arguments to a function, but in this case it is more explicit in how we do so. We also return an error if one is found in retrieving this AMI (either through it not existing, or an issue in our code), which will stop the infrastructure from being deployed.</p>
<p>Next, we will define our SSH key pair: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">        <span style="color:#a6e22e">user</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">Current</span>()

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">sshkey_path</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%v/.ssh/id_rsa.pub&#34;</span>, <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">HomeDir</span>)

		<span style="color:#a6e22e">sshkey_file</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">sshkey_path</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">sshkey_contents</span> <span style="color:#f92672">:=</span> string(<span style="color:#a6e22e">sshkey_file</span>)

		<span style="color:#a6e22e">sshkey</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">NewKeyPair</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">KeyPairArgs</span>{
			<span style="color:#a6e22e">PublicKey</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">sshkey_contents</span>),
		})

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}
</code></pre></div><p>Like in the Python example, we reference the user&rsquo;s home directory, opening the SSH key file, read the contents, and then pass it to the SSH key pair function. We also see our first occurrence of the <code>commonName</code> variable.</p>
<p>Notice the usage of <code>pulumi.String($STRING)</code>. This is an example of where types matter in Go. Rather than just expecting Go to handle whether this is a string or not, we must call the <code>pulumi.String</code> function to ensure that the variable being passed in is a string. This is because the <code>PublicKey</code> field requires a string, and would error immediately if you supplied an integer instead.</p>
<p>Like in the Python example, we must create the reference to our configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">		<span style="color:#a6e22e">conf</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;&#34;</span>)
		<span style="color:#a6e22e">commonName</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">conf</span>.<span style="color:#a6e22e">Require</span>(<span style="color:#e6db74">&#34;common_name&#34;</span>)
</code></pre></div><p>Next, we create our security group: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">		<span style="color:#a6e22e">group</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">NewSecurityGroup</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">SecurityGroupArgs</span>{
			<span style="color:#a6e22e">Ingress</span>: <span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">SecurityGroupIngressArray</span>{
				<span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">SecurityGroupIngressArgs</span>{
					<span style="color:#a6e22e">Protocol</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;tcp&#34;</span>),
					<span style="color:#a6e22e">FromPort</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Int</span>(<span style="color:#ae81ff">22</span>),
					<span style="color:#a6e22e">ToPort</span>:   <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Int</span>(<span style="color:#ae81ff">22</span>),
					<span style="color:#a6e22e">CidrBlocks</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
						<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">myIpCidr</span>)},
				},
				<span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">SecurityGroupIngressArgs</span>{
					<span style="color:#a6e22e">Protocol</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;tcp&#34;</span>),
					<span style="color:#a6e22e">FromPort</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Int</span>(<span style="color:#ae81ff">9100</span>),
					<span style="color:#a6e22e">ToPort</span>:   <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Int</span>(<span style="color:#ae81ff">9100</span>),
					<span style="color:#a6e22e">CidrBlocks</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
						<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">myIpCidr</span>)},
				},
			},
			<span style="color:#a6e22e">Egress</span>: <span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">SecurityGroupEgressArray</span>{
				<span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">SecurityGroupEgressArgs</span>{
					<span style="color:#a6e22e">Protocol</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;-1&#34;</span>),
					<span style="color:#a6e22e">FromPort</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Int</span>(<span style="color:#ae81ff">0</span>),
					<span style="color:#a6e22e">ToPort</span>:   <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Int</span>(<span style="color:#ae81ff">0</span>),
					<span style="color:#a6e22e">CidrBlocks</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
						<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>)},
				},
			},
		})

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}
</code></pre></div><p>We can see here that we are using <code>pulumi.Int</code> and <code>pulumiStringArray</code>. This ensures we are passing the right type variable types to each field. Again, we have a <code>myIpCidr</code> variable. We create a function that retrieves our IP for us, so we can use it in this security group: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">getMyIp</span>() (<span style="color:#66d9ef">string</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#e6db74">&#34;https://ifconfig.co&#34;</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">err</span>
	}

	<span style="color:#a6e22e">body</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadAll</span>(<span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Body</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">err</span>
	}

	<span style="color:#a6e22e">MyIp</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">TrimSuffix</span>(string(<span style="color:#a6e22e">body</span>), <span style="color:#e6db74">&#34;\n&#34;</span>)

	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">MyIp</span>, <span style="color:#66d9ef">nil</span>
}

[<span style="color:#f92672">...</span>]

		<span style="color:#a6e22e">myIp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">getMyIp</span>()
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">myIpCidr</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%v/32&#34;</span>, <span style="color:#a6e22e">myIp</span>)
</code></pre></div><p>Similar to the Python function, we retrieve the IP from <code>https://ifconfig.co</code>. We don&rsquo;t parse the JSON this time, as the base URL returns just the IP as the body. We also return any errors, in either contacting the <strong>ifconfig</strong> site or retrieving the body of the function. We also remove any new lines from the string, and then return the IP if no errors were found.</p>
<p>Finally, we call the function, return errors directly if any were found, and then add &ldquo;/32&rdquo; to the end of the string so we can use it directly in the security group.</p>
<p>After this, we can look at the <strong>user-data</strong> creation, using the <strong>cloudinit</strong> module again: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">          <span style="color:#a6e22e">b64encEnable</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">false</span>
          <span style="color:#a6e22e">gzipEnable</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">false</span>
          <span style="color:#a6e22e">contentType</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;text/cloud-config&#34;</span>
          <span style="color:#a6e22e">fileName</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;init.cfg&#34;</span>
          <span style="color:#a6e22e">cloudconfig</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">LookupConfig</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">LookupConfigArgs</span>{
              <span style="color:#a6e22e">Base64Encode</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">b64encEnable</span>,
              <span style="color:#a6e22e">Gzip</span>:         <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">gzipEnable</span>,
              <span style="color:#a6e22e">Parts</span>: []<span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">GetConfigPart</span>{
<span style="color:#960050;background-color:#1e0010">�</span>                 <span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">GetConfigPart</span>{
                      <span style="color:#a6e22e">Content</span>: <span style="color:#e6db74">&#34;#cloud-config\n&#34;</span> <span style="color:#f92672">+</span>
                          <span style="color:#e6db74">&#34;packages:\n&#34;</span> <span style="color:#f92672">+</span>
                          <span style="color:#e6db74">&#34;- prometheus-node-exporter&#34;</span>,
                      <span style="color:#a6e22e">ContentType</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">contentType</span>,
                      <span style="color:#a6e22e">Filename</span>:    <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">fileName</span>,
                  },
              },
          }, <span style="color:#66d9ef">nil</span>)

          <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
              <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
          }
</code></pre></div><p>As before, we refer to some variables with a <strong>pointer</strong>. Similar to the Python definition, we are creating the <code>cloud-config</code> file that installs the Prometheus <code>node_exporter</code>. Finally, we check to see if there was an error in this function, and return it if there is.</p>
<p>Again, like in the Python section, we need to install the Go module for <code>cloudinit</code>. This can be done like so: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ go get github.com/pulumi/pulumi-cloudinit/sdk/go/cloudinit
</code></pre></div><p>This will automatically update the <code>go.mod</code> and <code>go.sum</code> file with the module too.</p>
<p>Last but not least, we define our instance: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">          <span style="color:#a6e22e">srv</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">NewInstance</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">InstanceArgs</span>{
              <span style="color:#a6e22e">Tags</span>:                <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringMap</span>{<span style="color:#e6db74">&#34;Name&#34;</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">commonName</span>)},
              <span style="color:#a6e22e">InstanceType</span>:        <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;t4g.micro&#34;</span>),
              <span style="color:#a6e22e">VpcSecurityGroupIds</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{<span style="color:#a6e22e">group</span>.<span style="color:#a6e22e">ID</span>()},
              <span style="color:#a6e22e">Ami</span>:                 <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">ami</span>.<span style="color:#a6e22e">Id</span>),
              <span style="color:#a6e22e">KeyName</span>:             <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringOutput</span>(<span style="color:#a6e22e">sshkey</span>.<span style="color:#a6e22e">KeyName</span>),
              <span style="color:#a6e22e">UserData</span>:            <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">cloudconfig</span>.<span style="color:#a6e22e">Rendered</span>),
          })

          <span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Export</span>(<span style="color:#e6db74">&#34;publicIp&#34;</span>, <span style="color:#a6e22e">srv</span>.<span style="color:#a6e22e">PublicIp</span>)
          <span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Export</span>(<span style="color:#e6db74">&#34;publicHostName&#34;</span>, <span style="color:#a6e22e">srv</span>.<span style="color:#a6e22e">PublicDns</span>)

          <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</code></pre></div><p>Most of the input variables are strings, including reference to the IDs, our <code>commonName</code> and more. However we also need to use the <code>pulumi.StringOutput</code> function for the <code>sshkey.KeyName</code> field. The other fields will return as strings, or at least types that are compatible with a <code>StringMap</code> or a <code>StringArray</code>. However the <code>KeyName</code> returned from the <code>sshkey</code> function is a <code>StringOutput</code>. If we try and treat it as a normal string, this is what you will see: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ pulumi up

View Live: https://app.pulumi.com/yetiops/basic-ec2/basic-ec2/previews/2781a4ef-b9c3-461f-934d-6552550b0f5f

     Type                 Name                 Plan     Info
     pulumi:pulumi:Stack  basic-ec2-basic-ec2           <span style="color:#ae81ff">1</span> error; <span style="color:#ae81ff">2</span> messages

Diagnostics:
  pulumi:pulumi:Stack <span style="color:#f92672">(</span>basic-ec2-basic-ec2<span style="color:#f92672">)</span>:
    <span style="color:#75715e"># basic-ec2</span>
    ./main.go:143:38: cannot convert sshkey.KeyName <span style="color:#f92672">(</span>type pulumi.StringOutput<span style="color:#f92672">)</span> to type pulumi.String

    error: an unhandled error occurred: program exited with non-zero exit code: <span style="color:#ae81ff">2</span>
</code></pre></div><p>As you can see, if we use a <code>pulumi.String</code> function, it returns the wrong type. This is both the advantage of using Go, and the disadvantage if you aren&rsquo;t used to languages with static types. If you use Python, you don&rsquo;t tend to think about this kind of issue (and was one of my main issues coming from Python when I first started with Go). It does add an element of safety to what you are building though. At minimum, it at least makes you look to see if you&rsquo;ve referenced the wrong resource!</p>
<p>Otherwise, this is very similar to how we defined the instance in Python. We also have our outputs too, so we are given the Public IP and Hostname of the instance in our output.</p>
<p>Finally, we end with <code>return nil</code>, which says that if we reached this stage with no problems, there was no error.</p>
<h4 id="running-pulumi-1">Running Pulumi</h4>
<p>Now that we have everything defined, we can run <code>pulumi up</code> and see what happens: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ pulumi up

Previewing update <span style="color:#f92672">(</span>basic-ec2<span style="color:#f92672">)</span>

View Live: https://app.pulumi.com/yetiops/basic-ec2/basic-ec2/previews/6b7f7cfa-58c4-45da-bcce-54b7f046a280

     Type                      Name                 Plan
 +   pulumi:pulumi:Stack       basic-ec2-basic-ec2  create
 +   ├─ aws:ec2:KeyPair        yetiops-prom         create
 +   ├─ aws:ec2:SecurityGroup  yetiops-prom         create
 +   └─ aws:ec2:Instance       yetiops-prom         create

Resources:
    + <span style="color:#ae81ff">4</span> to create

Do you want to perform this update? yes
Updating <span style="color:#f92672">(</span>basic-ec2<span style="color:#f92672">)</span>

View Live: https://app.pulumi.com/yetiops/basic-ec2/basic-ec2/updates/14

     Type                      Name                 Status
 +   pulumi:pulumi:Stack       basic-ec2-basic-ec2  created
 +   ├─ aws:ec2:KeyPair        yetiops-prom         created
 +   ├─ aws:ec2:SecurityGroup  yetiops-prom         created
 +   └─ aws:ec2:Instance       yetiops-prom         created

Outputs:
    publicHostName: <span style="color:#e6db74">&#34;ec2-18-132-12-195.eu-west-2.compute.amazonaws.com&#34;</span>
    publicIp      : <span style="color:#e6db74">&#34;18.132.12.195&#34;</span>

Resources:
    + <span style="color:#ae81ff">4</span> created

Duration: 32s

$ ssh -i ~/.ssh/id_rsa ubuntu@18.132.12.195
Welcome to Ubuntu 20.04.2 LTS <span style="color:#f92672">(</span>GNU/Linux 5.4.0-1041-aws aarch64<span style="color:#f92672">)</span>

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Mon Apr  <span style="color:#ae81ff">5</span> 17:48:24 UTC <span style="color:#ae81ff">2021</span>

  System load:  0.6               Processes:             <span style="color:#ae81ff">157</span>
  Usage of /:   17.1% of 7.59GB   Users logged in:       <span style="color:#ae81ff">0</span>
  Memory usage: 29%               IPv4 address <span style="color:#66d9ef">for</span> ens5: 172.31.43.120
  Swap usage:   0%

<span style="color:#ae81ff">0</span> updates can be installed immediately.
<span style="color:#ae81ff">0</span> of these updates are security updates.


The list of available updates is more than a week old.
To check <span style="color:#66d9ef">for</span> new updates run: sudo apt update


The programs included with the Ubuntu system are free software;
the exact distribution terms <span style="color:#66d9ef">for</span> each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

To run a command as administrator <span style="color:#f92672">(</span>user <span style="color:#e6db74">&#34;root&#34;</span><span style="color:#f92672">)</span>, use <span style="color:#e6db74">&#34;sudo &lt;command&gt;&#34;</span>.
See <span style="color:#e6db74">&#34;man sudo_root&#34;</span> <span style="color:#66d9ef">for</span> details.

$ ss -tlunp
Netid                   State                    Recv-Q                   Send-Q                                          Local Address:Port                                     Peer Address:Port                  Process
udp                     UNCONN                   <span style="color:#ae81ff">0</span>                        <span style="color:#ae81ff">0</span>                                          172.31.43.120%ens5:68                                            0.0.0.0:*
udp                     UNCONN                   <span style="color:#ae81ff">0</span>                        <span style="color:#ae81ff">0</span>                                               127.0.0.53%lo:53                                            0.0.0.0:*
tcp                     LISTEN                   <span style="color:#ae81ff">0</span>                        <span style="color:#ae81ff">4096</span>                                            127.0.0.53%lo:53                                            0.0.0.0:*
tcp                     LISTEN                   <span style="color:#ae81ff">0</span>                        <span style="color:#ae81ff">128</span>                                                   0.0.0.0:22                                            0.0.0.0:*
tcp                     LISTEN                   <span style="color:#ae81ff">0</span>                        <span style="color:#ae81ff">4096</span>                                                        *:9100                                                *:*
tcp                     LISTEN                   <span style="color:#ae81ff">0</span>                        <span style="color:#ae81ff">128</span>                                                      <span style="color:#f92672">[</span>::<span style="color:#f92672">]</span>:22                                               <span style="color:#f92672">[</span>::<span style="color:#f92672">]</span>:*

$ curl localhost:9100/metrics | grep -i aarch
node_uname_info<span style="color:#f92672">{</span>domainname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(none)&#34;</span>,machine<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;aarch64&#34;</span>,nodename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;ip-172-31-43-120&#34;</span>,release<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;5.4.0-1041-aws&#34;</span>,sysname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Linux&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;#43-Ubuntu SMP Fri Mar 19 22:06:27 UTC 2021&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>
</code></pre></div><p>Success.</p>
<h4 id="all-the-code-1">All the code</h4>
<p>The full <code>main.go</code> is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go"><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>

<span style="color:#f92672">import</span> (
	<span style="color:#e6db74">&#34;fmt&#34;</span>
	<span style="color:#e6db74">&#34;io/ioutil&#34;</span>
	<span style="color:#e6db74">&#34;net/http&#34;</span>
	<span style="color:#e6db74">&#34;os/user&#34;</span>
	<span style="color:#e6db74">&#34;strings&#34;</span>

	<span style="color:#e6db74">&#34;github.com/pulumi/pulumi-aws/sdk/v3/go/aws&#34;</span>
	<span style="color:#e6db74">&#34;github.com/pulumi/pulumi-aws/sdk/v3/go/aws/ec2&#34;</span>
	<span style="color:#e6db74">&#34;github.com/pulumi/pulumi-cloudinit/sdk/go/cloudinit&#34;</span>
	<span style="color:#e6db74">&#34;github.com/pulumi/pulumi/sdk/v2/go/pulumi&#34;</span>
	<span style="color:#e6db74">&#34;github.com/pulumi/pulumi/sdk/v2/go/pulumi/config&#34;</span>
)

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">getMyIp</span>() (<span style="color:#66d9ef">string</span>, <span style="color:#66d9ef">error</span>) {
	<span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#e6db74">&#34;https://ifconfig.co&#34;</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">err</span>
	}

	<span style="color:#a6e22e">body</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadAll</span>(<span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Body</span>)
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
		<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">err</span>
	}

	<span style="color:#a6e22e">MyIp</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">TrimSuffix</span>(string(<span style="color:#a6e22e">body</span>), <span style="color:#e6db74">&#34;\n&#34;</span>)

	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">MyIp</span>, <span style="color:#66d9ef">nil</span>
}

<span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
	<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Context</span>) <span style="color:#66d9ef">error</span> {
		<span style="color:#a6e22e">conf</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;&#34;</span>)
		<span style="color:#a6e22e">commonName</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">conf</span>.<span style="color:#a6e22e">Require</span>(<span style="color:#e6db74">&#34;common_name&#34;</span>)

		<span style="color:#a6e22e">b64encEnable</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">false</span>
		<span style="color:#a6e22e">gzipEnable</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">false</span>
		<span style="color:#a6e22e">contentType</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;text/cloud-config&#34;</span>
		<span style="color:#a6e22e">fileName</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;init.cfg&#34;</span>
		<span style="color:#a6e22e">cloudconfig</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">LookupConfig</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">LookupConfigArgs</span>{
			<span style="color:#a6e22e">Base64Encode</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">b64encEnable</span>,
			<span style="color:#a6e22e">Gzip</span>:         <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">gzipEnable</span>,
			<span style="color:#a6e22e">Parts</span>: []<span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">GetConfigPart</span>{
				<span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">GetConfigPart</span>{
					<span style="color:#a6e22e">Content</span>: <span style="color:#e6db74">&#34;#cloud-config\n&#34;</span> <span style="color:#f92672">+</span>
						<span style="color:#e6db74">&#34;packages:\n&#34;</span> <span style="color:#f92672">+</span>
						<span style="color:#e6db74">&#34;- prometheus-node-exporter&#34;</span>,
					<span style="color:#a6e22e">ContentType</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">contentType</span>,
					<span style="color:#a6e22e">Filename</span>:    <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">fileName</span>,
				},
			},
		}, <span style="color:#66d9ef">nil</span>)

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">user</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">Current</span>()

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">sshkey_path</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%v/.ssh/id_rsa.pub&#34;</span>, <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">HomeDir</span>)

		<span style="color:#a6e22e">sshkey_file</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">sshkey_path</span>)
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">sshkey_contents</span> <span style="color:#f92672">:=</span> string(<span style="color:#a6e22e">sshkey_file</span>)

		<span style="color:#a6e22e">sshkey</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">NewKeyPair</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">KeyPairArgs</span>{
			<span style="color:#a6e22e">PublicKey</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">sshkey_contents</span>),
		})

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">myIp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">getMyIp</span>()
		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">myIpCidr</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%v/32&#34;</span>, <span style="color:#a6e22e">myIp</span>)

		<span style="color:#a6e22e">group</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">NewSecurityGroup</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">SecurityGroupArgs</span>{
			<span style="color:#a6e22e">Ingress</span>: <span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">SecurityGroupIngressArray</span>{
				<span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">SecurityGroupIngressArgs</span>{
					<span style="color:#a6e22e">Protocol</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;tcp&#34;</span>),
					<span style="color:#a6e22e">FromPort</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Int</span>(<span style="color:#ae81ff">22</span>),
					<span style="color:#a6e22e">ToPort</span>:   <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Int</span>(<span style="color:#ae81ff">22</span>),
					<span style="color:#a6e22e">CidrBlocks</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
						<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">myIpCidr</span>)},
				},
				<span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">SecurityGroupIngressArgs</span>{
					<span style="color:#a6e22e">Protocol</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;tcp&#34;</span>),
					<span style="color:#a6e22e">FromPort</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Int</span>(<span style="color:#ae81ff">9100</span>),
					<span style="color:#a6e22e">ToPort</span>:   <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Int</span>(<span style="color:#ae81ff">9100</span>),
					<span style="color:#a6e22e">CidrBlocks</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
						<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">myIpCidr</span>)},
				},
			},
			<span style="color:#a6e22e">Egress</span>: <span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">SecurityGroupEgressArray</span>{
				<span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">SecurityGroupEgressArgs</span>{
					<span style="color:#a6e22e">Protocol</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;-1&#34;</span>),
					<span style="color:#a6e22e">FromPort</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Int</span>(<span style="color:#ae81ff">0</span>),
					<span style="color:#a6e22e">ToPort</span>:   <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Int</span>(<span style="color:#ae81ff">0</span>),
					<span style="color:#a6e22e">CidrBlocks</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{
						<span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>)},
				},
			},
		})

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">mostRecent</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">true</span>
		<span style="color:#a6e22e">ami</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">aws</span>.<span style="color:#a6e22e">GetAmi</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">aws</span>.<span style="color:#a6e22e">GetAmiArgs</span>{
			<span style="color:#a6e22e">Filters</span>: []<span style="color:#a6e22e">aws</span>.<span style="color:#a6e22e">GetAmiFilter</span>{
				{
					<span style="color:#a6e22e">Name</span>:   <span style="color:#e6db74">&#34;name&#34;</span>,
					<span style="color:#a6e22e">Values</span>: []<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">&#34;ubuntu/images/hvm-ssd/ubuntu-focal-20.04-arm64-server-*&#34;</span>},
				},
			},
			<span style="color:#a6e22e">Owners</span>:     []<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">&#34;099720109477&#34;</span>},
			<span style="color:#a6e22e">MostRecent</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">mostRecent</span>,
		})

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">srv</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">NewInstance</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">InstanceArgs</span>{
			<span style="color:#a6e22e">Tags</span>:                <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringMap</span>{<span style="color:#e6db74">&#34;Name&#34;</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">commonName</span>)},
			<span style="color:#a6e22e">InstanceType</span>:        <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;t4g.micro&#34;</span>),
			<span style="color:#a6e22e">VpcSecurityGroupIds</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{<span style="color:#a6e22e">group</span>.<span style="color:#a6e22e">ID</span>()},
			<span style="color:#a6e22e">Ami</span>:                 <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">ami</span>.<span style="color:#a6e22e">Id</span>),
			<span style="color:#a6e22e">KeyName</span>:             <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringOutput</span>(<span style="color:#a6e22e">sshkey</span>.<span style="color:#a6e22e">KeyName</span>),
			<span style="color:#a6e22e">UserData</span>:            <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">cloudconfig</span>.<span style="color:#a6e22e">Rendered</span>),
		})

		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
		}

		<span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Export</span>(<span style="color:#e6db74">&#34;publicIp&#34;</span>, <span style="color:#a6e22e">srv</span>.<span style="color:#a6e22e">PublicIp</span>)
		<span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Export</span>(<span style="color:#e6db74">&#34;publicHostName&#34;</span>, <span style="color:#a6e22e">srv</span>.<span style="color:#a6e22e">PublicDns</span>)

		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>

	})
}
</code></pre></div><p>The code is definitely more verbose than the Python code, which is an artifact of Go (e.g. explicit rather than implicit error handling, defining types and context etc). It is down to your preference and which language you are most comfortable as to whether you choose Go or not for use with Pulumi.</p>
<h3 id="pulumi-using-typescript">Pulumi using Typescript</h3>
<p>Typescript was one of the first languages that Pulumi supported, and if you use their documentation, the primary language examples are shown in is usually Typescript. For those who are not aware, Typescript is a superset of Javascript, developed by Microsoft. One of the main benefits is that it adds the ability to support static typing.</p>
<p>To install and use Typescript, you can use <a href="https://www.npmjs.com/package/typescript">npm</a>. This is the package manager for NodeJS, which means you will also want to install NodeJS as well (instructions available <a href="https://nodejs.org/en/">here</a>). Once NodeJS and npm are installed, run <code>npm install -g typescript</code>, and you will be able to run <code>tsc</code> commands. More importantly for this post, you&rsquo;ll be able to define your resources using Typescript.</p>
<h4 id="why-typescript">Why Typescript?</h4>
<p>Javascript, and even Typescript, are very popular. As of 2020, Javascript and Typescript were the 1st and 4th most used languages on GitHub. While this doesn&rsquo;t cover every project in existence (e.g. those on other version control providers, internal applications), it covers enough code that these numbers mean something!</p>
<p>With that being the case, it makes sense that the most popular language in the world (Javascript) is supported by Pulumi. With Typescript also offering static typing, it adds some of the safety elements that you get with something like Go as well.</p>
<p>Why did I put it last after Python and Go given it&rsquo;s relative popularity? Because I&rsquo;ve never actually written anything in it before! Most of what I have written in this section is based upon existing examples, and changing to match what I am trying to achieve.</p>
<h4 id="the-code-2">The code</h4>
<p>Like Python and Go, we initialize our stack and project using <code>pulumi new aws-typescript --name basic-ec2-tsc</code> in a new directory. We also set our <code>common_name</code> variable using <code>pulumi config set common_name yetiops-prom</code> so that we can use it in our stack.</p>
<p>After this, we can start building our infrastructure. We first define the AMI: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-typescript" data-lang="typescript"><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">ami</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">output</span>(<span style="color:#a6e22e">aws</span>.<span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">getAmi</span>({
    <span style="color:#a6e22e">filters</span><span style="color:#f92672">:</span> [{
        <span style="color:#a6e22e">name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;name&#34;</span>,
        <span style="color:#a6e22e">values</span><span style="color:#f92672">:</span> [
            <span style="color:#e6db74">&#34;ubuntu/images/hvm-ssd/ubuntu-focal-20.04-arm64-server-*&#34;</span>
        ]
    }],
    <span style="color:#a6e22e">owners</span><span style="color:#f92672">:</span> [
        <span style="color:#e6db74">&#34;099720109477&#34;</span>
    ],
    <span style="color:#a6e22e">mostRecent</span>: <span style="color:#66d9ef">true</span>,
}));
</code></pre></div><p>This isn&rsquo;t too far removed from the Python code that defines the AMI. We define filters, the owner, the correct image, and to use the most recent AMI.</p>
<p>Now we&rsquo;ll define the reference to our configuration values: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-typescript" data-lang="typescript"><span style="color:#66d9ef">let</span> <span style="color:#a6e22e">config</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Config</span>();

<span style="color:#66d9ef">let</span> <span style="color:#a6e22e">common_name</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">require</span>(<span style="color:#e6db74">&#34;common_name&#34;</span>);
</code></pre></div><p>We can now use the <code>common_name</code> variable in the other functions.</p>
<p>Now we will define the security group: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-typescript" data-lang="typescript"><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">group</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">aws</span>.<span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">SecurityGroup</span>(<span style="color:#a6e22e">common_name</span>, {
    <span style="color:#a6e22e">ingress</span><span style="color:#f92672">:</span> [
        {
            <span style="color:#a6e22e">protocol</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;tcp&#34;</span>,
            <span style="color:#a6e22e">fromPort</span>: <span style="color:#66d9ef">22</span>,
            <span style="color:#a6e22e">toPort</span>: <span style="color:#66d9ef">22</span>,
            <span style="color:#a6e22e">cidrBlocks</span><span style="color:#f92672">:</span> [
                <span style="color:#e6db74">&#34;$MY-IP/32&#34;</span>
            ]
        },
        {
            <span style="color:#a6e22e">protocol</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;tcp&#34;</span>,
            <span style="color:#a6e22e">fromPort</span>: <span style="color:#66d9ef">9100</span>,
            <span style="color:#a6e22e">toPort</span>: <span style="color:#66d9ef">9100</span>,
            <span style="color:#a6e22e">cidrBlocks</span><span style="color:#f92672">:</span> [
                <span style="color:#e6db74">&#34;$MY-IP/32&#34;</span>
            ]
        },
    ],
    <span style="color:#a6e22e">egress</span><span style="color:#f92672">:</span> [
        {
            <span style="color:#a6e22e">protocol</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;-1&#34;</span>,
            <span style="color:#a6e22e">fromPort</span>: <span style="color:#66d9ef">0</span>,
            <span style="color:#a6e22e">toPort</span>: <span style="color:#66d9ef">0</span>,
            <span style="color:#a6e22e">cidrBlocks</span><span style="color:#f92672">:</span> [
                <span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>
            ]
        }
    ]
});
</code></pre></div><p>Unfortunately due to my lack of experience with Typescript, I wasn&rsquo;t able to replicate the function that calls to <code>https://ifconfig.co</code> so I have used my IP address directly in the rule definitions. If you already know Typescript, this shouldn&rsquo;t be much of a challenge to implement.</p>
<p>Now we can define our SSH key pair: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-typescript" data-lang="typescript"><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">homedir</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">require</span>(<span style="color:#e6db74">&#39;os&#39;</span>).<span style="color:#a6e22e">homedir</span>();

<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">sshkey_path</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">`</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">homedir</span><span style="color:#e6db74">}</span><span style="color:#e6db74">/.ssh/id_rsa.pub`</span>;

<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">sshkey_file</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">fs</span>.<span style="color:#a6e22e">readFileSync</span>(<span style="color:#a6e22e">sshkey_path</span>, <span style="color:#e6db74">&#39;utf8&#39;</span>);

<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">sshkey</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">aws</span>.<span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">KeyPair</span>(<span style="color:#a6e22e">common_name</span>, {
    <span style="color:#a6e22e">keyName</span>: <span style="color:#66d9ef">common_name</span>,
    <span style="color:#a6e22e">publicKey</span>: <span style="color:#66d9ef">sshkey_file</span>
});
</code></pre></div><p>Like the others, this references our home directory, and puts the contents of the file in as our public key contents.</p>
<p>Next, we cover the <strong>user-data</strong>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-typescript" data-lang="typescript"><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">cloudconfig</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">output</span>(<span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">getConfig</span>({
    <span style="color:#a6e22e">base64Encode</span>: <span style="color:#66d9ef">false</span>,
    <span style="color:#a6e22e">gzip</span>: <span style="color:#66d9ef">false</span>,
    <span style="color:#a6e22e">parts</span><span style="color:#f92672">:</span> [{
        <span style="color:#a6e22e">content</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;#cloudconfig\npackages:\n- prometheus-node-exporter&#34;</span>,
        <span style="color:#a6e22e">contentType</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;text/cloud-config&#34;</span>,
        <span style="color:#a6e22e">filename</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;init.cfg&#34;</span>
    }],
}, { <span style="color:#a6e22e">async</span>: <span style="color:#66d9ef">true</span> }));
</code></pre></div><p>Again, this is similar to the Python and Go resource definitions. Finally, we can now define the instance: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-typescript" data-lang="typescript"><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">size</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;t4g.micro&#34;</span>;

<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">instance</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">aws</span>.<span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">Instance</span>(<span style="color:#a6e22e">common_name</span>, {
    <span style="color:#a6e22e">instanceType</span>: <span style="color:#66d9ef">size</span>,
    <span style="color:#a6e22e">vpcSecurityGroupIds</span><span style="color:#f92672">:</span> [
        <span style="color:#a6e22e">group</span>.<span style="color:#a6e22e">id</span>
    ],
    <span style="color:#a6e22e">ami</span>: <span style="color:#66d9ef">ami.id</span>,
    <span style="color:#a6e22e">tags</span><span style="color:#f92672">:</span> {
        <span style="color:#a6e22e">Name</span>: <span style="color:#66d9ef">common_name</span>,
        <span style="color:#a6e22e">node_exporter</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;true&#34;</span>,
        <span style="color:#a6e22e">prometheus</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;true&#34;</span>
    },
    <span style="color:#a6e22e">keyName</span>: <span style="color:#66d9ef">sshkey.keyName</span>,
    <span style="color:#a6e22e">userData</span>: <span style="color:#66d9ef">cloudconfig.rendered</span>
});

<span style="color:#66d9ef">export</span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">publicIp</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">instance</span>.<span style="color:#a6e22e">publicIp</span>;
<span style="color:#66d9ef">export</span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">publicHostName</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">instance</span>.<span style="color:#a6e22e">publicDns</span>;
</code></pre></div><p>Similar to the Python and Go definitions, we reference the security group IDs, SSH key names, and also export some variables for us to see in the <code>pulumi up</code> output.</p>
<h4 id="running-pulumi-2">Running Pulumi</h4>
<p>We&rsquo;ll now run <code>pulumi up</code> and see whether an instance is created: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ pulumi up
Previewing update <span style="color:#f92672">(</span>basic-ec2-tsc<span style="color:#f92672">)</span>

View Live: https://app.pulumi.com/yetiops/basic-ec2-tsc/basic-ec2-tsc/previews/fc1e60e1-d5bf-41f8-8524-a8090795dce4

     Type                      Name                         Plan
 +   pulumi:pulumi:Stack       basic-ec2-tsc-basic-ec2-tsc  create
 +   ├─ aws:ec2:KeyPair        yetiops-prom                 create
 +   ├─ aws:ec2:SecurityGroup  yetiops-prom                 create
 +   └─ aws:ec2:Instance       yetiops-prom                 create

Resources:
    + <span style="color:#ae81ff">4</span> to create

Do you want to perform this update? Yes
Updating <span style="color:#f92672">(</span>basic-ec2-tsc<span style="color:#f92672">)</span>

View Live: https://app.pulumi.com/yetiops/basic-ec2-tsc/basic-ec2-tsc/updates/5

     Type                      Name                         Status
 +   pulumi:pulumi:Stack       basic-ec2-tsc-basic-ec2-tsc  created
 +   ├─ aws:ec2:SecurityGroup  yetiops-prom                 created
 +   ├─ aws:ec2:KeyPair        yetiops-prom                 created
 +   └─ aws:ec2:Instance       yetiops-prom                 created

Outputs:
    publicHostName: <span style="color:#e6db74">&#34;ec2-3-8-56-15.eu-west-2.compute.amazonaws.com&#34;</span>
    publicIp      : <span style="color:#e6db74">&#34;3.8.56.15&#34;</span>

Resources:
    + <span style="color:#ae81ff">4</span> created

Duration: 20s

$ ssh -i ~/.ssh/id_rsa ubuntu@3.8.56.15
Welcome to Ubuntu 20.04.2 LTS <span style="color:#f92672">(</span>GNU/Linux 5.4.0-1041-aws aarch64<span style="color:#f92672">)</span>
 
 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Mon Apr  <span style="color:#ae81ff">5</span> 18:41:35 UTC <span style="color:#ae81ff">2021</span>

  System load:  0.16              Processes:             <span style="color:#ae81ff">150</span>
  Usage of /:   18.2% of 7.59GB   Users logged in:       <span style="color:#ae81ff">0</span>
  Memory usage: 25%               IPv4 address <span style="color:#66d9ef">for</span> ens5: 172.31.41.195
  Swap usage:   0%

<span style="color:#ae81ff">4</span> updates can be installed immediately.
<span style="color:#ae81ff">3</span> of these updates are security updates.
To see these additional updates run: apt list --upgradable



The programs included with the Ubuntu system are free software;
the exact distribution terms <span style="color:#66d9ef">for</span> each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

To run a command as administrator <span style="color:#f92672">(</span>user <span style="color:#e6db74">&#34;root&#34;</span><span style="color:#f92672">)</span>, use <span style="color:#e6db74">&#34;sudo &lt;command&gt;&#34;</span>.
See <span style="color:#e6db74">&#34;man sudo_root&#34;</span> <span style="color:#66d9ef">for</span> details.

$ ss -tlunp
Netid                   State                    Recv-Q                   Send-Q                                          Local Address:Port                                     Peer Address:Port                  Process
udp                     UNCONN                   <span style="color:#ae81ff">0</span>                        <span style="color:#ae81ff">0</span>                                               127.0.0.53%lo:53                                            0.0.0.0:*
udp                     UNCONN                   <span style="color:#ae81ff">0</span>                        <span style="color:#ae81ff">0</span>                                          172.31.41.195%ens5:68                                            0.0.0.0:*
tcp                     LISTEN                   <span style="color:#ae81ff">0</span>                        <span style="color:#ae81ff">4096</span>                                            127.0.0.53%lo:53                                            0.0.0.0:*
tcp                     LISTEN                   <span style="color:#ae81ff">0</span>                        <span style="color:#ae81ff">128</span>                                                   0.0.0.0:22                                            0.0.0.0:*
tcp                     LISTEN                   <span style="color:#ae81ff">0</span>                        <span style="color:#ae81ff">128</span>                                                      <span style="color:#f92672">[</span>::<span style="color:#f92672">]</span>:22                                               <span style="color:#f92672">[</span>::<span style="color:#f92672">]</span>:*
tcp                     LISTEN                   <span style="color:#ae81ff">0</span>                        <span style="color:#ae81ff">4096</span>                                                        *:9100                                                *:*

$ curl localhost:9100/metrics | grep -i aarch
node_uname_info<span style="color:#f92672">{</span>domainname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(none)&#34;</span>,machine<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;aarch64&#34;</span>,nodename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;ip-172-31-41-195&#34;</span>,release<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;5.4.0-1041-aws&#34;</span>,sysname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Linux&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;#43-Ubuntu SMP Fri Mar 19 22:06:27 UTC 2021&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>
</code></pre></div><p>There we go, everything we asked for!</p>
<h4 id="all-the-code-2">All the code</h4>
<p>The full <code>index.ts</code> is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-typescript" data-lang="typescript"><span style="color:#66d9ef">import</span> <span style="color:#f92672">*</span> <span style="color:#66d9ef">as</span> <span style="color:#a6e22e">pulumi</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#34;@pulumi/pulumi&#34;</span>;
<span style="color:#66d9ef">import</span> <span style="color:#f92672">*</span> <span style="color:#66d9ef">as</span> <span style="color:#a6e22e">aws</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#34;@pulumi/aws&#34;</span>;
<span style="color:#66d9ef">import</span> <span style="color:#f92672">*</span> <span style="color:#66d9ef">as</span> <span style="color:#a6e22e">awsx</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#34;@pulumi/awsx&#34;</span>;
<span style="color:#66d9ef">import</span> <span style="color:#f92672">*</span> <span style="color:#66d9ef">as</span> <span style="color:#a6e22e">cloudinit</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#34;@pulumi/cloudinit&#34;</span>;
<span style="color:#66d9ef">import</span> <span style="color:#f92672">*</span> <span style="color:#66d9ef">as</span> <span style="color:#a6e22e">fs</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#34;fs&#34;</span>;

<span style="color:#66d9ef">let</span> <span style="color:#a6e22e">config</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">Config</span>();

<span style="color:#66d9ef">let</span> <span style="color:#a6e22e">common_name</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">require</span>(<span style="color:#e6db74">&#34;common_name&#34;</span>);

<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">size</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;t4g.micro&#34;</span>;

<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">ami</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">output</span>(<span style="color:#a6e22e">aws</span>.<span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">getAmi</span>({
    <span style="color:#a6e22e">filters</span><span style="color:#f92672">:</span> [{
        <span style="color:#a6e22e">name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;name&#34;</span>,
        <span style="color:#a6e22e">values</span><span style="color:#f92672">:</span> [
            <span style="color:#e6db74">&#34;ubuntu/images/hvm-ssd/ubuntu-focal-20.04-arm64-server-*&#34;</span>
        ]
    }],
    <span style="color:#a6e22e">owners</span><span style="color:#f92672">:</span> [
        <span style="color:#e6db74">&#34;099720109477&#34;</span>
    ],
    <span style="color:#a6e22e">mostRecent</span>: <span style="color:#66d9ef">true</span>,
}));

<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">group</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">aws</span>.<span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">SecurityGroup</span>(<span style="color:#a6e22e">common_name</span>, {
    <span style="color:#a6e22e">ingress</span><span style="color:#f92672">:</span> [
        {
            <span style="color:#a6e22e">protocol</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;tcp&#34;</span>,
            <span style="color:#a6e22e">fromPort</span>: <span style="color:#66d9ef">22</span>,
            <span style="color:#a6e22e">toPort</span>: <span style="color:#66d9ef">22</span>,
            <span style="color:#a6e22e">cidrBlocks</span><span style="color:#f92672">:</span> [
                <span style="color:#e6db74">&#34;86.29.247.18/32&#34;</span>
            ]
        },
        {
            <span style="color:#a6e22e">protocol</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;tcp&#34;</span>,
            <span style="color:#a6e22e">fromPort</span>: <span style="color:#66d9ef">9100</span>,
            <span style="color:#a6e22e">toPort</span>: <span style="color:#66d9ef">9100</span>,
            <span style="color:#a6e22e">cidrBlocks</span><span style="color:#f92672">:</span> [
                <span style="color:#e6db74">&#34;86.29.247.18/32&#34;</span>
            ]
        },
    ],
    <span style="color:#a6e22e">egress</span><span style="color:#f92672">:</span> [
        {
            <span style="color:#a6e22e">protocol</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;-1&#34;</span>,
            <span style="color:#a6e22e">fromPort</span>: <span style="color:#66d9ef">0</span>,
            <span style="color:#a6e22e">toPort</span>: <span style="color:#66d9ef">0</span>,
            <span style="color:#a6e22e">cidrBlocks</span><span style="color:#f92672">:</span> [
                <span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>
            ]
        }
    ]
});

<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">homedir</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">require</span>(<span style="color:#e6db74">&#39;os&#39;</span>).<span style="color:#a6e22e">homedir</span>();

<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">sshkey_path</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">`</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">homedir</span><span style="color:#e6db74">}</span><span style="color:#e6db74">/.ssh/id_rsa.pub`</span>;

<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">sshkey_file</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">fs</span>.<span style="color:#a6e22e">readFileSync</span>(<span style="color:#a6e22e">sshkey_path</span>, <span style="color:#e6db74">&#39;utf8&#39;</span>);

<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">sshkey</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">aws</span>.<span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">KeyPair</span>(<span style="color:#a6e22e">common_name</span>, {
    <span style="color:#a6e22e">keyName</span>: <span style="color:#66d9ef">common_name</span>,
    <span style="color:#a6e22e">publicKey</span>: <span style="color:#66d9ef">sshkey_file</span>
});

<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">cloudconfig</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">output</span>(<span style="color:#a6e22e">cloudinit</span>.<span style="color:#a6e22e">getConfig</span>({
    <span style="color:#a6e22e">base64Encode</span>: <span style="color:#66d9ef">false</span>,
    <span style="color:#a6e22e">gzip</span>: <span style="color:#66d9ef">false</span>,
    <span style="color:#a6e22e">parts</span><span style="color:#f92672">:</span> [{
        <span style="color:#a6e22e">content</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;#cloudconfig\npackages:\n- prometheus-node-exporter&#34;</span>,
        <span style="color:#a6e22e">contentType</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;text/cloud-config&#34;</span>,
        <span style="color:#a6e22e">filename</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;init.cfg&#34;</span>
    }],
}, { <span style="color:#a6e22e">async</span>: <span style="color:#66d9ef">true</span> }));

<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">instance</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">aws</span>.<span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">Instance</span>(<span style="color:#a6e22e">common_name</span>, {
    <span style="color:#a6e22e">instanceType</span>: <span style="color:#66d9ef">size</span>,
    <span style="color:#a6e22e">vpcSecurityGroupIds</span><span style="color:#f92672">:</span> [
        <span style="color:#a6e22e">group</span>.<span style="color:#a6e22e">id</span>
    ],
    <span style="color:#a6e22e">ami</span>: <span style="color:#66d9ef">ami.id</span>,
    <span style="color:#a6e22e">tags</span><span style="color:#f92672">:</span> {
        <span style="color:#a6e22e">Name</span>: <span style="color:#66d9ef">common_name</span>,
        <span style="color:#a6e22e">node_exporter</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;true&#34;</span>,
        <span style="color:#a6e22e">prometheus</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;true&#34;</span>
    },
    <span style="color:#a6e22e">keyName</span>: <span style="color:#66d9ef">sshkey.keyName</span>,
    <span style="color:#a6e22e">userData</span>: <span style="color:#66d9ef">cloudconfig.rendered</span>
});

<span style="color:#66d9ef">export</span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">publicIp</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">instance</span>.<span style="color:#a6e22e">publicIp</span>;
<span style="color:#66d9ef">export</span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">publicHostName</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">instance</span>.<span style="color:#a6e22e">publicDns</span>;
</code></pre></div><p>Compared to the Python and Go code, it is probably of a similar verbosity to the Python code, but does have the ability to add static types to it. While we haven&rsquo;t used them here, we can gain that safety if we so choose.</p>
<h2 id="what-else-can-we-do">What else can we do?</h2>
<p>This post just touches the surface of what Pulumi can do. Remember that one of the benefits here is that you are using a standard programming language. Whatever you can build in the language, you can probably use with Pulumi. Whether this is more natural looping, calling APIs, and potentially even providing your own API to Pulumi.</p>
<p>A very rudimentary example is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-go" data-lang="go">		<span style="color:#a6e22e">instances</span> <span style="color:#f92672">:=</span> []<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">&#39;a&#39;</span>, <span style="color:#e6db74">&#39;b&#39;</span>, <span style="color:#e6db74">&#39;b&#39;</span>} 

		<span style="color:#a6e22e">srvMap</span> <span style="color:#f92672">:=</span> make([]<span style="color:#f92672">*</span><span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">Instance</span>, <span style="color:#ae81ff">3</span>)

		<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">v</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">instances</span> {
			<span style="color:#a6e22e">index</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strconv</span>.<span style="color:#a6e22e">Itoa</span>(<span style="color:#a6e22e">i</span>)
			<span style="color:#a6e22e">srv</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">NewInstance</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">commonName</span><span style="color:#f92672">+</span><span style="color:#a6e22e">v</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">ec2</span>.<span style="color:#a6e22e">InstanceArgs</span>{
				<span style="color:#a6e22e">Tags</span>:                <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringMap</span>{<span style="color:#e6db74">&#34;Name&#34;</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">commonName</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">v</span>)},
				<span style="color:#a6e22e">InstanceType</span>:        <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;t4g.micro&#34;</span>),
				<span style="color:#a6e22e">VpcSecurityGroupIds</span>: <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>{<span style="color:#a6e22e">group</span>.<span style="color:#a6e22e">ID</span>()},
				<span style="color:#a6e22e">Ami</span>:                 <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">ami</span>.<span style="color:#a6e22e">Id</span>),
				<span style="color:#a6e22e">KeyName</span>:             <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringOutput</span>(<span style="color:#a6e22e">sshkey</span>.<span style="color:#a6e22e">KeyName</span>),
				<span style="color:#a6e22e">UserData</span>:            <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">String</span>(<span style="color:#a6e22e">cloudconfig</span>.<span style="color:#a6e22e">Rendered</span>),
			})

			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
				<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
			}

			<span style="color:#a6e22e">srvMap</span>[<span style="color:#a6e22e">v</span>] = <span style="color:#a6e22e">srv</span>
		}

		<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">srvPublicIPs</span> <span style="color:#a6e22e">pulumi</span>.<span style="color:#a6e22e">StringArray</span>

		<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">srv</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">srvMap</span> {
			<span style="color:#a6e22e">srvPublicIPs</span> = append(<span style="color:#a6e22e">srvPublicIPs</span>, <span style="color:#a6e22e">srv</span>.<span style="color:#a6e22e">PublicIp</span>)
		}

		<span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Export</span>(<span style="color:#e6db74">&#34;publicIps&#34;</span>, <span style="color:#a6e22e">srvPublicIPs</span>)
</code></pre></div><p>The equivalent Terraform would be: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;instances&#34;</span> {
  type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;list(string)&#34;</span>
  default <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;a&#34;, &#34;b&#34;, &#34;c&#34;</span>]
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_instance&#34; &#34;yetiops-aws-prom&#34;</span> {
  ami           <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">aws_ami</span>.<span style="color:#66d9ef">ubuntu</span>.<span style="color:#66d9ef">id</span>
  count         <span style="color:#f92672">=</span> <span style="color:#66d9ef">length</span>(<span style="color:#66d9ef">instances</span>)
  instance_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;t4g.micro&#34;</span>
  user_data     <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">template_file</span>.<span style="color:#66d9ef">ubuntu</span>.<span style="color:#66d9ef">template</span>

  key_name <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_key_pair</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">aws</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">key_name</span>

  vpc_security_group_ids <span style="color:#f92672">=</span> [
    <span style="color:#66d9ef">aws_security_group</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">aws</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">id</span>
  ]

  tags <span style="color:#f92672">=</span> {
    Name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-aws-prom-${[count.index]}&#34;</span>
    prometheus <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
    node_exporter <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
  }
}

<span style="color:#66d9ef">output</span> <span style="color:#e6db74">&#34;ip&#34;</span> {
  value <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_instance</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">aws</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#960050;background-color:#1e0010">*</span>.<span style="color:#66d9ef">public_ip</span>
}
</code></pre></div><p>There is less code, but I personally find it less obvious to read what is happening here.</p>
<p>We can see that in the output of <code>pulumi up</code>, it will create three instances: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ pulumi up
Previewing update <span style="color:#f92672">(</span>basic-ec2<span style="color:#f92672">)</span>

View Live: https://app.pulumi.com/yetiops/basic-ec2/basic-ec2/previews/4fb79336-452e-4b78-a7cb-bf1a699001d3

     Type                      Name                 Plan
 +   pulumi:pulumi:Stack       basic-ec2-basic-ec2  create
 +   ├─ aws:ec2:KeyPair        yetiops-prom         create
 +   ├─ aws:ec2:SecurityGroup  yetiops-prom         create
 +   ├─ aws:ec2:Instance       yetiops-proma        create
 +   ├─ aws:ec2:Instance       yetiops-promb        create
 +   └─ aws:ec2:Instance       yetiops-promc        create

Resources:
    + <span style="color:#ae81ff">6</span> to create

Do you want to perform this update? yes
Updating <span style="color:#f92672">(</span>basic-ec2<span style="color:#f92672">)</span>

View Live: https://app.pulumi.com/yetiops/basic-ec2/basic-ec2/updates/22

     Type                      Name                 Status
 +   pulumi:pulumi:Stack       basic-ec2-basic-ec2  created
 +   ├─ aws:ec2:KeyPair        yetiops-prom         created
 +   ├─ aws:ec2:SecurityGroup  yetiops-prom         created
 +   ├─ aws:ec2:Instance       yetiops-proma        created
 +   ├─ aws:ec2:Instance       yetiops-promc        created
 +   └─ aws:ec2:Instance       yetiops-promb        created

Outputs:
    publicIps: <span style="color:#f92672">[</span>
        <span style="color:#f92672">[</span>0<span style="color:#f92672">]</span>: <span style="color:#e6db74">&#34;3.10.55.200&#34;</span>
        <span style="color:#f92672">[</span>1<span style="color:#f92672">]</span>: <span style="color:#e6db74">&#34;18.132.17.85&#34;</span>
        <span style="color:#f92672">[</span>c<span style="color:#f92672">]</span>: <span style="color:#e6db74">&#34;18.134.206.111&#34;</span>
    <span style="color:#f92672">]</span>

Resources:
    + <span style="color:#ae81ff">6</span> created

Duration: 26s

$ curl 3.10.55.200:9100/metrics | grep -i aarch
<span style="color:#ae81ff">100</span>  116k    <span style="color:#ae81ff">0</span>  116k    <span style="color:#ae81ff">0</span>     <span style="color:#ae81ff">0</span>   694k      <span style="color:#ae81ff">0</span> --:--:-- --:--:-- --:--:--  698k
node_uname_info<span style="color:#f92672">{</span>domainname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(none)&#34;</span>,machine<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;aarch64&#34;</span>,nodename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;ip-172-31-34-49&#34;</span>,release<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;5.4.0-1041-aws&#34;</span>,sysname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Linux&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;#43-Ubuntu SMP Fri Mar 19 22:06:27 UTC 2021&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>

$ curl 18.132.17.85:9100/metrics | grep -i aarch
node_uname_info<span style="color:#f92672">{</span>domainname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(none)&#34;</span>,machine<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;aarch64&#34;</span>,nodename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;ip-172-31-38-67&#34;</span>,release<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;5.4.0-1041-aws&#34;</span>,sysname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Linux&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;#43-Ubuntu SMP Fri Mar 19 22:06:27 UTC 2021&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>

$ curl 18.134.206.111:9100/metrics | grep -i aarch
node_uname_info<span style="color:#f92672">{</span>domainname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(none)&#34;</span>,machine<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;aarch64&#34;</span>,nodename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;ip-172-31-46-14&#34;</span>,release<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;5.4.0-1041-aws&#34;</span>,sysname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Linux&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;#43-Ubuntu SMP Fri Mar 19 22:06:27 UTC 2021&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>
</code></pre></div><p>How about checking to see if we have a certain named SSH key, and uses that if it exists? We then can fallback to our default RSA key if it doesn&rsquo;t: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python">yetissh_path <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;{}/.ssh/ssh_yetiops.pub&#34;</span><span style="color:#f92672">.</span>format(homedir)

<span style="color:#66d9ef">if</span> os<span style="color:#f92672">.</span>path<span style="color:#f92672">.</span>isfile(yetissh_path):
    sshkey_path <span style="color:#f92672">=</span> yetissh_path
<span style="color:#66d9ef">else</span>:
    sshkey_path <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;{}/.ssh/id_rsa.pub&#34;</span><span style="color:#f92672">.</span>format(
                      homedir
                  )
sshkey_file <span style="color:#f92672">=</span> open(sshkey_path, <span style="color:#e6db74">&#39;r&#39;</span>)

sshkey <span style="color:#f92672">=</span> aws<span style="color:#f92672">.</span>ec2<span style="color:#f92672">.</span>KeyPair(common_name,
  public_key <span style="color:#f92672">=</span> sshkey_file<span style="color:#f92672">.</span>read()<span style="color:#f92672">.</span>strip(<span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>)
)
</code></pre></div><p>The equivalent Terraform would be: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;yetissh_path&#34;</span> {
  type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;string&#34;</span>
  default <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;~/.ssh/yetiops.pub&#34;</span>
}

<span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;rsa_ssh_path&#34;</span> {
  type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;string&#34;</span>
  default <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;~/.ssh/yetiops.pub&#34;</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_key_pair&#34; &#34;yetiops-aws-prom&#34;</span> {
  key_name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span>
  public_key <span style="color:#f92672">=</span> <span style="color:#66d9ef">fileexists</span>(<span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">yetissh_path</span>) <span style="color:#960050;background-color:#1e0010">?</span> <span style="color:#66d9ef">file</span>(<span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">yetissh_path</span>) <span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">rsa_ssh_path</span> 
}
</code></pre></div><p>Again, less code, but not as easy to ready (in my opinion).</p>
<p>We can prove the Python version works with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ pulumi up

Previewing update <span style="color:#f92672">(</span>basic-ec2-py<span style="color:#f92672">)</span>

View Live: https://app.pulumi.com/yetiops/basic-ec2-py/basic-ec2-py/previews/b1ee9964-f8e7-45fa-990b-94f7fbff0fca

     Type                      Name                       Plan       Info
 +   pulumi:pulumi:Stack       basic-ec2-py-basic-ec2-py  create     <span style="color:#ae81ff">1</span> warning
 +   ├─ aws:ec2:KeyPair        yetiops-prom               create
 +   ├─ aws:ec2:SecurityGroup  yetiops-prom               create
 +   └─ aws:ec2:Instance       yetiops-prom               create

Diagnostics:
  pulumi:pulumi:Stack <span style="color:#f92672">(</span>basic-ec2-py-basic-ec2-py<span style="color:#f92672">)</span>:
    warning: get_ami is deprecated: aws.getAmi has been deprecated in favor of aws.ec2.getAmi


Do you want to perform this update? yes
Updating <span style="color:#f92672">(</span>basic-ec2-py<span style="color:#f92672">)</span>

View Live: https://app.pulumi.com/yetiops/basic-ec2-py/basic-ec2-py/updates/11

     Type                      Name                       Status      Info
 +   pulumi:pulumi:Stack       basic-ec2-py-basic-ec2-py  created     <span style="color:#ae81ff">1</span> warning
 +   ├─ aws:ec2:KeyPair        yetiops-prom               created
 +   ├─ aws:ec2:SecurityGroup  yetiops-prom               created
 +   └─ aws:ec2:Instance       yetiops-prom               created

Diagnostics:
  pulumi:pulumi:Stack <span style="color:#f92672">(</span>basic-ec2-py-basic-ec2-py<span style="color:#f92672">)</span>:
    warning: get_ami is deprecated: aws.getAmi has been deprecated in favor of aws.ec2.getAmi

Outputs:
    publicHostName: <span style="color:#e6db74">&#34;ec2-18-134-210-137.eu-west-2.compute.amazonaws.com&#34;</span>
    publicIp      : <span style="color:#e6db74">&#34;18.134.210.137&#34;</span>

Resources:
    + <span style="color:#ae81ff">4</span> created

Duration: 24s

$ ssh -i ~/.ssh/ssh_yetiops ubuntu@18.134.210.137

Welcome to Ubuntu 20.04.2 LTS <span style="color:#f92672">(</span>GNU/Linux 5.4.0-1041-aws aarch64<span style="color:#f92672">)</span>

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Tue Apr  <span style="color:#ae81ff">6</span> 08:11:15 UTC <span style="color:#ae81ff">2021</span>

  System load:  0.32              Processes:             <span style="color:#ae81ff">148</span>
  Usage of /:   18.1% of 7.59GB   Users logged in:       <span style="color:#ae81ff">0</span>
  Memory usage: 25%               IPv4 address <span style="color:#66d9ef">for</span> ens5: 172.31.42.129
  Swap usage:   0%

<span style="color:#ae81ff">4</span> updates can be installed immediately.
<span style="color:#ae81ff">3</span> of these updates are security updates.
To see these additional updates run: apt list --upgradable


The programs included with the Ubuntu system are free software;
the exact distribution terms <span style="color:#66d9ef">for</span> each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

To run a command as administrator <span style="color:#f92672">(</span>user <span style="color:#e6db74">&#34;root&#34;</span><span style="color:#f92672">)</span>, use <span style="color:#e6db74">&#34;sudo &lt;command&gt;&#34;</span>.
See <span style="color:#e6db74">&#34;man sudo_root&#34;</span> <span style="color:#66d9ef">for</span> details.

$
</code></pre></div><p>Even these arbitrary examples show what is possible. If you can build it in your chosen language, you can make use of it in Pulumi. The fact you can integrate the modules directly into your code gives a lot of options for how, when and why you might build your infrastructure.</p>
<h2 id="summary">Summary</h2>
<p>Infrastructure-as-Code is fast becoming the defacto way to build, define and deploy your infrastructure. Given the support that many of these tools have for a varying array of cloud providers, applications, network hardware, monitoring solutions, it is becoming hard to justify not using this approach.</p>
<p>In many ways, Terraform is already becoming the go-to tool for this. For example, even though AWS already has IaC tools (CloudFormation, AWS CDK), they include Terraform in their own documentation examples (like <a href="https://docs.aws.amazon.com/managedservices/latest/accelerate-guide/acc-mem-tags-terraform.html">here</a>.</p>
<p>However, there are times when trying to describe your end state in Terraform can become very unwieldy, or at least not obvious. Using Pulumi, especially if you have skills/background in development provides an alternative approach. Want to create an EC2 instance per AWS region? Query the AWS API, and loop over the result in Pulumi to create the instances. Want to define your AWS/Azure/GCP users and their roles based upon an internal database? Query the database, and create the users based upon the query results.</p>
<p>Pulumi is very powerful. If you find Terraform is making it difficult to define some parts of your infrastructure, it is definitely worth a look!</p>
]]></content>
        </item>
        
        <item>
            <title>My Personal Setup and Workflow in 2021</title>
            <link>https://yetiops.net/posts/my-setup-and-workflow/</link>
            <pubDate>Fri, 26 Feb 2021 12:03:09 +0000</pubDate>
            
            <guid>https://yetiops.net/posts/my-setup-and-workflow/</guid>
            <description>I always enjoy looking at what other people use to get their work done, or what they use for their hobbies. With that in mind, I decided to write the kind of post I like reading!
This is going to cover: -
 My desktops My laptops My peripherals My network and server setup Audio Applications/workflow A few bonus parts too  Rooms I am very lucky in that I have two rooms to use for my setup.</description>
            <content type="html"><![CDATA[<p>I always enjoy looking at what other people use to get their work done, or what they use for their hobbies. With that in mind, I decided to write the kind of post I like reading!</p>
<p>This is going to cover: -</p>
<ul>
<li>My desktops</li>
<li>My laptops</li>
<li>My peripherals</li>
<li>My network and server setup</li>
<li>Audio</li>
<li>Applications/workflow</li>
<li>A few bonus parts too</li>
</ul>
<h2 id="rooms">Rooms</h2>
<p>I am very lucky in that I have two rooms to use for my setup. I am very aware of the fact that not everyone has this luxury, so I do not take it for granted.</p>
<p>The first is my office. This was classed as a bedroom (from a legal standpoint) when we purchased the house, but it could barely fit a single bed in. Recently though, thanks to some work we did on our house last year, the room is much larger. This work also happened to finish within a day of the UK going into lockdown for the first time. This was lucky as I effectively became a remote employee from that day onwards. I have since changed jobs, meaning I will now work remotely on a permanent basis.</p>
<p>The second is what we refer to as my music room. The room is quite small, but is enough to fit in my desk, guitar equipment and guitars. It is where I do podcasting, playing guitar, and also playing games with my son.</p>
<p>The separation is nice, as it allows me to focus entirely on my work when I am in my office, while also allowing me to shut the door on it when the day is done. It also gives a dedicated space for &ldquo;play&rdquo;, in that when I go into the music room, I don&rsquo;t feel the need or urge to check up on work or anything similar.</p>
<h3 id="pictures">Pictures</h3>
<p><strong>Office</strong>
<a href="/img/setup/office.jpg"><img src="/img/setup/office.jpg" alt="Office setup"></a></p>
<p><strong>Music Room</strong>
<a href="/img/setup/symphonyx-screens.jpg"><img src="/img/setup/symphonyx-screens.jpg" alt="Music room setup"></a></p>
<h2 id="naming-conventions">Naming conventions</h2>
<p>All of my computers are named after metal or rock bands. I used to name them after Greek gods or science fiction-related names, but I would end up either forgetting which was which or run out of usable names that were memorable.</p>
<p>Naming them after bands I like makes them easy to remember and personally relevant.</p>
<p>Because of this, I&rsquo;m going to add a link to band each is named after when referring to the computers. A little bonus for those who want to hear lots of notes and/or shouty music!</p>
<h2 id="desktops">Desktops</h2>
<p>As of 2021, I have two desktop computers (one in the office, one in the music room).</p>
<h3 id="behemoth">Behemoth</h3>
<p>Music Link: <a href="https://www.behemoth.pl/">Behemoth</a></p>
<p>Appropriately named due to the specifications of this machine, I put together this machine in September 2020 after not building a machine in nearly a decade. As you&rsquo;ll see later on, I have only quite recently moved my workflow mostly away from MacOS to Linux, hence not building a machine for so long.</p>
<p>This machine is my primary desktop in my office.</p>
<p>The specs are: -</p>
<ul>
<li>AMD Ryzen 3900X (12 cores, 24 threads)</li>
<li>64Gb of Corsair DDR4-3200Mhz RAM</li>
<li>NVIDIA GTX1660 Super GPU</li>
<li>Samsung 970 EVO Plus 1TB NVME drive (Mounted as <code>/</code>)</li>
<li>Crucial BX500 1TB SSD (Mounted as <code>/var</code>)</li>
<li>Western Digital Blue 4TB spinning rust drive (Mounted as <code>/home/$MY-USER/Store</code>)</li>
<li>Chelsio CC2-S310E-CR 10Gb SFP+ NIC</li>
<li>Intel AX200 802.11ax WiFi and Bluetooth 5.0 PCIe card</li>
<li>ASUS X570-P ATX Motherboard</li>
<li>Corsair RM650x PSU</li>
<li>Noctua NH-D15 CPU cooler</li>
<li>Fractal Design Meshify S2 Mid Tower case</li>
</ul>
<p>This is running on Manjaro i3 edition.</p>
<p>I previously ran the Western Digital Blue drive as <code>/home</code>, but this drive is not quick. Most actions (including opening a terminal to my home directory) involved waiting for the drive to spin up. While the idea of having lots of storage for <code>/home</code> was nice, the speed penalty wasn&rsquo;t worth it. Instead I now use it for larger directories (e.g. music, games, photos) and the NVME drive for nearly everything else.</p>
<p>I also run the Crucial SSD (SATA SSD, not NVME) for the <code>/var</code> directory. The reason for this is because both KVM/libvirt (for virtual machines) and containers (either Docker, Podman, CRI-O or otherwise) both use directories in <code>/var</code> for storage. This machine serves a dual purpose of being my primary personal desktop, as well as where I run most of my labs (that contribute to the other posts on this site). Having a dedicated SSD for this means that the lab machines start and run quick, without impacting other storage. My recent series on running a Kubernetes cluster with Windows and Linux used this machine, and the workers (even the Windows ones) started in seconds.</p>
<p>I also use a 10Gb SFP+ NIC, with multimode fibre to my core switch. Other than my router, nothing else I own runs 10Gb as of yet so this is more of a luxury for now, but in the future I intend to move as much as I can over to 10Gb (or at least multi-gigabit) networking.</p>
<h4 id="pictures-and-screenshots">Pictures and screenshots</h4>
<p><img src="/img/setup/behemoth-case.jpg" alt="Behemoth Desktop"></p>
<p><a href="/img/setup/behemoth-screenshot.png"><img src="/img/setup/behemoth-screenshot.png" alt="Behemoth Screenshot"></a></p>
<p><img src="/img/setup/behemoth-terminal.png" alt="Behemoth Terminal"></p>
<h3 id="symphony-x">Symphony X</h3>
<p>Music Link: <a href="https://www.youtube.com/watch?v=wzmZtXDI22M">Symphony X - Inferno (Unleash The Fire)</a></p>
<p>This machine was originally acquired for labbing and testing purposes, as my other machines at the time were all laptops (and hence burning my knees when running multiple VMs) or providing services in my home (DNS, DHCP, Prometheus, Plex etc). However since putting <strong><a href="#behemoth">Behemoth</a></strong> together, I no longer needed a dedicated labbing machine.</p>
<p>Instead, this is now the main computer in my music room, replacing <strong><a href="#pink-floyd">Pink Floyd</a></strong>.</p>
<p>The machine is a Dell Optiplex 3020 SFF, with the following specs: -</p>
<ul>
<li>Intel i5-4570 (4 cores, 4 threads)</li>
<li>16Gb of RAM</li>
<li>NVIDIA GeForce GT1030 GPU</li>
<li>256Gb SSD (Mounted as <code>/</code>)</li>
<li>480Gb spinning rust drive (Mounted as <code>/home</code>)</li>
</ul>
<p>This machine is runs Ubuntu MATE 20.10, using the Pantheon layout, Yaru icons and Yaru theme. There is work on Ubuntu MATE having official Yaru theme support in future, so I won&rsquo;t need to do this myself in future.</p>
<p>I added the RAM when I used the machine for labbing, and then the SSD and GPU when I turned it into a desktop machine. It&rsquo;s primary use is for <a href="https://adminadminpodcast.co.uk">podcasting</a>, a little video editing, and playing Minecraft, SuperTuxKart and Hotshot Racing with my son. I have yet to do much in the way of music recording with it, but I intend to at least get it functional for this purpose.</p>
<p>I am aiming to replace this machine at some point in the future with something more powerful and upgradable (currently limited to 16Gb of RAM and two PCIe slots), but right now it serves what I use it for with no issues or slowdown at all.</p>
<p>I also recently purchased a 10G SFP+ NIC for this machine, but unfortunately I made the error in not checking if I had any free PCIe slots that could handle it. The only slot it will work in is already taken up by the GT1030! At least when I decide to replace the machine, I&rsquo;ll have a 10G NIC available&hellip;</p>
<h4 id="pictures-and-screenshots-1">Pictures and screenshots</h4>
<p><img src="/img/setup/symphonyx.jpg" alt="Symphony X Desktop"></p>
<p><a href="/img/setup/symphonyx-desktop.png"><img src="/img/setup/symphonyx-desktop.png" alt="Symphony X Screenshot"></a></p>
<p><img src="/img/setup/symphonyx-terminal.png" alt="Symphony X Terminal"></p>
<h3 id="dream-theater">Dream Theater</h3>
<p>Music Link: <a href="https://www.youtube.com/watch?v=vjoKyj51o_I">Dream Theater - Count Of Tuscany</a></p>
<p>Dream Theater is a Raspberry Pi 4 (4Gb model). This used to host Nextcloud (with a couple of SSDs attached), however it is in the process of being turned into an ARM64-based workstation, with a USB-attached SSD. While it may not be up to something like an Apple M1, Graviton2 or Ampere Altra, it will be useful to test the ARM64 ecosystem for my workflow.</p>
<h2 id="laptops">Laptops</h2>
<p>I currently own 3 laptops (not including my work-provided machine): -</p>
<p><img src="/img/setup/laptops.jpg" alt="Laptops"></p>
<h3 id="deep-purple">Deep Purple</h3>
<p>Music Link: <a href="https://www.youtube.com/watch?v=t4fDCwDiWJQ">Deep Purple - Burn (Live 1974)</a></p>
<p>Deep Purple is a 2015 Apple Macbook Pro, which up until last September was the most powerful machine I owned. I am a bit of a recovering Mac addict, having used Linux daily for nearly 15 years, but only in the past 2-3 years have I transitioned to Linux being my primary desktop operating system.</p>
<p>When I was more focussed on music and audio, Macs made a lot of sense. I don&rsquo;t record anywhere near the amount of music I used to though, so staying in the Mac ecosystem made less and less sense, especially given how much easier I find developing and my day-to-day responsibilities using Linux.</p>
<p>If I do get back into recording music on a more consistent basis, I may still end up with a Mac again (mainly for plugin/virtual instrument availability), but right now my need is low for anything in the Apple ecosystem.</p>
<p>The specs of this laptop are: -</p>
<ul>
<li>Intel i7-4470HQ (4 cores, 8 threads)</li>
<li>16Gb of RAM</li>
<li>Intel Iris Pro GPU</li>
<li>256Gb PCIe SSD</li>
<li>15&rdquo; Retina Display</li>
</ul>
<h4 id="pictures-and-screenshots-2">Pictures and screenshots</h4>
<p><a href="/img/setup/deeppurple-screenshot.png"><img src="/img/setup/deeppurple-screenshot.png" alt="Deep Purple Screenshot"></a></p>
<p><img src="/img/setup/deeppurple-terminal.png" alt="Deep Purple Terminal"></p>
<h3 id="meshuggah">Meshuggah</h3>
<p>Music Link: <a href="https://www.youtube.com/watch?v=4A_tSyJBsRQ">Meshuggah - New Millennium Cyanide Christ</a></p>
<p>Meshuggah is a 2011 Lenovo Thinkpad X220. This machine was acquired when I fell into the <a href="https://www.reddit.com/r/thinkpad/">/r/thinkpad</a> rabbit-hole a few years ago while wanting a machine to run Linux on.</p>
<p>After going through a number of different distributions (Arch, Ubuntu MATE, KDE Neon), I didn&rsquo;t use it for about a year or so. I revived it as my homeserver for over a year.</p>
<p>The primary reason for using the X220 as a server over the previous setup of a couple of Raspberry Pis was because I wanted to learn more about Prometheus. To do so, I wanted a machine I could run VMs and containers on for testing purposes. This is all detailed in <a href="/posts/prometheus-the-exporter-life/">this post</a> I made towards the end of 2019.</p>
<p>In late 2020, with my move to using <strong><a href="#symphony-x">Symphony X</a></strong> as my main music room machine, I retired <strong><a href="#pink-floyd">Pink Floyd</a></strong>. <strong><a href="#pink-floyd">Pink Floyd</a></strong> then replaced <strong>Meshuggah</strong> as one of my home servers (as it is more powerful and with faster storage).</p>
<p><strong>Meshuggah</strong> has now become my distribution testing laptop again, currently running <strong>GhostBSD</strong> (a Desktop spin of FreeBSD).</p>
<p>The specs for this laptop are: -</p>
<ul>
<li>Intel i5-2420M (2 cores, 4 threads)</li>
<li>16Gb of RAM</li>
<li>256Gb SSD</li>
<li>Inbuilt Intel GPU</li>
<li>13&rdquo; display</li>
</ul>
<h4 id="pictures-and-screenshots-3">Pictures and screenshots</h4>
<p><a href="/img/setup/meshuggah-ghostbsd-screenshot.png"><img src="/img/setup/meshuggah-ghostbsd-screenshot.png" alt="Meshuggah Screenshot"></a></p>
<p><img src="/img/setup/meshuggah-ghostbsd-terminal.png" alt="Meshuggah Terminal"></p>
<h3 id="haken">Haken</h3>
<p>Music Link: <a href="https://www.youtube.com/watch?v=yW8nBlZZvxM">Haken - 1985</a></p>
<p>Haken is a 2012 HP Elitebook 9470m. This machine was given to me by my old workplace, as it was destined for eWaste. I wanted a machine for testing Linux/BSD on, and at the time <strong><a href="#meshuggah">Meshuggah</a></strong> was still in place as my homeserver.</p>
<p>Currently this machine runs Ubuntu 20.10 (Using GNOME rather than MATE) and also dual-boots to OpenBSD (using i3). I am still working on the OpenBSD install, but overall it is a very pleasant experience.</p>
<p>The specs for this laptop are: -</p>
<ul>
<li>Intel i7-3687u (2 cores, 4 threads)</li>
<li>16Gb of RAM</li>
<li>240Gb SSD</li>
<li>Inbuilt Intel GPU</li>
<li>14&rdquo; Display</li>
</ul>
<h4 id="pictures-and-screenshots-4">Pictures and screenshots</h4>
<p><strong>Ubuntu</strong>
<a href="/img/setup/haken-ubuntu-screenshot.png"><img src="/img/setup/haken-ubuntu-screenshot.png" alt="Haken Ubuntu Screenshot"></a></p>
<p><img src="/img/setup/haken-ubuntu-terminal.png" alt="Haken Ubuntu Terminal"></p>
<p><strong>OpenBSD</strong>
<a href="/img/setup/haken-openbsd-screenshot.png"><img src="/img/setup/haken-openbsd-screenshot.png" alt="Haken OpenBSD Screenshot"></a></p>
<p><img src="/img/setup/haken-openbsd-terminal.png" alt="Haken OpenBSD Terminal"></p>
<h2 id="peripherals">Peripherals</h2>
<p>The peripherals in my office can be seen below: -</p>
<p><a href="/img/setup/peripherals.jpg"><img src="/img/setup/peripherals.jpg" alt="Office Peripherals"></a></p>
<p>The peripherals in the music room can be seen below: -</p>
<p><a href="/img/setup/musicroom-peripherals.jpg"><img src="/img/setup/musicroom-peripherals.jpg" alt="Music Room Peripherals"></a></p>
<h3 id="monitors">Monitors</h3>
<p>As can be seen in the <strong><a href="#rooms">Rooms</a></strong> section of this post, I run dual monitors in both setups.</p>
<h4 id="office">Office</h4>
<p>In the office I have a Dell P3421W 34&rdquo; ultrawide display. This also functions as my KVM switch. All my other peripherals (keyboard, mice, audio interface, card reader, webcam) connect to it via USB.</p>
<p>When I switch between inputs, it also switches the USB peripherals over to the computer being displayed. This is normally either my personal desktop and my work laptop (via a USB-C dock). When I replace my main laptop later this year, it will also use the USB-C dock (or the dock will be replaced with a Thunderbolt dock) and hence be integrated into this setup.</p>
<p>I also have a Dell P2419H 1080p display in portrait mode, mainly for when I&rsquo;m working in the terminal (which is most of the time!).</p>
<p>They are both mounted on a dual monitor arm rather than using the provided stands. I can adjust them to the correct height, layout, and also have more space on my desk for computers and peripherals.</p>
<p><img src="/img/setup/office-monitors.jpg" alt="Office Monitors"></p>
<p><strong>Update (11/05/2021)</strong>: Since creating this post, the P2419H display is now in the music room, replaced by a P2421DC. This is a 1440p monitor, meaning that both the Ultrawide and the portrait monitor have similar DPI (making text crisp on both monitors).</p>
<p>I haven&rsquo;t included any images of this, as the chassis is identical to the P2419H, so it doesn&rsquo;t look like anything has changed!</p>
<h4 id="music-room">Music Room</h4>
<p>In the music room I have two AOC E2460SH 1080p displays. I have owned these displays for around 6 years now and they have served me well. I previously ran three of them in my office (prior to the two Dell monitors), with one in the music room. Now I am back to two monitors in the music room, and two spare monitors that will be used for <em>something</em> someday&hellip;</p>
<p>The monitors are mounted on a vertical dual monitor stand. They are mounted above each other, rather than next to each other, making the best use of space in the music room. The desk isn&rsquo;t quite big enough to have them side-by-side (along with the other peripherals and audio equipment), so this is the best compromise for now.</p>
<p><img src="/img/setup/musicroom-monitors.jpg" alt="Music Room Monitors"></p>
<p><strong>Update (11/05/2021)</strong>: After replacing the the P2419H in the office with a P2421DC, I decided to use the P2419H in the music room. I also bought another P2419H to maintain the dual monitors, and have sold all of the AOC monitors now. They were great monitors, but they were starting to show their age.</p>
<p>The P2419H displays, while being 1080p, look a lot nicer due to being IPS. The bezels are much smaller too, meaning the display panels are closer together too.</p>
<p><img src="/img/setup/musicroom-monitors-update.jpg" alt="Music Room Monitors - Update"></p>
<h3 id="keyboards">Keyboards</h3>
<p>Around 2016 I got bitten by the mechanical keyboard bug. Another I can blame on Reddit. Previously I was happy with any keyboard, but now membrane/rubber dome keyboards feel oddly squishy to me. It was probably better when I never knew the difference!</p>
<p>My main keyboard in the office is a <a href="https://www.zsa.io/moonlander/">ZSA Moonlander</a> with Cherry MX Brown switches, which I purchased at the end of 2020. I am still getting used to it, but I love the split design and the linear keys (rather than being staggered like on a standard keyboard). I can also customize every single key (which I have done several times). Now I don&rsquo;t need to get used to a layout, instead being able to make it exactly as I want.</p>
<p><img src="/img/setup/moonlander.jpg" alt="ZSA Moonlander"></p>
<p>My main keyboard in the music room is a Vortex Pok3r, also with Cherry MX Browns. This was my first mechanical keyboard and has held up very well since I bought it. I don&rsquo;t use the layer functionality much, but it is nice for those who require it. The Caps Lock key has been changed to work as a Fn key, as I never use Caps Lock anyway.</p>
<p><img src="/img/setup/pok3r.jpg" alt="Vortex Pok3r"></p>
<p>I also own an original IBM Model M keyboard, and a USB to SDL cable so that it can be plugged in to modern devices. Currently it isn&rsquo;t part of any setup, but I am hoping to rejig the layout in the rooms at some point to accommodate the Model M, as it is a wonderful (and LOUD) keyboard to type on.</p>
<p><img src="/img/setup/modelm.jpg" alt="Model M"></p>
<p>Finally, I have a Drevo Calibur V2 65% keyboard with Cherry MX Red clone (Outemu) switches. This was my main keyboard in my office before replacing it with the Moonlander. It is a very nice keyboard, especially for the price, but right now it isn&rsquo;t used for anything.</p>
<p><img src="/img/setup/drevo.jpg" alt="Drevo Calibur"></p>
<p>I have given away three keyboards recently as well. I had acquired quite a few over the years, namely a Noppoo Spyder, Aukey KM-G9 and a Magicforce 68. It is quite easy to get hooked into mechanical keyboards and getting new ones (especially when trying out different switches/options), so I had accumulated way more than I would ever need!</p>
<h3 id="mice">Mice</h3>
<p>I prefer trackballs over traditional mice. They help with avoiding strain in my arms and wrists when using computers for long periods. I also have a preference toward trackballs that have the ball on the top rather than at the side (i.e. controlled with fingers rather than just the thumb). I found those with trackballs under the thumb eventually led to strain anyway, whereas I get no strain at all using the &ldquo;top&rdquo;-style trackballs.</p>
<p>In my office I use the Elecom M-HT1DRBK, also known as the Elecom Huge. The entire mouse serves as a hand/wrist wrest, and the size works for me as I have large hands. It doesn&rsquo;t feel as premium as some mice I have used, but the shape and functionality makes it worth it for me.</p>
<p>In my music room I use a Kensington Expert Wired trackball. This is a really nice trackball, and will probably last for years. This used to be my main office mouse, but the Elecom Huge replaced it as the shape works a little better for me.</p>
<p>I recently purchased a Microsoft Intellimouse Pro for my office. The primary reason is that <a href="#behemoth">Behemoth</a> is a very capable gaming machine, and for the few occasions I want to play an FPS game (e.g. Quake 3 Arena, Unreal Tournament, Xonotic, Unvanquished, Prodeus) I find trackballs are not well suited. The Intellimouse is essentially a gaming mouse in a non-flashy case, and is large enough for my hands too.</p>
<p>I also have a couple of Kensington Orbit mice (that the Expert and Huge superseded), as well as a few thumb-style trackballs (Logitech MX570, Speedlink Aptico, Elecom M-XT3URBK). I use the Kensington Orbits when I need to use a mouse with a laptop, but otherwise the rest are side-effects of trying to find the right mouse.</p>
<h3 id="docks">Docks</h3>
<p>I currently have docks connected to both the office and music room setups. In the office, I have a Caldigit TS3 Plus. This is primarily for connecting to my work laptop, but I&rsquo;ll also use it with my personal laptop once I purchase one that has Thunderbolt support!</p>
<p><img src="/img/setup/dock.jpg" alt="Dock"></p>
<p>In the music room, I have a Dell <a href="https://www.dell.com/en-uk/work/shop/dell-universal-dock-d6000/apd/452-bcyj/pc-accessories">D6000</a> USB-C dock. As both monitors in the music room are 1080p, I don&rsquo;t need the Thunderbolt 3/4 bandwidth to be able to push high resolution displays.</p>
<p>I am also currently waiting on a <a href="https://pine64.com/product/pinebook-pro-usb-c-docking-deck/?v=0446c16e2e66">Pinebook Pro USB-C Docking Deck</a> to see how it compares against the D6000.</p>
<h2 id="my-network-and-server-setup">My network and server setup</h2>
<p><img src="/img/setup/rack.jpg" alt="Home Rack"></p>
<h3 id="wired-networking">Wired Networking</h3>
<p>I spent many years as a Network Engineer, so I have a tendency towards equipment that is command-line driven and very flexible. The network has multiple VLANs, 802.1x-based Wireless (to assign the correct VLAN based upon the client connecting) and can support BGP and OSPF if I chose to enable it (I&rsquo;m just looking for the right excuse!).</p>
<p>The brains of the network is the <a href="https://mikrotik.com/product/rb4011igs_rm">MikroTik RB4011</a>. The router my ISP (Virgin Media) provides runs in bridge mode, with the MikroTik receiving the public IP from the ISP via DHCP. This is rather than sitting behind NAT from the ISP-provided router (meaning I would need to double NAT everything for traffic to flow back through the router).</p>
<p>The MikroTik has 10 1G ports and a single 10G SFP+ port. One of the 1G ports connects to my core switch, serving as the &ldquo;WAN&rdquo; (i.e. the logical link to the ISP-provided router), and the 10G SFP+ port is connected by a Direct Attach Copper (DAC) cable to the same switch as the &ldquo;LAN&rdquo;, carrying all inter-VLAN traffic.</p>
<p>The default gateway for each VLAN is on the MikroTik router, so all traffic that routes between VLANs, as well as to the internet traverses this device.</p>
<p>The core switch is an <a href="https://www.fs.com/products/72944.html">FS S3900-24T4S</a>. This is a fanless switch with 24 1G ports and 4 10G SFP+ ports. The command line interface is similar enough to Cisco that I can do most configuration on it without referring to the manual (most of my network career was managing Cisco devices, so this is very familiar territory for me!). One of the 10G ports connects to the MikroTik RB4011 router, and another connects to <strong><a href="#behemoth">Behemoth</a></strong> (using multimode fibre and 10G SFP+ fibre transceivers). I intend to add more 10G devices, at the moment it is just so that I can see a 10G link on <code>ethtool</code>!</p>
<p>I also have two <a href="https://www.zyxel.com/uk/en/products_services/8-10-16-24-48-port-GbE-Smart-Managed-Switch-GS1900-Series/">ZyXEL GS1900-8</a> switches, one in the music room and one in the living room. The music room switch is to connect the equipment in there, whereas the one in the living room is mostly to connect the ISP-provided router and one of my wireless access points. Both of these are connected using 1G copper cabling.</p>
<p>I intend to run the cabling between rooms at 10G at some point in the future, but for now 1G is more than enough for each. The switches are basic, but do have VLAN functionality and SNMP so I can monitor them.</p>
<h3 id="wireless-networking">Wireless Networking</h3>
<p>For Wireless I use two <a href="https://www.ui.com/unifi/unifi-ap-ac-lr/">Unifi UAP-AC-LR</a> access points. While they only support 802.11ac rather than 802.11ax, they provide more than enough connectivity for any wireless device in my home.</p>
<p>I run the Unifi controller in a Docker container rather than a Cloud Key. I also run multiple SSIDs, mostly because not all devices support 802.1x (particularly games consoles), and also to provide a guest network for people visiting (although 2020 has stopped that from happening!).</p>
<p>When the time comes to do a refresh on my Wireless setup, I am in two minds as to whether I will stick with Ubiquiti or not. Some of the recent moves in terms of data privacy and removing features gives me cause for concern, but I haven&rsquo;t found anything else at the right price point as of yet. I would gladly use Aruba or Ruckus access points, but they are a little pricey for what is mostly phones, tablets and laptops in this house.</p>
<h3 id="vlans">VLANs</h3>
<p>I run multiple VLANs to keep my network segregated, allowing me to limit what hosts in each VLAN can access: -</p>
<ul>
<li>VLAN1: Management
<ul>
<li>Purpose: Management of physical hardware (servers, access points, switches, routers)</li>
</ul>
</li>
<li>VLAN2:
<ul>
<li>Purpose: Standard Users</li>
<li>Access: Allowed internet access and also to reach the IOT VLAN</li>
</ul>
</li>
<li>VLAN3:
<ul>
<li>Purpose: IOT - Any device that is not a computer that has internet access, e.g. TVs, games consoles, Chromecasts, Rokus etc</li>
<li>Access: Can reach the internet, and DHCP/DNS in the server VLAN. Can receive traffic from other VLANs (except guest)</li>
</ul>
</li>
<li>VLAN4:
<ul>
<li>Purpose: Guest - People visiting or device testing/install (when no other access required)</li>
<li>Access: Completely separate from the rest of the network, doesn&rsquo;t use internal DNS servers, can only reach the internet</li>
</ul>
</li>
<li>VLAN5:
<ul>
<li>Purpose: Engineer Users - Basically me!</li>
<li>Access: Full access to everything</li>
</ul>
</li>
<li>VLAN10:
<ul>
<li>Purpose: Servers - All virtual machines are in this range</li>
<li>Access: Access to most VLANs (due to monitoring/management)</li>
</ul>
</li>
<li>VLAN30:
<ul>
<li>Purpose: Lab - Labbing and testing</li>
<li>Access: Separate VLAN for testing/labbing, no DNS and DHCP running (as I may be testing DNS and DHCP servers)</li>
</ul>
</li>
<li>VLAN999:
<ul>
<li>Purpose: Bridge - The VLAN bridge between the ISP router and the MikroTik</li>
<li>Access: Layer 2 only, not routed, used due to the ISP router and the MikroTik being in different rooms (otherwise it would be plugged in directly to the MikroTik)</li>
</ul>
</li>
</ul>
<p>While this may seem overkill, it does allow me to restrict what devices can talk to what, while also still providing enough connectivity where required. Also, given my Network Engineering background, I may have a bit of a tendency to over-engineer the network sometimes!</p>
<h2 id="servers">Servers</h2>
<p>While I don&rsquo;t run a rack full of Dell/HP/Supermicro servers at home, I do have a few capable machines that run all the infrastructure I currently need.</p>
<h3 id="archspire">Archspire</h3>
<p>Music Link: <a href="https://www.youtube.com/watch?v=t9kosVvlb_M">Archspire - Relentless Mutation</a></p>
<p>One of the servers is an Intel NUC NUC6i5SYH with an Intel i5-6260U, 16Gb of RAM and a 256Gb internal NVME drive. I also have a Western Digital My Book 10Tb drive connected to it for backups, Plex and basic NAS functionality. This server runs Debian Buster. It has a mixture of virtual machines (running on KVM), containers (running in Docker) and native applications (Plex and Syncthing). The 10Tb drive is also exposed via Samba and NFS, allowing direct disk access from machines in my network.</p>
<p>The virtual machines on this server are: -</p>
<ul>
<li>netutil-01 (Debian Buster) - Running a TFTP server, the Unifi controller, <a href="https://github.com/netbox-community/netbox">NetBox</a> (for IPAM) and <a href="https://github.com/ytti/oxidized">Oxidized</a> for network config backups</li>
<li>auth-01 (Debian Buster) - Running RADIUS for the 802.1x wireless authentication</li>
<li>ns-04 (Debian Buster) - DNS and DHCP using BIND9, PiHole (for ad blocking) and isc-dhcp-server</li>
<li>bastion-01 (OpenBSD) - A bastion machine for external SSH access</li>
<li>mdns-01 (Alpine) - An MDNS repeater to allow hosts on the engineers or users VLANs to &ldquo;cast&rdquo; to the streaming devices in our network
<ul>
<li>Most streaming devices (Chromecast, Roku, FireTV) expect a single subnet, at which point they do not cross VLAN boundaries. Using the mDNS repeater allows other VLANs to see the devices and cast without an issue.</li>
</ul>
</li>
<li>db-02 (Ubuntu 20.04) - The secondary for MySQL (for RADIUS and Nextcloud) and PostgreSQL (for Netbox)</li>
<li>config-01 (Debian Buster) - This is where all of the Ansible Playbooks run from, and is also the Salt Master for my infrastructure (all servers and VMs run the Salt Minion)</li>
</ul>
<p>It also runs the following containers: -</p>
<ul>
<li>Statping (for service status)</li>
<li>Airsonic (to share my music collection)</li>
<li>Traefik (for routing HTTP requests)</li>
<li>cadvisor (for container metrics in Prometheus)</li>
<li>promtail (for logs to Grafana&rsquo;s Loki)</li>
</ul>
<p>This connects directly to the core switch in the office at 1Gb. I recently experimented with using a USB-connected 1G NIC to bond 2x1G ports with LACP, but it appears my VLAN configuration and bonding didn&rsquo;t work well together. I will probably rework this at some point to fix this.</p>
<p>This also runs: -</p>
<ul>
<li>Plex (for media)</li>
<li>Syncthing (for internal file syncing)</li>
</ul>
<p><img src="/img/setup/nuc.jpg" alt="Archspire"></p>
<h3 id="pink-floyd">Pink Floyd</h3>
<p>Music Link: <a href="https://www.youtube.com/watch?v=7jMlFXouPk8">Pink Floyd - High Hopes</a></p>
<p>The other server is a 2012 Mac Mini with an Intel i7-3615QM, 16Gb of RAM and a 1Tb internal SSD drive. This machine used to be my main desktop workstation in the music room, originally MacOS only (technically OS X, as the name change hadn&rsquo;t happened yet when I bought this machine). Around a year ago I installed Ubuntu MATE on it alongside MacOS, allowing me to get my Linux workflow for podcasting how I wanted it.</p>
<p>When <strong><a href="#symphony-x">Symphony X</a></strong> became superfluous for labbing purposes, I decided to make that my music room workstation, replace MacOS entirely with Debian Buster (as I could no longer install the latest MacOS anyway) on the Mac Mini and move all of the virtual machines, containers and other applications from <strong><a href="#meshuggah">Meshuggah</a></strong> to <strong>Pink Floyd</strong></p>
<p>Despite not being much higher in CPU generation, the machine being an i7 (with 4 cores and 8 threads) compared to <strong><a href="#meshuggah">Meshuggah&rsquo;s</a></strong> i5 (with 2 cores and 4 threads) and the much better SSD (Samsung EVO compared to Kingspec) made all the machines and containers much more performant. This also meant I could run <strong>Nextcloud</strong> on here as well (with the additional storage capacity and speed available) without taxing the machine too much. Previously <strong>Nextcloud</strong> was running on <strong><a href="#dream-theater">Dream Theater</a></strong> (a Raspberry Pi 4), whereas now it doesn&rsquo;t require a dedicated device.</p>
<p>The virtual machines on this server are: -</p>
<ul>
<li>auth-02 (Debian Buster) - Secondary RADIUS server, in the event that auth-01 fails</li>
<li>db-01 (Ubuntu 20.04) - Primary MySQL and PostgreSQL server</li>
<li>git-01 (Alpine) - Runs Gitea, for my internal repositories</li>
<li>ns-03 (Ubuntu 20.04) - The backup DNS and DHCP server</li>
<li>vpn-01 (Debian Buster) - Wireguard VPN server for external VPN access to my network</li>
<li>gitlab-01 (Ubuntu 20.04) - Running Gitlab, will eventually replace git-01</li>
</ul>
<p>It also hosts the following containers: -</p>
<ul>
<li>Grafana (monitoring dashboard)</li>
<li>Traefik</li>
<li>cadvisor</li>
<li>promtail</li>
</ul>
<p>In addition, this machine also runs: -</p>
<ul>
<li>Nextcloud (replaced Dropbox)</li>
<li>Prometheus (Monitoring my infrastructure)</li>
</ul>
<p><img src="/img/setup/pinkfloyd.jpg" alt="Pink Floyd"></p>
<h3 id="monitoring-and-management">Monitoring and management</h3>
<p>As you may have seen if you follow my articles, I love Prometheus. Each server and virtual machine has the Prometheus <code>node_exporter</code> installed to get retrieve CPU, memory, storage and network statistics. I also run a number of other exporters for various applications including: -</p>
<ul>
<li><code>blackbox_exporter</code> - For a number of <strong>black box</strong> style metrics (i.e. ICMP, HTTP, DNS, TCP checks on external services)
<ul>
<li>I also use the TCP checks for streaming devices (Roku and Chromecast) to monitor their availability</li>
</ul>
</li>
<li><code>cadvisor</code> - For container metrics (network, storage, CPU etc)</li>
<li><code>dhcp_exporter</code> - For metrics on DHCP leases</li>
<li><code>libvirt_exporter</code> - For metrics on my virtual machines</li>
<li><code>mikrotik_exporter</code> - For metrics from my MikroTik RB4011 router</li>
<li><code>mysql_exporter</code> - Metrics from the database servers</li>
<li><code>named_exporter</code> - Metrics from the BIND servers</li>
<li><code>nextcloud_exporter</code> - Metrics from Nextcloud on <strong><a href="#pink-floyd">Pink Floyd</a></strong></li>
<li><code>pihole_exporter</code> - Metrics on blocked/allowed domains</li>
<li><code>plex_exporter</code> - Plex-based metrics (active watchers, size of collection etc)</li>
<li><code>postgres_exporter</code> - PostgreSQL metrics from the database servers</li>
<li><code>redis_exporter</code> - For Redis metrics (used by both Nextcloud and Netbox)</li>
<li><code>snmp_exporter</code> - For monitoring my core switch and the two ZyXEL switches</li>
<li><code>wireguard_exporter</code> - For monitoring my VPNs to and from the network</li>
</ul>
<p>The Prometheus configuration itself is quite small, as I also use Hashicorp&rsquo;s Consul for service discovery. This means that rather than statically configuring all of the exporters and what machine they run on, the machines themselves register with Consul, and inform the Consul servers what services they are running. I cover this in a bit more detail <a href="/posts/prometheus-consul-node_exporter/">here</a></p>
<p>All of the services, exporters, Consul configuration, DNS/DHCP configuration, SSH key management and more are managed with a mixture of Ansible and Saltstack. I run both mainly because I like both tools, and like to keep fresh on both. Saltstack has a speed advantage once it is up and running (in my experience) but Ansible is quicker to start using straight away.</p>
<p>I also run Prometheus&rsquo;s Alertmanager, with most of the alerts being sent to a Slack channel on a personal Slack instance. Nobody else but me uses it, so I never have to worry about losing history (or at least not for a long time anyway). I am tempted to move this to Discord or Matrix, but no native integrations exist for these as of yet.</p>
<p>I also use some alerts in Grafana, but have gradually moved them over to Alertmanager, as it is much more flexible at defining thresholds, conditions, complex queries and conditions.</p>
<h3 id="the-whys">The whys</h3>
<p>There are a few questions I would expect to be asked of this setup: -</p>
<ol>
<li>Why do you run so many different virtual machines?</li>
<li>Why do you run some in containers, some in virtual machines, and some natively on the servers?</li>
<li>Why do you need a monitoring system for all of this?</li>
<li>Why bother with all of this?</li>
</ol>
<h4 id="why-so-many-vms">Why so many VMs?</h4>
<p>To answer the first, there are a few reasons. The primary reason, if I&rsquo;m being realistic, is because trying to learn something like Prometheus, Consul, Saltstack, Ansible or anything else with only a machine or two doesn&rsquo;t teach you as much as trying to run it on many machines. The infrastructure is as much as about learning as it is about providing services for my home.</p>
<p>For similar reasons, I run different operating systems (Debian, Ubuntu, OpenBSD and Alpine) because it means that my Ansible and Saltstack configuration needs to cater to different systems (rather than being tailored to one, and never working on anything else).</p>
<p>I also do appreciate the lightness of Alpine when it makes sense, Ubuntu for the software availability, Debian for the stability and OpenBSD for the wonderful documentation and focus on security/networking. I can choose the system which works best for the purpose, rather than trying to make a certain system fit where it doesn&rsquo;t quite match up.</p>
<h4 id="containers-vms-native-processes">Containers, VMs, native processes?</h4>
<p>The reason for this is because some of the software is packaged as containers (e.g. Grafana), and updating is really straightforward. I rarely, if ever need to change the running configuration of Grafana, so running it in a container works really well.</p>
<p>Something like Ansible or Saltstack though is well suited to being on a virtual machine I can login to. I could setup a CI/CD pipeline to roll out changes, but that is probably a step too far for my infrastructure. Logging in to the virtual machine to update my Playbooks/States is a good compromise here.</p>
<p>For native processes, Prometheus especially makes a lot of sense not being a virtual machine, as if for some reason my virtualisation fails (due to configuration changes or otherwise), then the monitoring carries on functioning (and bombarding me with notifications to fix it!).</p>
<p>Eventually I aim to move to some form of container orchestration (Kubernetes, K3S or similar), but this is a long term goal rather than anything I will be doing soon.</p>
<h4 id="why-a-monitoring-system">Why a monitoring system?</h4>
<p>Originally, it was because one of the streaming devices in the house was dropping off the network regularly but I couldn&rsquo;t narrow down the issue (see <a href="https://yetiops.net/posts/prometheus-the-exporter-life/">here</a>). Eventually it became apparent that there were issues with using Powerline adapters (random dropouts) and the location was a bit of a wireless deadspot. The move to Unifi Access Points solved these issues.</p>
<p>In the process though, I fell in love with Prometheus as a technology, and decided to keep it. Any time I find out something new about Prometheus, I can improve my home setup as well.</p>
<p>In short, the answer is &ldquo;Because I can&rdquo; 😁</p>
<h4 id="why-bother">Why bother?</h4>
<p>Similar to the previous, none of this is required, I just enjoy it. There is always something to learn. Also there is no SLA better than your own family for making you fix the issues you just caused when trying something out new&hellip;</p>
<p>I&rsquo;ve learned a lot about Prometheus, Consul, Ansible and Saltstack with all of this, which is worth it for me. Is it for everyone? Not at all. Is it for me? Definitely.</p>
<h2 id="audio">Audio</h2>
<p>As mentioned previously on this site, I have some background in Sound Engineering/Music Technology. It never became my career, but it did mean that when it came to starting with Podcasting, I wasn&rsquo;t starting fresh.</p>
<p>In my music room, I have a Behringer UMC404HD audio interface, a Rode NT1-A microphone and a Rode PSA1 boom arm for the microphone. I use a set of Audio Technica M50x over-ear headphones for monitoring the podcasts (and just general listening). I also have a Onkyo A-9155 amplifier and Mission MX-1 speakers for when I want to play something at volume too.</p>
<p>When I&rsquo;m podcasting, this is generally what I&rsquo;m seeing: -</p>
<p><img src="/img/setup/rode.jpg" alt="Rode NT1-A"></p>
<p>In my office, I have a Focusrite Scarlett 2i2 audio interface, a Pyle PDMIC78 microphone (a Shure SM57 clone) and a generic boom arm. The boom arm used to be in my music room, but the weight of the NT1-A meant that it would slowly drop to my desk during recording. The Pyle microphone is much lighter, meaning no such problems. I also have a set of Beyerdynamic DT100 headphones that I have had for 15 years for monitoring audio via this interface. Finally, I have a set of
Edifier R1850DB speakers for when I want something loud to listen to when working/coding/labbing/gaming.</p>
<p>Last but not least, I have a Jabra Evolve2 65 Wireless Headset for work calls and music when I&rsquo;m not the only person in the house.</p>
<h2 id="applications-and-workflow">Applications and workflow</h2>
<p>A lot of my work is based around the terminal and CLI applications. While I am not immune to a a good GUI experience (hence why I used MacOS for so many years), I still tend to prefer working in the terminal for most of what I do.</p>
<h3 id="shell">Shell</h3>
<p>I use <code>zsh</code>, with <code>powerlevel10k</code> as a theme on workstations (as seen in the Desktops/Laptops sections) and <code>gianu</code> on servers (example <a href="https://github.com/ohmyzsh/ohmyzsh/wiki/Themes#gianu">here</a>). I use different themes just to give myself some visual separation in terms of what I am working on (i.e. my local machine or a server/virtual machine).</p>
<p>I prefer <code>zsh</code> over <code>bash</code> mainly because of the syntax highlighting, history substring search (i.e. type the first part of a command and you will be given options from your history for completion) and autocompletions (especially useful for <code>git</code>, <code>kubectl</code>, <code>terraform</code> etc). I find <code>zsh</code> is close enough to <code>bash</code> that a lot of my quick <code>awk</code>, <code>sed</code>, <code>for</code> and <code>xargs</code> one-liners work without issue (mainly for quick text/output manipulation). I previously used the <code>fish</code> shell, but the difference between native <code>bash</code> and <code>fish</code> caused a lot of errors with my one-liners.</p>
<p>For the terminal itself, I use either <code>termite</code> when on <strong>i3</strong> (although I am evaluating <strong>alacritty</strong> right now), the standard <strong>MATE</strong> terminal when on Ubuntu MATE, and iTerm when on MacOS.</p>
<h3 id="text-editor">Text editor</h3>
<p>I use <code>vim</code> for nearly all of my text editing (including writing the articles on this site). One of my first jobs in tech (around 12 years ago) required editing files on machines with SCO or HP-UX Unix, and only had <code>vi</code> on them (no <code>nano</code> or anything similar). This required learning some of the <code>vi</code> commands quickly, as otherwise I wouldn&rsquo;t have been able to do my job. Since then, I have found that I can move around, replace text, jump to different parts of files/lines and more far quicker than I can in anything else. That <code>vi</code> is installed on pretty much every system I manage also helps.</p>
<p>I customize my ViM experience with the following: -</p>
<ul>
<li><code>airline</code> - For a nice looking status bar</li>
<li><code>nerdtree</code> - I don&rsquo;t use it much, but it is nice have a directory structure when required</li>
<li><code>challenger-deep</code> theme - I just like the way it looks!</li>
<li><code>vim-go</code> - This does linting and syntax checking for Golang</li>
</ul>
<p><a href="/img/setup/vim.png"><img src="/img/setup/vim.png" alt="ViM Setup"></a></p>
<p>I am also experimenting with <code>coc.nvim</code> (see <a href="https://github.com/neoclide/coc.nvim">here</a>) for syntax completion and linting. I currently use it with Golang, Python, YAML, Terraform, Markdown and Shell, while also using the Git extension to show Git status inside of files.</p>
<p>I do sparingly use Sublime Text and Visual Studio Code. I like Sublime Text for it being quick to start and having the last bit of text I wrote in it (useful for quick notes, or quickly copying and pasting configuration sections on network hardware), and I will sometimes use Visual Studio Code for quick previews/conversion to PDF of Markdown files. Besides that, almost my entire text/configuration/coding workflow is in ViM.</p>
<h3 id="dotfiles">Dotfiles</h3>
<p>My dotfiles were previously all managed with a Bash script, however it wasn&rsquo;t very idempotent. Subsequent runs would create errors, and had no true checks for whether a file/application existed before trying to reinstall it. It catered for the following operating systems: -</p>
<ul>
<li>Debian-based Linux distributions (Debian, Ubuntu, Raspbian)</li>
<li>RHEL-based (CentOS, Fedora, Amazon Linux)</li>
<li>Arch-based (Arch, Manjaro, Endeavour)</li>
<li>Clear Linux</li>
<li>Alpine Linux</li>
<li>Void Linux</li>
<li>OpenSUSE</li>
<li>OpenBSD</li>
<li>FreeBSD</li>
<li>Solaris/illumos</li>
<li>Darwin/MacOS</li>
</ul>
<p>I am currently migrating this to use Ansible. A lot of the logic I put in place (distribution checking, packaging installs, templating, file downloads) can be covered with native Ansible modules, while also making it quite straightforward to add feature flags, additional modules, and idempotence.</p>
<p>Currently it works on all of the above operating systems except: -</p>
<ul>
<li>Clear Linux - I use it so rarely that it doesn&rsquo;t matter anyway</li>
<li>Void - The <code>xbps</code> package installs don&rsquo;t seem to work correctly in Ansible right now</li>
<li>illumos - The support for package installs isn&rsquo;t perfect</li>
<li>Darwin/MacOS - I&rsquo;m moving away from MacOS so&hellip;</li>
</ul>
<p>I intend to make at least Void work at some point, and maybe the others. However in reality as long as it works on Debian-based, RHEL-based, Arch-based, Alpine and Open/FreeBSD, I have covered most/all of the systems I run. The others are just more out of curiosity.</p>
<p>In the near future I will put an article together on this, including how to manage multiple user&rsquo;s configurations.</p>
<h3 id="other-shell-applications">Other shell applications</h3>
<p>I use <code>tmux</code> from time to time, mostly when labbing. This is especially useful when managing network hardware. I don&rsquo;t use it all the time (a lot of my work now is committing to repositories and running CI jobs, or managing immutable infrastructure like containers), but for labbing it is wonderful.</p>
<p>I sometimes use the <code>fish</code> shell, mainly for labbing machines. This is so that the machine &ldquo;feels&rdquo; different (especially useful when replicating a production service!), but also has the syntax highlighting and history search that <code>fish</code> provides. If I installed <code>zsh</code> on every lab machine as well, I would find it too easy to forget I&rsquo;m not on a lab machine and end up breaking something in my network!</p>
<p>I use a lot of the Hashicorp tools for work and labbing, especially Terraform. I also make a lot of use of Packer and Consul. I am starting to get into using Vault more (I may run my own Vault server at home, as secret management for labs is becoming unwieldy), and I am finally making use of Vagrant after years of not really having a use case for it. Putting together my Ansible-based Dotfiles playbooks has been so much easier using Vagrant to quickly spin up and check that they work, rather than having to have dedicated VMs running at all times.</p>
<p>I use Kubernetes and containers at work and at home, so the Kubernetes CLI tools and Docker are a must. I am looking to move away from Docker (to Podman and/or CRI-O), but for the moment Docker is where I am still at.</p>
<p>Finally, I also use a few other tools like the <code>aws</code> CLI (along with <code>aws-mfa</code> for two-factor authentication), <code>bpytop</code> (for a pretty system resource usage view) and the standard Linux/Unix toolset (<code>awk</code>, <code>sed</code>, <code>xargs</code>, <code>cut</code>, <code>tr</code> etc).</p>
<h3 id="desktop-environments">Desktop Environments</h3>
<p>I tend to use two styles of desktop environment: -</p>
<ul>
<li>Tiling window managers like <strong>i3</strong> for machines that are either lower in resource requirements, or for high productivity</li>
<li>Standard desktop environments (Usually the <strong>MATE</strong> desktop) for machines that are used for &ldquo;play&rdquo; or &ldquo;creative&rdquo; workflows</li>
</ul>
<p>For example, my main desktop uses <strong>i3</strong> because while it serves as a gaming machine as well, the primary focus on it is my coding, labbing and infrastructure management. I find the <strong>i3</strong> desktop very well suited to this, as it is very keyboard driven. I very rarely need to touch a mouse when using this machine (unless I&rsquo;ve fired up <a href="https://quake.fandom.com/wiki/Q3DM17:_The_Longest_Yard">The Longest Yard on Quake 3</a>). Even in my early years with computers, I always used keyboard shortcuts rather than the mouse, so using <strong>i3</strong> (and other tiling window managers like <strong>dwm</strong>) feels like a natural progression.</p>
<p>The desktop that is in my music room is centred around creative/play endeavours. This could be podcasting, playing games with son, playing guitar or some light video editing. This means that the speed of my workflow is less of a concern, and more about the ease of using it. While I do love the workflow that a tiling window manager provides, I find that it doesn&rsquo;t translate as well (for me) to these use cases. Also if my son wants to do anything on the machine, I don&rsquo;t want to have to give him an <strong>i3</strong> cheat sheet to be able to start Minecraft!</p>
<p>I may be leaving MacOS, but I still do appreciate the ease of the workflow when it comes to audio and recording. Because of this, my &ldquo;standard&rdquo; desktop environment veers towards the MacOS/ElementaryOS style of a title bar at the top of the screen and a dock at the bottom. With Ubuntu MATE specifically, I use the Pantheon layout.</p>
<h3 id="other-applications">Other applications</h3>
<p>The other applications I use on a GUI are: -</p>
<ul>
<li>Firefox or Brave for a browser
<ul>
<li>I am stuck between the two, as Brave has better support across the web (due to the Chromium backend) but I prefer Firefox. Unfortunately I don&rsquo;t know how much longer there will be a choice, so I am readying myself for the Chromium-based domination of all browsers.</li>
</ul>
</li>
<li>Bitwarden for password management</li>
<li>Syncthing and Nextcloud for file synchronization</li>
<li>Wireguard for VPNs</li>
<li>Protonmail for email</li>
<li>Spotify for streaming music
<ul>
<li>I am still undecided on a desktop music player on Linux</li>
</ul>
</li>
<li>Trello for task management
<ul>
<li>I would use something open source here, but I also have shared boards with family and friends so I&rsquo;d need them to migrate too!</li>
</ul>
</li>
<li>Backblaze for offsite backups, using <code>rclone</code></li>
<li>Guitar Pro or TuxGuitar for writing/reading guitar tabs and music</li>
<li>Reaper or Ardour for recording music</li>
<li>Audacity and Mumble for podcasting</li>
<li>KDEnlive for video editing</li>
</ul>
<h4 id="why-syncthing-and-nextcloud">Why Syncthing and Nextcloud?</h4>
<p>I use both Syncthing and Nextcloud for slightly different purposes.</p>
<p>Syncthing is used to synchronize directories between my machines (e.g. using the same Downloads/Documents/Pictures/Podcasts/Music/Video folders across all machines), meaning if I download something on one machine, it appears on all others I&rsquo;m syncing with. The same is true of videos I edit, pictures and temporary text files that I&rsquo;m working with (command output that I&rsquo;m manipulating, configuration files I&rsquo;m working with etc). This is a trick I picked up from <a href="https://www.youtube.com/watch?v=UpaOd_9ZSAI">Jay Lacroix of LearnLinux.tv</a> and means I don&rsquo;t need to work out which machine I downloaded a file on, or edited a file on or similar.</p>
<p>For Nextcloud, I use it the way I used to use Dropbox, which is just an easy way to access files on multiple devices (i.e. phones, tablets, laptops) and also for easy sharing with other people. Syncthing could be used for this, but I previously used Dropbox for so many years that the workflow is still imprinted on my brain!</p>
<h2 id="musical-instruments">Musical instruments</h2>
<p>For those who came for the tech and computers, this section is probably not of interest. However I enjoy playing guitar (and other instruments) as much as I like tech, so it felt right to include it all in here too.</p>
<p>My twenties were almost entirely defined by guitars, bands and playing gigs, tech was just what paid for it all!</p>
<h3 id="guitars">Guitars</h3>
<p>My favourite music to play is metal (mainly progressive, tech death or death metal) so you&rsquo;ll notice most of my guitars are geared towards that. I also prefer extended range instruments (particularly 7 string guitars) as they fit with my writing style better. For over a decade I didn&rsquo;t own a 6 string guitar (until a few years ago).</p>
<h4 id="espltd-m207">ESP/LTD M207</h4>
<p>Bought: 2002<br>
Tuned: BEADGBE<br>
Pickups: Bridge - Entwistle Dark Star, Neck - Stock Duncan Designed</p>
<p>This was my first 7 string guitar, bought within my first year of playing. Strange to think I&rsquo;ve had this for nearly two decades!</p>
<p>I have upgraded it a lot over time, with new pickups (it had a Bareknuckle Nailbomb in at one stage as well), new tremolo, new tuners, pickup switcher and all of the electronics. However the body, neck and neck pickup are still the same.</p>
<p><img src="/img/setup/m207.png" alt="ESP/LTD M207"></p>
<h4 id="jackson-rhoads-rr7r">Jackson Rhoads RR7R</h4>
<p>Bought: 2008<br>
Tuned: Not tuned currently&hellip;.<br>
Pickups: Warman Deadly Sinners</p>
<p>This was my second 7 string, and started the path to me getting rid of all my 6 string guitars and become 7+ string only for a decade.</p>
<p>Unfortunately this guitar has been through a lot, with part of the headstock getting snapped off (fixed back together, but still a shame). Around 5-6 years ago, I removed all of the electronics, pickups and strings ready to overhaul the guitar, but time constraints, shifting priorities and house moves stopped that.</p>
<p>Ever since, it has unfortunately been left in it&rsquo;s current state. I do intend to get back to sorting the electronics and restoring it to it&rsquo;s former glory, I just don&rsquo;t know when!</p>
<p><img src="/img/setup/rhoads.png" alt="Jackson RR7R"></p>
<h4 id="ibanez-xiphos-xpt707">Ibanez Xiphos XPT707</h4>
<p>Bought: 2011<br>
Tuned: BEADGBE<br>
Pickups: Dimarzio D-Activators (Bridge and Neck)</p>
<p>This guitar was purchased from a friend, who had also purchased it from another friend.</p>
<p>This became my main guitar in both bands I was in at the time, and is an absolute monster. The sound is huge, and it was because of this guitar that I ended up putting Dimarzio D-Activators in a lot of my other guitars too.</p>
<p>Other than adding straplocks, I have done nothing but change strings and clean it. Wonderful guitar.</p>
<p><img src="/img/setup/xiphos.png" alt="Ibanez Xiphos XPT707"></p>
<h4 id="bc-rich-stealth-7-marc-rizzo-signature">BC Rich Stealth 7 (Marc Rizzo signature)</h4>
<p>Bought: 2012<br>
Tuned: ADGCFAD <br>
Pickups: Bridge - Dimarzio X2N, Neck - Nothing, removed</p>
<p>This guitar was purchased during my obsession with Chuck Schuldiner (of <a href="https://www.youtube.com/watch?v=8256VJ4hkJU">Death</a> and <a href="https://www.youtube.com/watch?v=32m0zj4kaRk">Control Denied</a>). While Chuck never played 7 strings, this was the closest I would come and have a guitar I&rsquo;d actually use!</p>
<p>This is tuned one step below 7 string standard (to match Chuck&rsquo;s one step below 6 string standard), has the neck pickup removed, and the X2N is wired directly to the volume control and output jack.</p>
<p><img src="/img/setup/bcrich.png" alt="BC Rich Stealth 7"></p>
<h4 id="ibanez-prestige-rgd2127z">Ibanez Prestige RGD2127Z</h4>
<p>Bought: 2013<br>
Tuned: GCFA#D#GC<br>
Pickups: Bridge - Dimarzio D-Activator, Neck - Stock Ibanez V77</p>
<p>This guitar is probably my favourite to play out of all of my collection, and has a wonderful sound to it. The Edge Zero 7 tremolo is also one of the most stable and easy to work with tremolos I&rsquo;ve used.</p>
<p>I have this tuned down two steps from 7 string standard, with some of my favourite songs I&rsquo;ve written came from playing this guitar.</p>
<p><img src="/img/setup/rgd.png" alt="Ibanez RGD2127z"></p>
<h4 id="jackson-js32-8q-dinky">Jackson JS32-8Q Dinky</h4>
<p>Bought: 2013<br>
Tuned: F#BEADGBE<br>
Pickups: Bridge - Dimarzio D-Activator, Neck - Stock</p>
<p>I bought this guitar due to it being easier to use than the 8 string I already had (an Agile Intrepid Pro 828), and I ended up selling the Agile because of it.</p>
<p>I am more of a 7 string player than an 8 string player, but I do like the extra range every so often. One of my favourite songs I&rsquo;ve written (prior to owning the Agile) was already in the key of F# Harmonic Minor, so it worked instantly with the extra range.</p>
<p><img src="/img/setup/jackson8.png" alt="Jackson JS32-8Q"></p>
<h4 id="ibanez-rg7321">Ibanez RG7321</h4>
<p>Bought: 2014<br>
Tuned: AEADGBE<br>
Pickups: Bridge - Entwistle Dark Star, Neck - Stock Ibanez</p>
<p>I bought this guitar as part of trading in a guitar I didn&rsquo;t use much. I replaced the bridge pickup with an Entwistle Dark Star (a wonderful sounding pickup from a UK-based pickup manufacturer), and also added a mirrored pickguard.</p>
<p>My wife also bought me a feather boa guitar strap for it, which I think improves it!</p>
<p><img src="/img/setup/rg7321.png" alt="Ibanez RG7321"></p>
<h4 id="charvel-so-cal-pro-mod">Charvel So-Cal Pro Mod</h4>
<p>Bought: 2018<br>
Tuned: EADGBE<br>
Pickups: Seymour Duncan Distortions (Bridge and Neck)</p>
<p>This was the first 6 string I bought in a decade, and came right around the time I got back into 80s rock and metal quite heavily.</p>
<p>This guitar is wonderful to play and sounds outstanding. It might not be one of the original 1980s Charvels, but it plays so well I don&rsquo;t care!</p>
<p><img src="/img/setup/charvel.png" alt="Charvel So-Cal Pro Mod"></p>
<h4 id="fender-stratocaster-partscaster">&ldquo;Fender&rdquo; Stratocaster (Partscaster)</h4>
<p>Bought: 2020<br>
Tuned: EADGBE<br>
Pickups: Bridge - Entwistle Dark Star, Middle and Neck - Entwistle XS-62N</p>
<p>A friend of mine (who I used to be in two bands with) sold this guitar to me last year, and it is a dream to play. It isn&rsquo;t a real Fender, instead made from parts sourced from multiple locations.</p>
<p>The neck is also scalloped (a la <a href="https://www.youtube.com/watch?v=eK0rvReE-4c">Yngwie Malmsteen</a> and <a href="https://www.youtube.com/watch?v=qu8HiZepRWo">Ritchie Blackmore</a>), meaning when you press down the strings behind the fret, you don&rsquo;t actually touch the wood of the fretboard. This makes it easy to go out of tune if you are heavy handed, but also means that bends and vibrato are easier due to no friction from the fretboard.</p>
<p><img src="/img/setup/strat.png" alt="Partscaster"></p>
<h3 id="other-instruments">Other instruments</h3>
<p>I also dabble in a few other instruments, namely bass, ukulele, banjo, and sometimes keyboards/piano.</p>
<p>I got my first ukulele about 12-13 years ago, and while it isn&rsquo;t my main instrument, it is nice to make noise with. The kids also love playing it too!</p>
<p><img src="/img/setup/ukulele.jpg" alt="Ukulele"></p>
<p>I play bass sometimes, although mainly for recording music. I&rsquo;m no Victor Wooten, Sean Malone or Billy Sheehan, but I do okay on bass when I need to. I own an OLP MM5 5-string Stingray bass (the pre-Sterling officially licensed Musicman copy brand) that I bought about 15 years ago, and it has served me (and the bassist in one of my previous bands) well when we needed some low end fun.</p>
<p><img src="/img/setup/olp-mm5.jpg" alt="Bass"></p>
<p>I dabble in banjo, although other than a clunky version of Duelling Banjos and some random tunes I&rsquo;ve put together, I have not played this as much as I would have liked. Still, it is nice when I want a bluegrass edge to what I am doing.</p>
<p><img src="/img/setup/banjo.jpg" alt="Banjo"></p>
<p>I also own a Novation Launchkey 49 for when I want to play some keyboards, synths or piano. Right now I don&rsquo;t have the space to have it set up and playable at all times, but hopefully in the future there will be a place for it and I can resurrect my basic keyboard skills!</p>
<h3 id="amplifiers">Amplifiers</h3>
<p>My main amplifier is an Engl Invader 150. I bought this in 2008 and instantly found the tone I&rsquo;d been searching for since I started playing guitar. I have owned, played and borrowed many other amplifiers over the years, but none feel as &ldquo;right&rdquo; to me as this one does. It is paired with an Engl Standard 4x12, with Celestion V60 speakers.</p>
<p>This amplifier has been with me for every gig I&rsquo;ve played since I bought it, and has never caused me any reliability concerns or problems. To say I&rsquo;m glad I bought it would be an understatement!</p>
<p>The below photo is from the day I purchased it (with the amp it replaced, an Engl Thunder 50, on top): -</p>
<p><img src="/img/setup/engl.jpg" alt="Engl Invader 150"></p>
<p>I also own a Marshall Valvestate 8100 head. This was purchased due to my obsession with Chuck Schuldiner, and while it does need some repair work (the volume can cut out every so often), the sound is wonderful, especially for what worked out to be the £30 I paid for it. I bought it for £100 including a 4x12 speaker cabinet and sold the 4x12 at a later date for £70. Bargain!</p>
<p><img src="/img/setup/marshall8100.jpg" alt="Marshall 8100"></p>
<p>Finally, I own a Hotone Purple Wind that my wife bought me for my birthday a few years go. This has a slight Marshall Plexi-style tone to it, which when fronted with an overdrive can get tones like Yngwie Malmsteen, Eddie Van Halen or even Brian May at a push.</p>
<h3 id="effects">Effects</h3>
<p>For effects, I have used a Line 6 HD500 for about a decade now in conjunction with my Engl Invader. While the Line 6 would often be used as a standalone device (with the amplifier models), I use it mainly as a very good multieffects device. With my Engl also having MIDI capabilities, I can switch presets and channels on my amplifier at the same time with a single button press.</p>
<p>I have tried to do the whole pedalboard/tap dancing thing in the past, but I often want to change between a lot of effects in quick succession (e.g. reverb/delay and maybe chorus on a clean channel, to just my rhythm sound with no effects, to a lead channel with delay and the option of harmony/wah). I would always end up almost falling over trying to use a traditional pedalboard, so I have always strived for a one-stomp style setup.</p>
<p>As the HD500 is over a decade old now, and at the time the effects algorithms were not the latest and greatest, I am considering replacing this at some point in the near future. Whether this is with something like a Fractal Audio AxeFX, Neural DSP Quad Cortex or something else entirely, I am not sure.</p>
<p>I do own some other effects as well for playing with/recording, with my favourites being: -</p>
<ul>
<li>Mr Black Eterna shimmering reverb</li>
<li>Homebrew Electronics Germania treble booster (for my best Brian May impression)</li>
<li>Greg Covington Fireplace treble booster prototype (that I found for sale in a guitar shop mostly unlabelled!)</li>
</ul>
<p>I also own a couple of slides (for when the blues hits) and an eBow for when I want to pretend I own a violin.</p>
<h2 id="summary">Summary</h2>
<p>While this type of post isn&rsquo;t my usual technical affair, I hope you enjoy reading it. As I mentioned at the beginning, I always enjoy seeing peoples setups, whether to get ideas, enjoy the aesthetics or simply see a personal side of what is going on behind the work.</p>
]]></content>
        </item>
        
        <item>
            <title>Kubernetes - Building a Mixed Linux and Windows Cluster using Packer, Terraform, Ansible and KVM - Part 3: Running Applications</title>
            <link>https://yetiops.net/posts/kubernetes-terraform-kvm-linux-windows-part-3/</link>
            <pubDate>Sat, 09 Jan 2021 20:25:36 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/kubernetes-terraform-kvm-linux-windows-part-3/</guid>
            <description>This post is the third and final part in a series on creating a Kubernetes cluster containing both Linux and Windows workers. The first post details building the virtual machine images ready to be configured as Control Plane or Worker nodes. The second post covers initializing the cluster using Terraform and Cloud-Init. This post is on how to deploy applications to the cluster, and how to make them available outside of the cluster.</description>
            <content type="html"><![CDATA[<p>This post is the third and final part in a series on creating a Kubernetes cluster containing both Linux and Windows workers. The <a href="/posts/kubernetes-terraform-kvm-linux-windows-part-1/">first post</a> details building the virtual machine images ready to be configured as Control Plane or Worker nodes. The <a href="/posts/kubernetes-terraform-kvm-linux-windows-part-2/">second post</a> covers initializing the cluster using Terraform and Cloud-Init. This post is on how to deploy applications to the cluster, and how to make them available outside of the cluster.</p>
<h2 id="errata">Errata</h2>
<p>In the last post, we looked at how to use Cloud-Init to initialize members of the cluster. When writing this post, I discovered that one of the lines was incorrect. If you tried to deploy applications on the Windows workers, they could only be reached using a <a href="https://kubernetes.io/docs/concepts/services-networking/service/#nodeport">service of type NodePort</a>, and you knew the IP and port of the node the application runs on.</p>
<p>A single character change in the template for creating the first Control Plane node fixes this: -</p>
<p><strong>Before</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">curl -L https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml | sed <span style="color:#e6db74">&#39;s/vxlan&#34;,/vxlan&#34;,\n        &#34;VNI&#34; : 4096,\n        &#34;Port&#34;: 4789/g&#39;</span> | kubectl apply -f -
</code></pre></div><p><strong>After</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">curl -L https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml | sed <span style="color:#e6db74">&#39;s/vxlan&#34;/vxlan&#34;,\n        &#34;VNI&#34; : 4096,\n        &#34;Port&#34;: 4789/g&#39;</span> | kubectl apply -f -
</code></pre></div><p>This is because the line <code>vxlan</code> in the <code>kube-flannel.yml</code> configuration does not have a comma at the end of it: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">[...]
<span style="color:#66d9ef">kind</span>: ConfigMap
<span style="color:#66d9ef">apiVersion</span>: v1
<span style="color:#66d9ef">metadata</span>:
  <span style="color:#66d9ef">name</span>: kube-flannel-cfg
  <span style="color:#66d9ef">namespace</span>: kube-system
  <span style="color:#66d9ef">labels</span>:
    <span style="color:#66d9ef">tier</span>: node
    <span style="color:#66d9ef">app</span>: flannel
<span style="color:#66d9ef">data</span>:
  <span style="color:#66d9ef">cni-conf.json</span>: <span style="color:#e6db74">|
</span><span style="color:#e6db74">    {</span>
      <span style="color:#66d9ef">&#34;name&#34;: </span><span style="color:#e6db74">&#34;cbr0&#34;</span>,
      <span style="color:#66d9ef">&#34;cniVersion&#34;: </span><span style="color:#e6db74">&#34;0.3.1&#34;</span>,
      <span style="color:#66d9ef">&#34;plugins&#34;: </span>[
        {
          <span style="color:#66d9ef">&#34;type&#34;: </span><span style="color:#e6db74">&#34;flannel&#34;</span>,
          <span style="color:#66d9ef">&#34;delegate&#34;: </span>{
            <span style="color:#66d9ef">&#34;hairpinMode&#34;: </span><span style="color:#66d9ef">true</span>,
            <span style="color:#66d9ef">&#34;isDefaultGateway&#34;: </span><span style="color:#66d9ef">true</span>
          }
        },
        {
          <span style="color:#66d9ef">&#34;type&#34;: </span><span style="color:#e6db74">&#34;portmap&#34;</span>,
          <span style="color:#66d9ef">&#34;capabilities&#34;: </span>{
            <span style="color:#66d9ef">&#34;portMappings&#34;: </span><span style="color:#66d9ef">true</span>
          }
        }
      ]
    }
  <span style="color:#66d9ef">net-conf.json</span>: <span style="color:#e6db74">|
</span><span style="color:#e6db74">    {</span>
      <span style="color:#66d9ef">&#34;Network&#34;: </span><span style="color:#e6db74">&#34;10.244.0.0/16&#34;</span>,
      <span style="color:#66d9ef">&#34;Backend&#34;: </span>{
        <span style="color:#66d9ef">&#34;Type&#34;: </span><span style="color:#e6db74">&#34;vxlan&#34;</span>
      }
    }
[...]
</code></pre></div><p>Using the correct <code>sed</code> expression, this is changed to: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">[...]
  <span style="color:#66d9ef">net-conf.json</span>: <span style="color:#e6db74">|
</span><span style="color:#e6db74">    {</span>
      <span style="color:#66d9ef">&#34;Network&#34;: </span><span style="color:#e6db74">&#34;10.244.0.0/16&#34;</span>,
      <span style="color:#66d9ef">&#34;Backend&#34;: </span>{
        <span style="color:#66d9ef">&#34;Type&#34;: </span><span style="color:#e6db74">&#34;vxlan&#34;</span>,
        <span style="color:#66d9ef">&#34;VNI&#34;: </span><span style="color:#ae81ff">4096</span>,
        <span style="color:#66d9ef">&#34;Port&#34;: </span><span style="color:#ae81ff">4789</span>
      }
    }
[...]
</code></pre></div><p>Without this, the Kubernetes proxy (i.e. the application which forwards traffic to the correct node, regardless of which node the traffic ingresses first) is not able to communicate with the Windows nodes.</p>
<p>The original post has been updated to reflect this, but for anyone who has used it already, ensure you change your Cloud-Init template to match.</p>
<h2 id="tasks">Tasks</h2>
<p>The aim of his post is to run applications on the Linux and Windows workers and expose them outside of the cluster.</p>
<p>We need to make a distinction between applications that run internal to the cluster and those exposed externally. Not every application that runs in a Kubernetes cluster needs to be exposed to user traffic. Examples of this would be: -</p>
<ul>
<li>Monitoring agents (e.g. Prometheus exporters) when the monitoring application (e.g. Prometheus) runs inside the cluster</li>
<li>Webhooks for interacting with/manipulating Kubernetes resources within the cluster (e.g. <a href="https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/">Dynamic Admission Control</a>)</li>
<li>A datastore/API that is only accessed by microservices/containers within the cluster</li>
</ul>
<p>To achieve this we need to: -</p>
<ul>
<li>Be able to manage the cluster using <code>kubectl</code></li>
<li>Ensure that Linux applications run on the Linux workers, and Windows applications run on the Windows workers</li>
<li>Setup an <a href="https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/">Ingress Controller</a>
<ul>
<li>An Ingress object in Kubernetes is what manages external access to a service within the cluster</li>
</ul>
</li>
<li>Setup a Kubernetes-native Load Balancer that exposes Kubernetes Services
<ul>
<li>Specifically, Services that have a type of <a href="https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer">LoadBalancer</a></li>
</ul>
</li>
<li>Run some applications!</li>
</ul>
<p>While it may seem like the Ingress and LoadBalancer steps are achieving the similar goals, there is an important distinction that needs to be made. A LoadBalancer may be able to expose a service, but if every service had an associated LoadBalancer you may run out of resources on your chosen Load Balancer. This could be IPs, services, resource limits, or even money to pay for the Load Balancers!</p>
<p>Ingress objects allow you to route different paths to different services, while sharing the same LoadBalancer endpoint. For example, you could route <code>/important-api</code> to the <code>VeryImportantAPI</code> Kubernetes Service, and the <code>/not-as-important-api</code> to the <code>UnimportantAPI</code> Kubernetes Service, while reusing the same Load Balancer resource.</p>
<p>In most cloud providers, there is a mechanism for automatically creating Load Balancers based upon Ingress objects and Services (e.g. the <a href="https://aws.amazon.com/blogs/opensource/kubernetes-ingress-aws-alb-ingress-controller/">AWS ALB Ingress Controller</a>). As we are running this on KVM though, this is not the case, so we need another way to achieve this.</p>
<h2 id="administering-the-cluster">Administering the cluster</h2>
<p>There are multiple ways to control access to a Kubernetes cluster. For example, if you run a cluster on AWS, you can use the <a href="https://docs.aws.amazon.com/eks/latest/userguide/install-aws-iam-authenticator.html">AWS IAM Authenticator</a>. If you use the Red Hat distribution of Kubernetes (<a href="https://www.openshift.com/">OpenShift</a>), you can use many different methods including single-sign on via LDAP, Oauth2, and much more.</p>
<p>In this instance though, we will just use the <code>admin.conf</code> configuration file generated by the Control Plane. In a production scenario, I would recommend using a more robust method of authentication and authorization, but for lab purposes this is fine.</p>
<p>To use this configuration file, navigate to <code>/etc/kubernetes/admin.conf</code> on one of the Control Plane nodes, copy the contents, and place it in the default Kubernetes configuration location on your machine. For Linux and Mac, this is in the <code>.kube</code> directory in your home directory, named <code>config</code> (i.e. <code>~/.kube/config</code>). On Windows, this would be the same, but using your <code>%USERPROFILE%</code> location (e.g. <code>C:\Users\yetiops\.kube\config</code>).</p>
<p>Download <code>kubectl</code> using <a href="https://kubernetes.io/docs/tasks/tools/install-kubectl/">these</a> instructions and then you should be able to access the cluster: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ kubectl get nodes
NAME              STATUS   ROLES    AGE   VERSION
k8s-cp-01         Ready    master   30h   v1.19.3
k8s-cp-02         Ready    master   30h   v1.19.3
k8s-cp-03         Ready    master   30h   v1.19.3
k8s-winworker-0   Ready    &lt;none&gt;   30h   v1.19.3
k8s-worker-01     Ready    &lt;none&gt;   30h   v1.19.3
k8s-worker-02     Ready    &lt;none&gt;   30h   v1.19.3
k8s-worker-03     Ready    &lt;none&gt;   30h   v1.19.3
</code></pre></div><h2 id="ensure-applications-run-on-the-correct-workers">Ensure applications run on the correct workers</h2>
<p>If you deploy applications to a cluster containing Linux and Windows workers, the Kubernetes control plane will try to schedule these applications on any worker available regardless of operating system. If you are attempting to run a Linux native application, using a Linux container image, you will have failures on scheduling Pods if it is scheduled on your Windows worker. The same is also true for a Windows container being scheduled on a Linux worker.</p>
<p>To avoid this behaviour, we can deploy a <a href="https://kubernetes.io/docs/concepts/containers/runtime-class/">RuntimeClass</a> and a <strong>Taint</strong>.</p>
<p>A RuntimeClass contains two main sections. The first is  a <strong>NodeSelector</strong>. This refers to types of nodes (e.g. nodes running Windows on x86 64-bit) and their associated container runtime. Multiple different container runtimes exist like Docker on Linux, Docker on Windows, <a href="https://katacontainers.io/">Kata Containers</a> and more. Not only that, but workers in the same cluster may run different runtimes. Kubernetes therefore needs to know what workloads are valid for each node type and runtime.</p>
<p>Secondly, it contains a <strong>Toleration</strong>. What this says is that for nodes that match the <strong>Taint</strong> specified in this <strong>Toleration</strong>, any workload that has this RuntimeClass attached is able to run on them. If this RuntimeClass is not attached (or a <strong>Toleration</strong> applied directly) to the workload, then the Kubernetes control plane knows to schedule it on other workers.</p>
<p>The below shows the RuntimeClass we use in this cluster: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">apiVersion</span>: node.k8s.io/v1beta1
<span style="color:#66d9ef">kind</span>: RuntimeClass
<span style="color:#66d9ef">metadata</span>:
  <span style="color:#66d9ef">name</span>: windows<span style="color:#ae81ff">-2019</span>
<span style="color:#66d9ef">handler</span>: <span style="color:#e6db74">&#39;docker&#39;</span>
<span style="color:#66d9ef">scheduling</span>:
  <span style="color:#66d9ef">nodeSelector</span>:
    <span style="color:#66d9ef">kubernetes.io/os</span>: <span style="color:#e6db74">&#39;windows&#39;</span>
    <span style="color:#66d9ef">kubernetes.io/arch</span>: <span style="color:#e6db74">&#39;amd64&#39;</span>
    <span style="color:#66d9ef">node.kubernetes.io/windows-build</span>: <span style="color:#e6db74">&#39;10.0.17763&#39;</span>
  <span style="color:#66d9ef">tolerations</span>:
  - <span style="color:#66d9ef">effect</span>: NoSchedule
    <span style="color:#66d9ef">key</span>: OS
    <span style="color:#66d9ef">operator</span>: Equal
    <span style="color:#66d9ef">value</span>: <span style="color:#e6db74">&#34;Windows&#34;</span>
</code></pre></div><p>This class specifies that: -</p>
<ul>
<li>The nodes in question must be running Windows, on the AMD64 (i.e. 64-bit x86) architecture</li>
<li>They must have the Windows build <code>10.0.17763</code> (the version number for Windows 2019 Server)</li>
<li>A toleration that allows it to run on &ldquo;tainted&rdquo; workers</li>
</ul>
<p>In addition, we must also add a <strong>taint</strong> to the Windows workers themselves. A taint is a way of saying that workloads shouldn&rsquo;t run on a certain node (or nodes). A <strong>toleration</strong> can then be used to say &ldquo;This workload can run on nodes with these taints&rdquo;. The taint that this RuntimeClass would match is applied like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ kubectl taint nodes k8s-winworker-0 OS<span style="color:#f92672">=</span>Windows:NoSchedule
node/k8s-winworker-0 tainted
</code></pre></div><p>This worker is now &ldquo;tainted&rdquo;. Only workloads that tolerate this taint will be allowed to run on it (i.e. those that have our RuntimeClass attached). For any Windows-based workload, we just need to add RuntimeClass to it to ensure it runs on the correct nodes.</p>
<p>To attach the <code>RuntimeClass</code> to the Deployment, you can use something like the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">apiVersion</span>: apps/v1
<span style="color:#66d9ef">kind</span>: Deployment
<span style="color:#66d9ef">metadata</span>:
  <span style="color:#66d9ef">name</span>: iis<span style="color:#ae81ff">-2019</span>
  <span style="color:#66d9ef">labels</span>:
    <span style="color:#66d9ef">app</span>: iis<span style="color:#ae81ff">-2019</span>
<span style="color:#66d9ef">spec</span>:
  <span style="color:#66d9ef">replicas</span>: <span style="color:#ae81ff">1</span>
  <span style="color:#66d9ef">template</span>:
    <span style="color:#66d9ef">metadata</span>:
      <span style="color:#66d9ef">name</span>: iis<span style="color:#ae81ff">-2019</span>
      <span style="color:#66d9ef">labels</span>:
        <span style="color:#66d9ef">app</span>: iis<span style="color:#ae81ff">-2019</span>
    <span style="color:#66d9ef">spec</span>:
      <span style="color:#66d9ef">runtimeClassName</span>: windows<span style="color:#ae81ff">-2019</span>
</code></pre></div><p>This Deployment can now &ldquo;tolerate&rdquo; the previously-specified taint, allowing it to deploy on the Windows workers. It also uses the <code>nodeSelector</code> for scheduling, looking for a 64-bit Windows 2019 Server based worker.</p>
<h3 id="apply-the-runtimeclass">Apply the RuntimeClass</h3>
<p>To apply the RuntimeClass, create a file that contains the RuntimeClass as specified above, and then apply with <code>kubectl apply -f $NAME-OF-FILE.yaml</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ kubectl apply -f win2k19Class.yml
runtimeclass.node.k8s.io/windows-2019 created
</code></pre></div><p>You can check the RuntimeClass with the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ kubectl get runtimeclasses
NAME           HANDLER   AGE
windows-2019   docker    7s

$ kubectl describe runtimeclasses windows-2019
Name:         windows-2019
Namespace:
Labels:       &lt;none&gt;
Annotations:  &lt;none&gt;
API Version:  node.k8s.io/v1beta1
Handler:      docker
Kind:         RuntimeClass
Metadata:
  Creation Timestamp:  2021-01-03T21:16:35Z
  Managed Fields:
    API Version:  node.k8s.io/v1beta1
    Fields Type:  FieldsV1
    fieldsV1:
      f:handler:
      f:metadata:
        f:annotations:
          .:
          f:kubectl.kubernetes.io/last-applied-configuration:
      f:scheduling:
        .:
        f:nodeSelector:
          .:
          f:kubernetes.io/arch:
          f:kubernetes.io/os:
          f:node.kubernetes.io/windows-build:
        f:tolerations:
    Manager:         kubectl-client-side-apply
    Operation:       Update
    Time:            2021-01-03T21:16:35Z
  Resource Version:  <span style="color:#ae81ff">26030</span>
  Self Link:         /apis/node.k8s.io/v1beta1/runtimeclasses/windows-2019
  UID:               0f70a952-3381-40bb-887e-27db0864c86f
Scheduling:
  Node Selector:
    kubernetes.io/arch:                amd64
    kubernetes.io/os:                  windows
    node.kubernetes.io/windows-build:  10.0.17763
  Tolerations:
    Effect:    NoSchedule
    Key:       os
    Operator:  Equal
    Value:     windows
Events:        &lt;none&gt;
</code></pre></div><p>With this applied, Kubernetes will only attempt to schedule Pods on Windows nodes if they have the RuntimeClass. This does mean that if we applied the RuntimeClass to a Linux workload, Kubernetes would try to schedule it on the Windows nodes.</p>
<p>However this would mean someone (or some process) has applied this intentionally (or at least made an error in configuration), rather than the Kubernetes scheduler being unaware of where to run the workload.</p>
<h2 id="ingress">Ingress</h2>
<p>Now that we have ensured that workloads will run on the correct operating system, we can deploy an Ingress Controller. The purpose of this is to allow external access to workloads within the cluster.</p>
<p>A <a href="https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/">number of</a> Ingress Controllers exist for Kubernetes, covering everything from Azure to F5 BIG-IP loadbalancers. For this cluster we are using the <a href="https://www.nginx.com/products/nginx-ingress-controller">NGINX Ingress Controller</a>, as it is widely used and supported, with excellent documentation. It also has no reliance on an external cloud provider or a commercial loadbalancer/application gateway.</p>
<h3 id="apply-the-ingress-controller">Apply the Ingress Controller</h3>
<p>The process to apply the Ingress Controller is a single command: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/provider/baremetal/deploy.yaml
namespace/ingress-nginx created
serviceaccount/ingress-nginx created
configmap/ingress-nginx-controller created
clusterrole.rbac.authorization.k8s.io/ingress-nginx created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx created
role.rbac.authorization.k8s.io/ingress-nginx created
rolebinding.rbac.authorization.k8s.io/ingress-nginx created
service/ingress-nginx-controller-admission created
service/ingress-nginx-controller created
deployment.apps/ingress-nginx-controller created
validatingwebhookconfiguration.admissionregistration.k8s.io/ingress-nginx-admission created
serviceaccount/ingress-nginx-admission created
clusterrole.rbac.authorization.k8s.io/ingress-nginx-admission created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
role.rbac.authorization.k8s.io/ingress-nginx-admission created
rolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
job.batch/ingress-nginx-admission-create created
job.batch/ingress-nginx-admission-patch created
</code></pre></div><p>This creates all the necessary resources to run the Ingress Controller. We can verify all of the objects using: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ kubectl get all -n ingress-nginx
NAME                                           READY   STATUS      RESTARTS   AGE
pod/ingress-nginx-admission-create-kmrv2       0/1     Completed   <span style="color:#ae81ff">0</span>          7m52s
pod/ingress-nginx-admission-patch-s4jwq        0/1     Completed   <span style="color:#ae81ff">0</span>          7m52s
pod/ingress-nginx-controller-96fb64b49-6x6jt   1/1     Running     <span style="color:#ae81ff">4</span>          7m52s

NAME                                         TYPE           CLUSTER-IP      EXTERNAL-IP   PORT<span style="color:#f92672">(</span>S<span style="color:#f92672">)</span>                      AGE
service/ingress-nginx-controller             LoadBalancer   10.110.160.21   &lt;pending&gt;     80:31293/TCP,443:31561/TCP   7m52s
service/ingress-nginx-controller-admission   ClusterIP      10.103.99.180   &lt;none&gt;        443/TCP                      7m52s

NAME                                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/ingress-nginx-controller   1/1     <span style="color:#ae81ff">1</span>            <span style="color:#ae81ff">1</span>           7m52s

NAME                                                 DESIRED   CURRENT   READY   AGE
replicaset.apps/ingress-nginx-controller-96fb64b49   <span style="color:#ae81ff">1</span>         <span style="color:#ae81ff">1</span>         <span style="color:#ae81ff">1</span>       7m52s

NAME                                       COMPLETIONS   DURATION   AGE
job.batch/ingress-nginx-admission-create   1/1           6s         7m52s
job.batch/ingress-nginx-admission-patch    1/1           7s         7m52s
</code></pre></div><p>At this stage, there is nothing else we need to do. Without a Load Balancer mechanism in place, we are yet unable to expose applications externally from the cluster. This is evident in that the <code>service/ingress-nginx-controller</code> Service is stuck in <code>Pending</code> on waiting for an external IP.</p>
<h2 id="load-balancing">Load balancing</h2>
<p>For traffic to be able to reach the cluster from outside, we can use a combination of Ingress and load balancing. Ingress is used to provide path-based routing (i.e. <code>/app1</code> goes to the <code>app1</code> Kubernetes service, <code>/app2</code> goes to the <code>app2</code> Kubernetes service). The load balancing mechanism is then used to provide the routing from the external network into the cluster.</p>
<p>A point to note here is that I am using external network to mean anything outside of the cluster, not necessarily exposing this to the public internet. This is still required even if you are only running in a lab scenario.</p>
<p>The reason for using load balancing is that the application could be available on any worker in the cluster. As of Kubernetes v1.20, it can support 5000 nodes, so having to remember the IP address of the node and port the application uses could be very cumbersome to say the least.</p>
<p>Most cloud providers provide load balancing functionality, meaning that so long as the cluster knows how to interact with the cloud provider, it can provision load balancers dynamically. As we are building this cluster on KVM, we do not have this luxury. Enter <a href="https://metallb.universe.tf/">MetalLB</a>.</p>
<h3 id="metallb">MetalLB</h3>
<p>MetalLB aims to make non-Cloud provisioned Kubernetes clusters first class citizens when it comes to bringing user traffic into the cluster. MetalLB can function in two ways: -</p>
<ul>
<li>Layer 2 Mode - Assign a pool of IP addresses (from a subnet the Kubernetes worker nodes are attached to) and MetalLB will announce itself as &ldquo;owning&rdquo; that IP (using <a href="https://en.wikipedia.org/wiki/Address_Resolution_Protocol">ARP</a> for IPv4 or <a href="https://en.wikipedia.org/wiki/Neighbor_Discovery_Protocol">NDP</a> for IPv6).</li>
<li>BGP Mode - Peer with a BGP speaking router and announce addresses to bring traffic into the cluster</li>
</ul>
<p>Up until very recently, I spent nearly a decade in Network Engineering and Architecture, so BGP is something I am very familiar with. However, I do also appreciate that not everyone has this background so diving deep into running routing protocols is going to be beyond the scope of this post.</p>
<p>Because of this, I have chosen to use Layer 2 mode for this post for simplicity. I will probably cover using the BGP mode in a future post though!</p>
<h3 id="deploying-metallb">Deploying MetalLB</h3>
<p>To deploy MetalLB, we can follow the steps from the MetalLB <a href="https://metallb.universe.tf/installation/#installation-by-manifest">documentation</a>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.9.5/manifests/namespace.yaml
namespace/metallb-system created

$ kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.9.5/manifests/metallb.yaml
podsecuritypolicy.policy/controller created
podsecuritypolicy.policy/speaker created
serviceaccount/controller created
serviceaccount/speaker created
clusterrole.rbac.authorization.k8s.io/metallb-system:controller created
clusterrole.rbac.authorization.k8s.io/metallb-system:speaker created
role.rbac.authorization.k8s.io/config-watcher created
role.rbac.authorization.k8s.io/pod-lister created
clusterrolebinding.rbac.authorization.k8s.io/metallb-system:controller created
clusterrolebinding.rbac.authorization.k8s.io/metallb-system:speaker created
rolebinding.rbac.authorization.k8s.io/config-watcher created
rolebinding.rbac.authorization.k8s.io/pod-lister created
daemonset.apps/speaker created
deployment.apps/controller created

$ kubectl create secret generic -n metallb-system memberlist --from-literal<span style="color:#f92672">=</span>secretkey<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>openssl rand -base64 128<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span>
secret/memberlist created

$ kubectl get all -n metallb-system
NAME                              READY   STATUS    RESTARTS   AGE
pod/controller-65db86ddc6-r9bhj   1/1     Running   <span style="color:#ae81ff">0</span>          7m58s
pod/speaker-7tqvf                 1/1     Running   <span style="color:#ae81ff">0</span>          7m58s
pod/speaker-85jnx                 1/1     Running   <span style="color:#ae81ff">0</span>          7m58s
pod/speaker-b8sbt                 1/1     Running   <span style="color:#ae81ff">0</span>          7m58s
pod/speaker-fvzlm                 1/1     Running   <span style="color:#ae81ff">0</span>          7m58s
pod/speaker-qx8l4                 1/1     Running   <span style="color:#ae81ff">0</span>          7m58s
pod/speaker-ws4jn                 1/1     Running   <span style="color:#ae81ff">0</span>          7m58s

NAME                     DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
daemonset.apps/speaker   <span style="color:#ae81ff">6</span>         <span style="color:#ae81ff">6</span>         <span style="color:#ae81ff">6</span>       <span style="color:#ae81ff">6</span>            <span style="color:#ae81ff">6</span>           kubernetes.io/os<span style="color:#f92672">=</span>linux   7m58s

NAME                         READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/controller   1/1     <span style="color:#ae81ff">1</span>            <span style="color:#ae81ff">1</span>           7m58s

NAME                                    DESIRED   CURRENT   READY   AGE
replicaset.apps/controller-65db86ddc6   <span style="color:#ae81ff">1</span>         <span style="color:#ae81ff">1</span>         <span style="color:#ae81ff">1</span>       7m58s
</code></pre></div><p>This deploys all the Pods, service accounts, roles, namespaces and more. MetalLB will stay idle for now, until it receives it&rsquo;s configuration (which we will cover in the next step): -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#960050;background-color:#1e0010">$</span> <span style="color:#960050;background-color:#1e0010">kubectl</span> <span style="color:#960050;background-color:#1e0010">logs</span> <span style="color:#960050;background-color:#1e0010">-n</span> <span style="color:#960050;background-color:#1e0010">metallb-system</span> <span style="color:#960050;background-color:#1e0010">controller</span><span style="color:#ae81ff">-65</span><span style="color:#960050;background-color:#1e0010">db</span><span style="color:#ae81ff">86</span><span style="color:#960050;background-color:#1e0010">ddc</span><span style="color:#ae81ff">6</span><span style="color:#960050;background-color:#1e0010">-r</span><span style="color:#ae81ff">9</span><span style="color:#960050;background-color:#1e0010">bhj</span> <span style="color:#960050;background-color:#1e0010">|</span> <span style="color:#960050;background-color:#1e0010">grep</span> <span style="color:#960050;background-color:#1e0010">-i</span> <span style="color:#960050;background-color:#1e0010">config</span> <span style="color:#960050;background-color:#1e0010">|</span> <span style="color:#960050;background-color:#1e0010">jq</span>
[<span style="color:#960050;background-color:#1e0010">...</span>]
{
  <span style="color:#f92672">&#34;caller&#34;</span>: <span style="color:#e6db74">&#34;main.go:62&#34;</span>,
  <span style="color:#f92672">&#34;event&#34;</span>: <span style="color:#e6db74">&#34;noConfig&#34;</span>,
  <span style="color:#f92672">&#34;msg&#34;</span>: <span style="color:#e6db74">&#34;not processing, still waiting for config&#34;</span>,
  <span style="color:#f92672">&#34;service&#34;</span>: <span style="color:#e6db74">&#34;ingress-nginx/ingress-nginx-controller&#34;</span>,
  <span style="color:#f92672">&#34;ts&#34;</span>: <span style="color:#e6db74">&#34;2021-01-08T20:12:39.495793513Z&#34;</span>
}
[<span style="color:#960050;background-color:#1e0010">...</span>]
</code></pre></div><h3 id="configuring-metallb">Configuring MetalLB</h3>
<p>To configure MetalLB, we use steps similar to the MetalLB <a href="https://metallb.universe.tf/configuration/">Configuration Documentation</a>. This will define the mode (Layer 2 or BGP), and also the IP range we will use to serve traffic on.</p>
<p>I am using the <code>10.15.32.0/24</code> subnet for my KVM lab, and have chosen a subset of this range for MetalLB to use for load balanced services: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">apiVersion</span>: v1
<span style="color:#66d9ef">kind</span>: ConfigMap
<span style="color:#66d9ef">metadata</span>:
  <span style="color:#66d9ef">namespace</span>: metallb-system
  <span style="color:#66d9ef">name</span>: config
<span style="color:#66d9ef">data</span>:
  <span style="color:#66d9ef">config</span>: <span style="color:#e6db74">|
</span><span style="color:#e6db74">    address-pools:</span>
    - <span style="color:#66d9ef">name</span>: default
      <span style="color:#66d9ef">protocol</span>: layer2
      <span style="color:#66d9ef">addresses</span>:
      - <span style="color:#ae81ff">10.15.32.200-10.15.32.210</span>
</code></pre></div><p>The <code>protocol</code> defines the mode (in this case, <code>layer2</code>).</p>
<p>Now, whenever we use the <code>type: LoadBalancer</code> within a Kubernetes <code>Service</code>, MetalLB will use one of the IPs from <code>10.15.32.200</code> to <code>10.15.32.210</code> and will respond to Address Resolution Protocol (ARP) requests for the IP. As long as this range is reachable on your network, Kubernetes Services will now be available.</p>
<p>To deploy the <code>ConfigMap</code>, add something like the above to a file (changing to suit your network/subnetting structure), and then deploy like so: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ kubectl apply -f configmap.yaml
configmap/config created
</code></pre></div><p>We should now see some activity in the logs: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#960050;background-color:#1e0010">$</span> <span style="color:#960050;background-color:#1e0010">kubectl</span> <span style="color:#960050;background-color:#1e0010">logs</span> <span style="color:#960050;background-color:#1e0010">-n</span> <span style="color:#960050;background-color:#1e0010">metallb-system</span> <span style="color:#960050;background-color:#1e0010">controller</span><span style="color:#ae81ff">-65</span><span style="color:#960050;background-color:#1e0010">db</span><span style="color:#ae81ff">86</span><span style="color:#960050;background-color:#1e0010">ddc</span><span style="color:#ae81ff">6</span><span style="color:#960050;background-color:#1e0010">-r</span><span style="color:#ae81ff">9</span><span style="color:#960050;background-color:#1e0010">bhj</span> <span style="color:#960050;background-color:#1e0010">|</span> <span style="color:#960050;background-color:#1e0010">grep</span> <span style="color:#960050;background-color:#1e0010">-i</span> <span style="color:#960050;background-color:#1e0010">config</span> <span style="color:#960050;background-color:#1e0010">|</span> <span style="color:#960050;background-color:#1e0010">jq</span>
[<span style="color:#960050;background-color:#1e0010">...</span>]
{
  <span style="color:#f92672">&#34;caller&#34;</span>: <span style="color:#e6db74">&#34;main.go:108&#34;</span>,
  <span style="color:#f92672">&#34;configmap&#34;</span>: <span style="color:#e6db74">&#34;metallb-system/config&#34;</span>,
  <span style="color:#f92672">&#34;event&#34;</span>: <span style="color:#e6db74">&#34;startUpdate&#34;</span>,
  <span style="color:#f92672">&#34;msg&#34;</span>: <span style="color:#e6db74">&#34;start of config update&#34;</span>,
  <span style="color:#f92672">&#34;ts&#34;</span>: <span style="color:#e6db74">&#34;2021-01-08T20:23:17.644609367Z&#34;</span>
}
{
  <span style="color:#f92672">&#34;caller&#34;</span>: <span style="color:#e6db74">&#34;main.go:121&#34;</span>,
  <span style="color:#f92672">&#34;configmap&#34;</span>: <span style="color:#e6db74">&#34;metallb-system/config&#34;</span>,
  <span style="color:#f92672">&#34;event&#34;</span>: <span style="color:#e6db74">&#34;endUpdate&#34;</span>,
  <span style="color:#f92672">&#34;msg&#34;</span>: <span style="color:#e6db74">&#34;end of config update&#34;</span>,
  <span style="color:#f92672">&#34;ts&#34;</span>: <span style="color:#e6db74">&#34;2021-01-08T20:23:17.644842185Z&#34;</span>
}
{
  <span style="color:#f92672">&#34;caller&#34;</span>: <span style="color:#e6db74">&#34;k8s.go:402&#34;</span>,
  <span style="color:#f92672">&#34;configmap&#34;</span>: <span style="color:#e6db74">&#34;metallb-system/config&#34;</span>,
  <span style="color:#f92672">&#34;event&#34;</span>: <span style="color:#e6db74">&#34;configLoaded&#34;</span>,
  <span style="color:#f92672">&#34;msg&#34;</span>: <span style="color:#e6db74">&#34;config (re)loaded&#34;</span>,
  <span style="color:#f92672">&#34;ts&#34;</span>: <span style="color:#e6db74">&#34;2021-01-08T20:23:17.644924109Z&#34;</span>
}
{
  <span style="color:#f92672">&#34;caller&#34;</span>: <span style="color:#e6db74">&#34;main.go:49&#34;</span>,
  <span style="color:#f92672">&#34;event&#34;</span>: <span style="color:#e6db74">&#34;startUpdate&#34;</span>,
  <span style="color:#f92672">&#34;msg&#34;</span>: <span style="color:#e6db74">&#34;start of service update&#34;</span>,
  <span style="color:#f92672">&#34;service&#34;</span>: <span style="color:#e6db74">&#34;ingress-nginx/ingress-nginx-controller&#34;</span>,
  <span style="color:#f92672">&#34;ts&#34;</span>: <span style="color:#e6db74">&#34;2021-01-08T20:23:17.650177469Z&#34;</span>
}
{
  <span style="color:#f92672">&#34;caller&#34;</span>: <span style="color:#e6db74">&#34;service.go:114&#34;</span>,
  <span style="color:#f92672">&#34;event&#34;</span>: <span style="color:#e6db74">&#34;ipAllocated&#34;</span>,
  <span style="color:#f92672">&#34;ip&#34;</span>: <span style="color:#e6db74">&#34;10.15.32.200&#34;</span>,
  <span style="color:#f92672">&#34;msg&#34;</span>: <span style="color:#e6db74">&#34;IP address assigned by controller&#34;</span>,
  <span style="color:#f92672">&#34;service&#34;</span>: <span style="color:#e6db74">&#34;ingress-nginx/ingress-nginx-controller&#34;</span>,
  <span style="color:#f92672">&#34;ts&#34;</span>: <span style="color:#e6db74">&#34;2021-01-08T20:23:17.650293618Z&#34;</span>
}
{
  <span style="color:#f92672">&#34;caller&#34;</span>: <span style="color:#e6db74">&#34;main.go:96&#34;</span>,
  <span style="color:#f92672">&#34;event&#34;</span>: <span style="color:#e6db74">&#34;serviceUpdated&#34;</span>,
  <span style="color:#f92672">&#34;msg&#34;</span>: <span style="color:#e6db74">&#34;updated service object&#34;</span>,
  <span style="color:#f92672">&#34;service&#34;</span>: <span style="color:#e6db74">&#34;ingress-nginx/ingress-nginx-controller&#34;</span>,
  <span style="color:#f92672">&#34;ts&#34;</span>: <span style="color:#e6db74">&#34;2021-01-08T20:23:17.674400824Z&#34;</span>
}
[<span style="color:#960050;background-color:#1e0010">...</span>]
</code></pre></div><p>As you can see, one of the services has already had an IP address allocated. This is the NGINX Ingress Controller, which is now exposing itself on the IP <code>10.15.32.200</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ kubectl get service -n ingress-nginx
NAME                                 TYPE           CLUSTER-IP      EXTERNAL-IP    PORT<span style="color:#f92672">(</span>S<span style="color:#f92672">)</span>                      AGE
ingress-nginx-controller             LoadBalancer   10.110.160.21   10.15.32.200   80:31293/TCP,443:31561/TCP   4d22h
ingress-nginx-controller-admission   ClusterIP      10.103.99.180   &lt;none&gt;         443/TCP                      4d22h

$ curl 10.15.32.200
&lt;html&gt;
&lt;head&gt;&lt;title&gt;404 Not Found&lt;/title&gt;&lt;/head&gt;
&lt;body&gt;
&lt;center&gt;&lt;h1&gt;404 Not Found&lt;/h1&gt;&lt;/center&gt;
&lt;hr&gt;&lt;center&gt;nginx&lt;/center&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre></div><p>While the page returned is a <code>404 Not Found</code> error page, this is being presented by the NGINX Ingress Controller. This means that Services are now available outside of the cluster. Now we can start running some applications!</p>
<h2 id="running-applications">Running applications</h2>
<h3 id="linux">Linux</h3>
<p>To test running a Linux workload, we will use a generic NGINX deployment, with a Service of <code>type: LoadBalancer</code>, meaning that traffic will be forwarded directly from the LoadBalancer to the application (no ingress involved).</p>
<p>The Deployment and Service are defined like so: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">apiVersion</span>: apps/v1
<span style="color:#66d9ef">kind</span>: Deployment
<span style="color:#66d9ef">metadata</span>:
  <span style="color:#66d9ef">name</span>: nginx-deployment
  <span style="color:#66d9ef">labels</span>:
    <span style="color:#66d9ef">app</span>: nginx
<span style="color:#66d9ef">spec</span>:
  <span style="color:#66d9ef">replicas</span>: <span style="color:#ae81ff">3</span>
  <span style="color:#66d9ef">selector</span>:
    <span style="color:#66d9ef">matchLabels</span>:
      <span style="color:#66d9ef">app</span>: nginx
  <span style="color:#66d9ef">template</span>:
    <span style="color:#66d9ef">metadata</span>:
      <span style="color:#66d9ef">labels</span>:
        <span style="color:#66d9ef">app</span>: nginx
    <span style="color:#66d9ef">spec</span>:
      <span style="color:#66d9ef">containers</span>:
      - <span style="color:#66d9ef">name</span>: nginx
        <span style="color:#66d9ef">image</span>: nginx:<span style="color:#ae81ff">1.14.2</span>
        <span style="color:#66d9ef">ports</span>:
        - <span style="color:#66d9ef">containerPort</span>: <span style="color:#ae81ff">80</span>
---
<span style="color:#66d9ef">apiVersion</span>: v1
<span style="color:#66d9ef">kind</span>: Service
<span style="color:#66d9ef">metadata</span>:
  <span style="color:#66d9ef">name</span>: nginx
<span style="color:#66d9ef">spec</span>:
  <span style="color:#66d9ef">type</span>: LoadBalancer
  <span style="color:#66d9ef">ports</span>:
  - <span style="color:#66d9ef">protocol</span>: TCP
    <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">80</span>
  <span style="color:#66d9ef">selector</span>:
    <span style="color:#66d9ef">app</span>: nginx
</code></pre></div><p>The Deployment will run three NGINX containers, and the containers will listen on TCP port 80. The Service matches the NGINX application (using the <code>selector</code>), and exposes it on a new IP from the range we assigned to MetalLB: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ kubectl get pods
NAME                                READY   STATUS    RESTARTS   AGE
nginx-deployment-66b6c48dd5-7qp64   1/1     Running   <span style="color:#ae81ff">0</span>          13m
nginx-deployment-66b6c48dd5-cbtwt   1/1     Running   <span style="color:#ae81ff">0</span>          13m
nginx-deployment-66b6c48dd5-qfd52   1/1     Running   <span style="color:#ae81ff">0</span>          13m

$ kubectl get service
NAME         TYPE           CLUSTER-IP       EXTERNAL-IP    PORT<span style="color:#f92672">(</span>S<span style="color:#f92672">)</span>        AGE
kubernetes   ClusterIP      10.96.0.1        &lt;none&gt;         443/TCP        13d
nginx        LoadBalancer   10.102.182.166   10.15.32.201   80:30041/TCP   14m
</code></pre></div><p>If we then browse to this IP address, we should get the default NGINX landing page: -</p>
<p><img src="/img/kubernetes-win-linux/metallb-nginx-lb.png" alt="MetalLB with NGINX - Basic Load Balancer"></p>
<h3 id="linux-using-ingress">Linux using Ingress</h3>
<p>While the above method works well, we will run out of available IP addresses very quickly if every service gets it&rsquo;s own IP address. This is where we can use the Ingress method, which allows us to do path-based routing for traffic: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">apiVersion</span>: apps/v1
<span style="color:#66d9ef">kind</span>: Deployment
<span style="color:#66d9ef">metadata</span>:
  <span style="color:#66d9ef">name</span>: nginx-deployment
  <span style="color:#66d9ef">labels</span>:
    <span style="color:#66d9ef">app</span>: nginx
<span style="color:#66d9ef">spec</span>:
  <span style="color:#66d9ef">replicas</span>: <span style="color:#ae81ff">3</span>
  <span style="color:#66d9ef">selector</span>:
    <span style="color:#66d9ef">matchLabels</span>:
      <span style="color:#66d9ef">app</span>: nginx
  <span style="color:#66d9ef">template</span>:
    <span style="color:#66d9ef">metadata</span>:
      <span style="color:#66d9ef">labels</span>:
        <span style="color:#66d9ef">app</span>: nginx
    <span style="color:#66d9ef">spec</span>:
      <span style="color:#66d9ef">containers</span>:
      - <span style="color:#66d9ef">name</span>: nginx
        <span style="color:#66d9ef">image</span>: nginx:<span style="color:#ae81ff">1.14.2</span>
        <span style="color:#66d9ef">ports</span>:
        - <span style="color:#66d9ef">containerPort</span>: <span style="color:#ae81ff">80</span>
---
<span style="color:#66d9ef">apiVersion</span>: v1
<span style="color:#66d9ef">kind</span>: Service
<span style="color:#66d9ef">metadata</span>:
  <span style="color:#66d9ef">name</span>: nginx
<span style="color:#66d9ef">spec</span>:
  <span style="color:#66d9ef">type</span>: NodePort
  <span style="color:#66d9ef">ports</span>:
  - <span style="color:#66d9ef">protocol</span>: TCP
    <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">80</span>
  <span style="color:#66d9ef">selector</span>:
    <span style="color:#66d9ef">app</span>: nginx
---
<span style="color:#66d9ef">apiVersion</span>: networking.k8s.io/v1
<span style="color:#66d9ef">kind</span>: Ingress
<span style="color:#66d9ef">metadata</span>:
  <span style="color:#66d9ef">name </span>: nginx
  <span style="color:#66d9ef">annotations</span>:
    <span style="color:#66d9ef">nginx.ingress.kubernetes.io/rewrite-target</span>: /
<span style="color:#66d9ef">spec</span>:
  <span style="color:#66d9ef">rules</span>:
  - <span style="color:#66d9ef">http</span>:
      <span style="color:#66d9ef">paths</span>:
      - <span style="color:#66d9ef">path</span>: /nginx
        <span style="color:#66d9ef">pathType</span>: Prefix
        <span style="color:#66d9ef">backend</span>:
          <span style="color:#66d9ef">service</span>:
            <span style="color:#66d9ef">name</span>: nginx
            <span style="color:#66d9ef">port</span>:
              <span style="color:#66d9ef">number</span>: <span style="color:#ae81ff">80</span>
</code></pre></div><p>To explain the above configuration, we expose the Service using <code>type: NodePort</code>. Rather than exposing via MetalLB directly, we add an Ingress object which matches the service. This object says that if we target the IP of the NGINX Ingress Controller (<code>10.15.32.200</code>) and use the path <code>/nginx</code>, traffic will be forwarded to the NGINX service we have created. We can check the status of the Ingress object with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ kubectl get ingresses
NAME    CLASS    HOSTS   ADDRESS       PORTS   AGE
nginx   &lt;none&gt;   *       10.15.32.20   <span style="color:#ae81ff">80</span>      9m19s

$ kubectl describe ingresses nginx
Name:             nginx
Namespace:        default
Address:          10.15.32.20
Default backend:  default-http-backend:80 <span style="color:#f92672">(</span>&lt;error: endpoints <span style="color:#e6db74">&#34;default-http-backend&#34;</span> not found&gt;<span style="color:#f92672">)</span>
Rules:
  Host        Path  Backends
  ----        ----  --------
  *
              /nginx   nginx:80   10.244.2.9:80,10.244.3.12:80,10.244.4.14:80<span style="color:#f92672">)</span>
Annotations:  nginx.ingress.kubernetes.io/rewrite-target: /
Events:
  Type    Reason  Age    From                      Message
  ----    ------  ----   ----                      -------
  Normal  CREATE  9m23s  nginx-ingress-controller  Ingress default/nginx
  Normal  UPDATE  9m19s  nginx-ingress-controller  Ingress default/nginx
</code></pre></div><p>As we can see, we have our Ingress object, as well as the paths for that object too.</p>
<p>We can now verify that path-based routing works with the NGINX Ingress Controller: -</p>
<p><img src="/img/kubernetes-win-linux/metallb-nginx-with-ingress.png" alt="MetalLB NGINX - With Ingress"></p>
<p>It works. Now to deploy a Windows application!</p>
<h3 id="windows">Windows</h3>
<p>In this section, we will deploy an IIS container on Windows so that it exposes a basic web server (as we did with NGINX on the Linux workers).</p>
<p>One point to note is that I have yet to be able to make MetalLB work directly with Windows nodes. The underlying functionality of MetalLB specifies that it should attract the traffic via the Linux MetalLB pods, and then forward to the Windows workers via the Flannel networking we configured in the previous parts of this series. This does not seem to work in my lab, nor have I seen any other articles or documentation which demonstrates this working correctly.</p>
<p>Instead we shall make use of the NGINX Ingress Controller, and forward to the IIS container using path-based routing. We define a Deployment, Service and Ingress object like in the previous section: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">apiVersion</span>: apps/v1
<span style="color:#66d9ef">kind</span>: Deployment
<span style="color:#66d9ef">metadata</span>:
  <span style="color:#66d9ef">name</span>: iis<span style="color:#ae81ff">-2019</span>
  <span style="color:#66d9ef">labels</span>:
    <span style="color:#66d9ef">app</span>: iis<span style="color:#ae81ff">-2019</span>
<span style="color:#66d9ef">spec</span>:
  <span style="color:#66d9ef">replicas</span>: <span style="color:#ae81ff">1</span>
  <span style="color:#66d9ef">template</span>:
    <span style="color:#66d9ef">metadata</span>:
      <span style="color:#66d9ef">name</span>: iis<span style="color:#ae81ff">-2019</span>
      <span style="color:#66d9ef">labels</span>:
        <span style="color:#66d9ef">app</span>: iis<span style="color:#ae81ff">-2019</span>
    <span style="color:#66d9ef">spec</span>:
      <span style="color:#66d9ef">runtimeClassName</span>: windows<span style="color:#ae81ff">-2019</span>
      <span style="color:#66d9ef">containers</span>:
      - <span style="color:#66d9ef">name</span>: iis
        <span style="color:#66d9ef">image</span>: mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019
        <span style="color:#66d9ef">ports</span>:
          - <span style="color:#66d9ef">containerPort</span>: <span style="color:#ae81ff">80</span>
        <span style="color:#66d9ef">resources</span>:
          <span style="color:#66d9ef">limits</span>:
            <span style="color:#66d9ef">cpu</span>: <span style="color:#ae81ff">1</span>
            <span style="color:#66d9ef">memory</span>: 800Mi
          <span style="color:#66d9ef">requests</span>:
            <span style="color:#66d9ef">cpu</span>: .<span style="color:#ae81ff">1</span>
            <span style="color:#66d9ef">memory</span>: 300Mi
  <span style="color:#66d9ef">selector</span>:
    <span style="color:#66d9ef">matchLabels</span>:
      <span style="color:#66d9ef">app</span>: iis<span style="color:#ae81ff">-2019</span>
---
<span style="color:#66d9ef">apiVersion</span>: v1
<span style="color:#66d9ef">kind</span>: Service
<span style="color:#66d9ef">metadata</span>:
  <span style="color:#66d9ef">name</span>: iis
<span style="color:#66d9ef">spec</span>:
  <span style="color:#66d9ef">type</span>: NodePort
  <span style="color:#66d9ef">ports</span>:
  - <span style="color:#66d9ef">protocol</span>: TCP
    <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">80</span>
  <span style="color:#66d9ef">selector</span>:
    <span style="color:#66d9ef">app</span>: iis<span style="color:#ae81ff">-2019</span>
---
<span style="color:#66d9ef">apiVersion</span>: networking.k8s.io/v1
<span style="color:#66d9ef">kind</span>: Ingress
<span style="color:#66d9ef">metadata</span>:
  <span style="color:#66d9ef">name </span>: iis
  <span style="color:#66d9ef">annotations</span>:
    <span style="color:#66d9ef">nginx.ingress.kubernetes.io/rewrite-target</span>: /
<span style="color:#66d9ef">spec</span>:
  <span style="color:#66d9ef">rules</span>:
  - <span style="color:#66d9ef">http</span>:
      <span style="color:#66d9ef">paths</span>:
      - <span style="color:#66d9ef">path</span>: /iis
        <span style="color:#66d9ef">pathType</span>: Prefix
        <span style="color:#66d9ef">backend</span>:
          <span style="color:#66d9ef">service</span>:
            <span style="color:#66d9ef">name</span>: iis
            <span style="color:#66d9ef">port</span>:
              <span style="color:#66d9ef">number</span>: <span style="color:#ae81ff">80</span>
</code></pre></div><p>This is very similar to the previous section. The only major difference (other than specifying the IIS image and names rather than NGINX) is the <code>runtimeClassName</code>. If you recall from earlier in this post, this is required to be able to tolerate the taint on the Windows nodes that stops Linux workloads from attempting to schedule on Windows. Without this, the container is unable to schedule on the Windows worker.</p>
<p>After applying this, we should see the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">$ kubectl get pod
NAME                                READY   STATUS    RESTARTS   AGE
iis<span style="color:#ae81ff">-2019</span>-c7b8f4cdb-q6cmf            <span style="color:#ae81ff">1</span>/<span style="color:#ae81ff">1</span>     Running   <span style="color:#ae81ff">0</span>          15m
nginx-deployment-66b6c48dd5-5xpr6   <span style="color:#ae81ff">1</span>/<span style="color:#ae81ff">1</span>     Running   <span style="color:#ae81ff">0</span>          38m
nginx-deployment-66b6c48dd5-dx4nv   <span style="color:#ae81ff">1</span>/<span style="color:#ae81ff">1</span>     Running   <span style="color:#ae81ff">0</span>          38m
nginx-deployment-66b6c48dd5-rfxqj   <span style="color:#ae81ff">1</span>/<span style="color:#ae81ff">1</span>     Running   <span style="color:#ae81ff">0</span>          38m

$ kubectl get service iis
NAME   TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
iis    NodePort   <span style="color:#ae81ff">10.100.120.68</span>   &lt;none<span style="color:#e6db74">&gt;        80:30208/TCP   16m</span>

$ kubectl get ingress
NAME    CLASS    HOSTS   ADDRESS       PORTS   AGE
iis     &lt;none<span style="color:#e6db74">&gt;   *       10.15.32.20   80      14m</span>
nginx   &lt;none<span style="color:#e6db74">&gt;   *       10.15.32.20   80      37m</span>

$ kubectl describe ingresses iis
<span style="color:#66d9ef">Warning</span>: extensions/v1beta1 Ingress is deprecated in v1<span style="color:#ae81ff">.14</span>+, unavailable in v1<span style="color:#ae81ff">.22</span>+; use networking.k8s.io/v1 Ingress
<span style="color:#66d9ef">Name</span>:             iis
<span style="color:#66d9ef">Namespace</span>:        default
<span style="color:#66d9ef">Address</span>:          <span style="color:#ae81ff">10.15.32.20</span>
<span style="color:#66d9ef">Default backend:  default-http-backend:80 (&lt;error</span>: endpoints <span style="color:#e6db74">&#34;default-http-backend&#34;</span> not found&gt;)
<span style="color:#66d9ef">Rules</span>:
  Host        Path  Backends
  ----        ----  -------<span style="color:#e6db74">-
</span><span style="color:#e6db74">  *</span>
              /iis   iis:<span style="color:#ae81ff">80</span>   <span style="color:#ae81ff">10.244.1.14</span>:<span style="color:#ae81ff">80</span>)
<span style="color:#66d9ef">Annotations:  nginx.ingress.kubernetes.io/rewrite-target</span>: /
<span style="color:#66d9ef">Events</span>:
  Type    Reason  Age   From                      Message
  ----    ------  ----  ----                      ------<span style="color:#e6db74">-
</span><span style="color:#e6db74">  Normal  CREATE  14m   nginx-ingress-controller  Ingress default/iis</span>
  Normal  UPDATE  13m   nginx-ingress-controller  Ingress default/iis
</code></pre></div><p>Now if we navigate to the <code>10.15.32.200</code> IP using the path IIS, we should see the IIS landing page: -</p>
<p><img src="/img/kubernetes-win-linux/metallb-iis-with-ingress.png" alt="MetalLB IIS - With Ingress"></p>
<p>It loads, although the main image doesn&rsquo;t. This appears to be because the Ingress controller is exposing the path directly (i.e. passing <code>/iis</code> to the IIS backend) for the image. To work around this, we can update the Ingress object to the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#66d9ef">apiVersion</span>: networking.k8s.io/v1
<span style="color:#66d9ef">kind</span>: Ingress
<span style="color:#66d9ef">metadata</span>:
  <span style="color:#66d9ef">name </span>: iis
  <span style="color:#66d9ef">annotations</span>:
    <span style="color:#66d9ef">nginx.ingress.kubernetes.io/rewrite-target</span>: /$<span style="color:#ae81ff">2</span>
<span style="color:#66d9ef">spec</span>:
  <span style="color:#66d9ef">rules</span>:
  - <span style="color:#66d9ef">http</span>:
      <span style="color:#66d9ef">paths</span>:
      - <span style="color:#66d9ef">path</span>: /iis(/|$)(.<span style="color:#75715e">*)</span>
        <span style="color:#66d9ef">pathType</span>: Prefix
        <span style="color:#66d9ef">backend</span>:
          <span style="color:#66d9ef">service</span>:
            <span style="color:#66d9ef">name</span>: iis
            <span style="color:#66d9ef">port</span>:
              <span style="color:#66d9ef">number</span>: <span style="color:#ae81ff">80</span>
</code></pre></div><p>This uses a <a href="https://www.rexegg.com/regex-capture.html">capture group</a>, saying that anything that comes after the IIS path must also be passed to the Ingress backend as is. We can see the results of this below: -</p>
<p><img src="/img/kubernetes-win-linux/metallb-iis-with-ingress-capture.png" alt="MetalLB IIS - With Ingress and Capture Group"></p>
<p>Notice the trailing <code>/</code> after the <code>/iis</code> path in the URL. Without this, the image is still not visible.</p>
<p>With the above, we now have both Linux and Windows applications running on the same cluster!</p>
<h2 id="summary">Summary</h2>
<p>Kubernetes is fast becoming the de facto way of deploying container-based applications. While Linux is still the easiest and preferred method for deploying applications on Kubernetes, this series has shown that it is perfectly possible to run container-based workloads on Windows. It has also showed that building and running a Windows worker in a Kubernetes cluster is not too dissimilar from running Linux workers.</p>
<p>We created our machine images (the basis for our control plane nodes and workers) using the same tools (Packer and Ansible). We created the cluster using Terraform using Cloud-Init, with a very similar approach to configuring and initializing the workers (both Windows and Linux). Finally, we deployed applications to both Linux and Windows, using the same tools (the Kubernetes command line) and resource definitions (Deployments, Services, Ingress).</p>
<p>All of this has enabled us to treat Linux and Windows almost equally. By doing this, the choice of how to create applications to run on Kubernetes is down to preference and language choice. For developers who are more comfortable with creating Windows-native applications, they no longer need to start from the beginning and rewrite/refactor their application to run inside a Linux container.</p>
<p>Hopefully this series has provided some benefit to those who want to use Windows in a Kubernetes environment, as well as those just curious to see how well they work together.</p>
]]></content>
        </item>
        
        <item>
            <title>Kubernetes - Building a Mixed Linux and Windows Cluster using Packer, Terraform, Ansible and KVM - Part 2: Creating the Cluster</title>
            <link>https://yetiops.net/posts/kubernetes-terraform-kvm-linux-windows-part-2/</link>
            <pubDate>Tue, 08 Dec 2020 19:25:36 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/kubernetes-terraform-kvm-linux-windows-part-2/</guid>
            <description>This post is the second in a series on creating a Kubernetes cluster containing both Linux and Windows workers. The first post covered building the virtual machine images ready to be configured as Control Plane or Worker nodes. This post will cover initializing the cluster using Terraform and Cloud-Init. The final post in the series will cover how to deploy applications to the cluster and make them available.
Packer changes Our virtual machine images are created using Packer, which was covered in the previous post.</description>
            <content type="html"><![CDATA[<p>This post is the second in a series on creating a Kubernetes cluster containing both Linux and Windows workers. The <a href="/posts/kubernetes-terraform-kvm-linux-windows-part-1/">first post</a> covered building the virtual machine images ready to be configured as Control Plane or Worker nodes. This post will cover initializing the cluster using Terraform and Cloud-Init. The final post in the series will cover how to deploy applications to the cluster and make them available.</p>
<h2 id="packer-changes">Packer changes</h2>
<p>Our virtual machine images are created using Packer, which was covered in the previous post. Since creating that post, we have also added the <a href="https://www.vaultproject.io/">Hashicorp Vault</a> CLI to both the Linux and Windows images, necessitating some changes in the Ansible roles applied to the Packer images. Reasons for this will become apparent in the next section</p>
<h3 id="linux">Linux</h3>
<p>To install on Linux, we add an additional Ansible role (called Vault) and place the following into the <strong>tasks/main.yml</strong> file: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e"># tasks file for vault</span>
- <span style="color:#66d9ef">name</span>: Add Hashicorp Repository key
  <span style="color:#66d9ef">apt_key</span>:
    <span style="color:#66d9ef">url</span>: https://apt.releases.hashicorp.com/gpg
    <span style="color:#66d9ef">state</span>: present

- <span style="color:#66d9ef">name</span>: Add Hashicorp Repository
  <span style="color:#66d9ef">apt_repository</span>:
    <span style="color:#66d9ef">repo</span>: deb [arch=amd64] https://apt.releases.hashicorp.com buster main
    <span style="color:#66d9ef">state</span>: present

- <span style="color:#66d9ef">name</span>: Install Vault
  <span style="color:#66d9ef">package</span>:
    <span style="color:#66d9ef">name</span>:
      - vault
    <span style="color:#66d9ef">state</span>: present
    <span style="color:#66d9ef">update_cache</span>: yes
</code></pre></div><p>Now that there is an official Debian repository for most of Hashicorp&rsquo;s projects, we can just add it to our Debian machines and install Vault using Apt. If your chosen flavour of Linux is RPM-based (e.g. Red Hat, CentOS, Fedora), repositories are <a href="https://learn.hashicorp.com/tutorials/vault/getting-started-install">available</a> too.</p>
<p>After this, the base Packer image will have the Vault CLI installed.</p>
<h3 id="windows">Windows</h3>
<p>To install on Windows, we do the same as for Linux (add an Ansible rolled called Vault). The contents of the <strong>tasks/main.yml</strong> file are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e"># tasks file for vault</span>
- <span style="color:#66d9ef">name</span>: Install vault
  <span style="color:#66d9ef">win_chocolatey</span>:
    <span style="color:#66d9ef">name</span>:
    - vault
    <span style="color:#66d9ef">state</span>: present
</code></pre></div><p>When installing packages with <a href="https://chocolatey.org/">Chocolatey</a>, Ansible will automatically install the Chocolatey package manager if it hasn&rsquo;t been already. This means we only need to a task that installs the Vault package.</p>
<h2 id="vault">Vault?</h2>
<p>To create a Kubernetes cluster, we use <a href="https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/">kubeadm</a>. First, we run the step to create a cluster (<code>kubeadm init</code>) on the first Control Plane node. After this,  we join the other nodes to the first Control Plane node using <code>kubeadm join</code>.</p>
<p>The issue here is that <code>kubeadm join</code> requires the use of a <strong>token</strong>, a hash of the <strong>discovery token CA certificate</strong> and a <strong>certificate key</strong>. Even if we are comfortable with exposing these in plain text, we still need to find a way for the  other Control Plane nodes and all of the Workers to access them.</p>
<p>One option would be to manually join each node, but this isn&rsquo;t very scalable. If you create multiple clusters, and with many more workers, it could take hours to go through all of the nodes and manually join them together.</p>
<p>Instead we configure the first Control Plane node to place the <strong>token</strong>, <strong>discovery token CA certificate</strong> and <strong>certificate key</strong> into Hashicorp&rsquo;s Vault once the <code>kubeadm init</code> phase is complete. Vault is a secret management store, which makes it perfect for this task.</p>
<p>The other Control Plane nodes and Workers will then be able to retrieve he secret and join the first Control Plane node (and therefore the cluster).</p>
<p>As an added bonus, Vault can be configured using Terraform. Other than the initial Vault node(s) setup, all of our Vault configuration can be created and managed as Infrastructure-as-Code.</p>
<h3 id="creating-a-vault-node">Creating a Vault node</h3>
<p>To create a Vault node, follow the instructions from Hashicorp&rsquo;s <a href="https://learn.hashicorp.com/collections/vault/getting-started">Getting Started</a> page. If you intend to run Vault in production, or at least simulate Vault in a production scenario (with sealing, unsealing, access policies and more), follow the <a href="https://learn.hashicorp.com/tutorials/vault/getting-started-install?in=vault/getting-started">Install Vault</a> section and the <a href="https://learn.hashicorp.com/tutorials/vault/getting-started-deploy?in=vault/getting-started">Deploy Vault</a> section.</p>
<p>If you have never used Vault before, I would advise following the entire tutorial to gain familiarity. If you are already comfortable with the concepts of Vault though, the two sections mentioned will get Vault up and running.</p>
<h3 id="unsealing-vault">Unsealing Vault</h3>
<p>When you go through the process of installing Vault, you will be presented with something like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">Unseal Key 1: $UNSEAL_KEY_1
Unseal Key 2: $UNSEAL_KEY_2
Unseal Key 3: $UNSEAL_KEY_3
Unseal Key 4: $UNSEAL_KEY_4
Unseal Key 5: $UNSEAL_KEY_5

Initial Root Token: $ROOT_TOKEN 

Vault initialized with 5 key shares and a key threshold of 3. Please securely
distribute the key shares printed above. When the Vault is re-sealed,
restarted, or stopped, you must supply at least 3 of these keys to unseal it
before it can start servicing requests.

Vault does not store the generated master key. Without at least 3 key to
reconstruct the master key, Vault will remain permanently sealed!

It is possible to generate new unseal keys, provided you have a quorum of
existing unseal keys shares. See &#34;vault operator rekey&#34; for more information.
</code></pre></div><p>Vault starts in a sealed state by default. If Vault is sealed, the secrets are not accessible until is is unsealed. How you manage the unseal keys (a password store, in a physical safe, etc) is down to your processes.</p>
<p>Vault will stay sealed until it is presented with at least 3 unseal keys. To provide these keys, run <code>vault operator unseal</code> and enter one of the keys. Do this two more times (providing a different unseal key each time), and you will be presented with the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">Key                     Value
---                     -----
Seal Type               shamir
Initialized             true
Sealed                  false &lt;-----------
Total Shares            5
Threshold               3
Version                 1.5.4
Cluster Name            vault-cluster-5fd7d77e
Cluster ID              71aa20ba-1fd4-e2ec-79bc-3748c8548d0e
HA Enabled              true
HA Cluster              https://127.0.0.1:8201
HA Mode                 standby
Active Node Address     https://0.0.0.0:8200
Raft Committed Index    8061
Raft Applied Index      8061
</code></pre></div><p>In the above, you can see that the <code>Sealed</code> value is <code>false</code>, meaning we can now access the secrets inside Vault.</p>
<h3 id="interacting-with-vault">Interacting with Vault</h3>
<p>To interact with Vault, you can use the CLI or the web interface. The web interface looks like this: -</p>
<p><img src="/img/kubernetes-win-linux/vault-ui.png" alt="Vault UI"></p>
<p>Initially, you will need to use the root token (as shown in the previous section) to login. After this, you will be presented with something like the following: -</p>
<p><img src="/img/kubernetes-win-linux/vault-ui-secrets.png" alt="Vault UI - Secret Engines"></p>
<p>From here you can create <strong>Secrets Engines</strong>, define <strong>Access</strong> (users, groups, auth methods etc), create <strong>Policies</strong> and also use some of the tools available (e.g. password generation, hashing etc). It is strongly advised to create users and/or enable alternative authentication methods rather than using the root key for access by users. Authentication methods include OIDC, LDAP, AWS IAM and more (see <a href="https://www.vaultproject.io/docs/auth">here</a> for the full list).</p>
<p>The below shows the options available from the Vault CLI: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ vault
Usage: vault &lt;command&gt; <span style="color:#f92672">[</span>args<span style="color:#f92672">]</span>

Common commands:
    read        Read data and retrieves secrets
    write       Write data, configuration, and secrets
    delete      Delete secrets and configuration
    list        List data or secrets
    login       Authenticate locally
    agent       Start a Vault agent
    server      Start a Vault server
    status      Print seal and HA status
    unwrap      Unwrap a wrapped secret

Other commands:
    audit          Interact with audit devices
    auth           Interact with auth methods
    debug          Runs the debug command
    kv             Interact with Vault<span style="color:#960050;background-color:#1e0010">&#39;</span>s Key-Value storage
    lease          Interact with leases
    monitor        Stream log messages from a Vault server
    namespace      Interact with namespaces
    operator       Perform operator-specific tasks
    path-help      Retrieve API help <span style="color:#66d9ef">for</span> paths
    plugin         Interact with Vault plugins and catalog
    policy         Interact with policies
    print          Prints runtime configurations
    secrets        Interact with secrets engines
    ssh            Initiate an SSH session
    token          Interact with tokens
</code></pre></div><h3 id="using-terraform-with-vault">Using Terraform with Vault</h3>
<p>Terraform has a <a href="https://registry.terraform.io/providers/hashicorp/vault/latest/docs">Vault provider</a> that is well supported (as both projects were created by Hashicorp). This means that not only can we create our Kubernetes cluster using Terraform, but we can also setup our roles, secrets and policies in Vault at the same time.</p>
<p>Being able to use Terraform for this means that everything we need to create a cluster can be committed to a code repository (e.g. Git). You can also create reusable <a href="https://www.terraform.io/docs/modules/index.html">modules</a>. This means that defining a cluster can be as simple as defining a few variables, with the modules taking care of everything else.</p>
<p>To use the Vault provider, define something like the following: -</p>
<p><strong>providers.tf</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">terraform</span> {
 required_version <span style="color:#f92672">=</span> &#34;&gt;<span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>.<span style="color:#ae81ff">13</span><span style="color:#960050;background-color:#1e0010">&#34;</span>
  <span style="color:#66d9ef">required_providers</span> {
    vault <span style="color:#f92672">=</span> {
      source <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;hashicorp/vault&#34;</span>
      version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;2.15.0&#34;</span>
    }
  }
}
</code></pre></div><p>This will bring in the Vault provider, allowing you to create Vault resources.</p>
<p>The following Terraform configuration create the necessary Vault components for the clusters to use: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#75715e"># Create the generic auth backend - User and password
</span><span style="color:#75715e"></span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;vault_auth_backend&#34; &#34;approle&#34;</span> {
  type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;approle&#34;</span>
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e"># KV Path Mount
</span><span style="color:#75715e"></span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;vault_mount&#34; &#34;secret-kv&#34;</span> {
  path        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;secret&#34;</span>
  type        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;kv&#34;</span>
  options <span style="color:#f92672">=</span> {
    &#34;version&#34; <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;2&#34;</span>
  }
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e"># Create a policy that can only write the kubeadm token
</span><span style="color:#75715e"></span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;vault_policy&#34; &#34;kubeadm_token_write&#34;</span> {
    name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;kubeadm_token_write&#34;</span>

    policy <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">&lt;&lt;</span><span style="color:#66d9ef">EOT</span>
<span style="color:#66d9ef">path</span> <span style="color:#e6db74">&#34;secret/kubeadm_token&#34;</span> {
  capabilities <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;update&#34;, &#34;create&#34;</span>]
}
<span style="color:#66d9ef">path</span> <span style="color:#e6db74">&#34;secret/data/kubeadm_token&#34;</span> {
  capabilities <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;update&#34;, &#34;create&#34;</span>]
}
<span style="color:#66d9ef">EOT</span>
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e"># Create a policy that can only read the kubeadm token
</span><span style="color:#75715e"></span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;vault_policy&#34; &#34;kubeadm_token_read&#34;</span> {
    name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;kubeadm_token_read&#34;</span>

    policy <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">&lt;&lt;</span><span style="color:#66d9ef">EOT</span>
<span style="color:#66d9ef">path</span> <span style="color:#e6db74">&#34;secret/kubeadm_token&#34;</span> {
  capabilities <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;read&#34;</span>]
}
<span style="color:#66d9ef">path</span> <span style="color:#e6db74">&#34;secret/data/kubeadm_token&#34;</span> {
  capabilities <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;read&#34;</span>]
}
<span style="color:#66d9ef">EOT</span>
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e"># KubeADM Read User
</span><span style="color:#75715e"></span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;vault_approle_auth_backend_role&#34; &#34;kubeadm_read&#34;</span> {
  backend   <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault_auth_backend</span>.<span style="color:#66d9ef">approle</span>.<span style="color:#66d9ef">path</span>
  role_name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;kubeadm-read&#34;</span>
  token_policies  <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;kubeadm_token_read&#34;</span>]
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;vault_approle_auth_backend_role_secret_id&#34; &#34;kubeadm_read&#34;</span> {
  backend   <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault_auth_backend</span>.<span style="color:#66d9ef">approle</span>.<span style="color:#66d9ef">path</span>
  role_name <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault_approle_auth_backend_role</span>.<span style="color:#66d9ef">kubeadm_read</span>.<span style="color:#66d9ef">role_name</span>
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e"># KubeADM Write User
</span><span style="color:#75715e"></span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;vault_approle_auth_backend_role&#34; &#34;kubeadm_write&#34;</span> {
  backend   <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault_auth_backend</span>.<span style="color:#66d9ef">approle</span>.<span style="color:#66d9ef">path</span>
  role_name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;kubeadm-write&#34;</span>
  token_policies  <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;kubeadm_token_write&#34;</span>]
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;vault_approle_auth_backend_role_secret_id&#34; &#34;kubeadm_write&#34;</span> {
  backend   <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault_auth_backend</span>.<span style="color:#66d9ef">approle</span>.<span style="color:#66d9ef">path</span>
  role_name <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault_approle_auth_backend_role</span>.<span style="color:#66d9ef">kubeadm_write</span>.<span style="color:#66d9ef">role_name</span>
}
</code></pre></div><p>There are a few terms we need to cover here: -</p>
<ul>
<li>Auth Backend - This is how a user and/or application will login to Vault (to be able to retrieve a secret)</li>
<li>Mount - This is the kind of secret backend used for a particular path
<ul>
<li>Many other backends (known as Secrets Engines) are <a href="https://www.vaultproject.io/docs/secrets">available</a></li>
</ul>
</li>
<li>Path - Where in Vault the secret is stored (e.g. <code>/secret/my-token</code> or <code>/another-path/another-token</code>)
<ul>
<li>It is worth noting that a secret (e.g. <code>kubeadm_token</code>) can have multiple keys and values</li>
</ul>
</li>
<li>App Role - This is effectively a service account that has access to Vault
<ul>
<li>App roles come with a Role ID and Secret ID that is used to authenticate against Vault with (similar to AWS Access Keys and Secret Keys)</li>
<li>An app role can be used to retrieve a token, which can be used in future Vault requests (via the CLI or API)</li>
</ul>
</li>
<li>Policy - A policy defines what a user and/or applications is allowed to do
<ul>
<li>This can be restricted to certain paths</li>
<li>It can also restrict operations (e.g. <code>read</code> only, or <code>update</code> and <code>create</code> only)</li>
</ul>
</li>
</ul>
<p>To summarize what we are doing in the above, we are: -</p>
<ul>
<li>Creating the <code>approle</code> auth backend (i.e. allowing an <code>approle</code> to login to Vault)</li>
<li>Creating the <code>secret</code> path, as a generic Key-Value store</li>
<li>Create two <code>approles</code> (<strong>kubeadm-read</strong> and <strong>kubeadm-write</strong>)</li>
<li>Create the associated policies for the <code>approles</code></li>
<li>Ensure a <strong>secret ID</strong> is created for each <code>approle</code></li>
</ul>
<p>The <strong>kubeadm-write</strong> app role cannot read the secret (and doesn&rsquo;t need to). Similarly the <strong>kubeadm-read</strong> app role only needs to read the secret, so it is not given &ldquo;update&rdquo; or &ldquo;create&rdquo; permissions.</p>
<p>In our scenario, the only Kubernetes node that needs the <strong>kubeadm-write</strong> role is the first Control Plane node. It knows the <code>kubeadm</code> secret values when it creates them, which are then added to Vault.</p>
<p>All other nodes can use the <strong>kubeadm-read</strong>. They read the values of the secret and use them as part of the <strong>join</strong> process (i.e. joining to the first Control Plane node).</p>
<h2 id="libvirt-terraform-provider">Libvirt Terraform Provider</h2>
<p>In previous versions of Terraform (0.12 and below), adding third-party providers involved installing from source and moving providers into certain directories. From version 0.13 and above, the process is far simpler. All you need to do now is add something like the below to your Providers definition: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">terraform</span> {
 required_version <span style="color:#f92672">=</span> &#34;&gt;<span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>.<span style="color:#ae81ff">13</span><span style="color:#960050;background-color:#1e0010">&#34;</span>
  <span style="color:#66d9ef">required_providers</span> {
    libvirt <span style="color:#f92672">=</span> {
      source  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;dmacvicar/libvirt&#34;</span>
      version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0.6.2&#34;</span>
    }
    vault <span style="color:#f92672">=</span> {
      source <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;hashicorp/vault&#34;</span>
      version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;2.15.0&#34;</span>
    }
  }
}
</code></pre></div><p>If you then run <code>terraform init</code>, it will bring in the <strong>libvirt</strong> provider.</p>
<p>The provider itself covers the creation of virtual machines in KVM/libvirt and related resources, including: -</p>
<ul>
<li>Creating &ldquo;domains&rdquo; (i.e. virtual machines)</li>
<li>Creating networks</li>
<li>Creating pools for storage</li>
<li>Creating volumes (i.e. disks to install machines on)</li>
<li>Support for CloudInit or CoreOS Ignition for first-time boot configuration</li>
</ul>
<p>As with the <a href="https://yetiops.net/posts/proxmox-terraform-cloudinit-saltstack-prometheus/">Proxmox Provider</a> I covered in a previous post, it as expansive as something like the AWS or Azure providers. This is because KVM/libvirt is just a hypervisor. It does not provide firewalling, managed databases, functions as a service (e.g. AWS Lambdas) or anything else. This means it only needs to cover enough to build and run a virtual machine.</p>
<h2 id="creating-the-initial-storage-pool">Creating the initial storage pool</h2>
<p>I am running this cluster on a machine that already runs KVM, so a lot of the basic networking and storage exists already. I did create a storage pool just for the Kubernetes nodes using Terraform though: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;libvirt_pool&#34; &#34;libvirt-default-pool&#34;</span> {
  name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;kubernetes&#34;</span>
  type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;dir&#34;</span>
  path <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">libvirt_disk_path</span>
}

<span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;libvirt_disk_path&#34;</span> {
  description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;path for libvirt pool&#34;</span>
  default     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/var/lib/libvirt/images/kubernetes&#34;</span>
}
</code></pre></div><p>There are multiple options for the type of pool you can create (everything from directories to iSCSI to ZFS). As I&rsquo;m not using any shared storage (like iSCSI) or advanced disk management (like ZFS), I have opted for using a directory.</p>
<h2 id="creating-the-first-control-plane-node">Creating the first Control Plane node</h2>
<p>To create the first Control Plane node, we need to create a volume, a domain and our Cloud-Init configuration. The following Terraform configuration covers all of this: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;libvirt_volume&#34; &#34;k8s-cp-01&#34;</span> {
  name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;k8s-cp-01&#34;</span>
  pool <span style="color:#f92672">=</span> <span style="color:#66d9ef">libvirt_pool</span>.<span style="color:#66d9ef">libvirt</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">default</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">pool</span>.<span style="color:#66d9ef">name</span>
  source <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/var/lib/libvirt/images/debian10-kube.qcow2&#34;</span>
  format <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;qcow2&#34;</span>
}

<span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;template_file&#34; &#34;k8s-cp-01&#34;</span> {
  template <span style="color:#f92672">=</span> <span style="color:#66d9ef">file</span>(<span style="color:#e6db74">&#34;${path.module}/cloud-init/debian-primary.cfg&#34;</span>)
  vars <span style="color:#f92672">=</span> {
    SSH_KEY <span style="color:#f92672">=</span> <span style="color:#66d9ef">file</span>(<span style="color:#e6db74">&#34;~/.ssh/id_rsa.pub&#34;</span>)
    VAULT_KW_ROLE_ID <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault_approle_auth_backend_role</span>.<span style="color:#66d9ef">kubeadm_write</span>.<span style="color:#66d9ef">role_id</span>
    VAULT_KW_SECRET_ID <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault_approle_auth_backend_role_secret_id</span>.<span style="color:#66d9ef">kubeadm_write</span>.<span style="color:#66d9ef">secret_id</span>
    HOSTNAME <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;k8s-cp-01&#34;</span>
  }
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;libvirt_cloudinit_disk&#34; &#34;k8s-cp-01&#34;</span> {
  name           <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;k8s-cp-01.iso&#34;</span>
  user_data      <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">template_file</span>.<span style="color:#66d9ef">k8s</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">cp</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">01</span>.<span style="color:#66d9ef">rendered</span>
  pool           <span style="color:#f92672">=</span> <span style="color:#66d9ef">libvirt_pool</span>.<span style="color:#66d9ef">libvirt</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">default</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">pool</span>.<span style="color:#66d9ef">name</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;libvirt_domain&#34; &#34;k8s-cp-01&#34;</span> {
  name   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;k8s-cp-01&#34;</span>
  memory <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;4096&#34;</span>
  vcpu   <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>

  cloudinit <span style="color:#f92672">=</span> <span style="color:#66d9ef">libvirt_cloudinit_disk</span>.<span style="color:#66d9ef">k8s</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">cp</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">01</span>.<span style="color:#66d9ef">id</span>

  <span style="color:#66d9ef">network_interface</span> {
    network_name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;mgmt-bridge&#34;</span>
    mac          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;52:54:00:FE:80:01&#34;</span>
  }<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">  # IMPORTANT: this is a known bug on cloud images, since they expect a console
</span><span style="color:#75715e">  # we need to pass it
</span><span style="color:#75715e">  # https://bugs.launchpad.net/cloud-images/+bug/1573095
</span><span style="color:#75715e"></span>  <span style="color:#66d9ef">console</span> {
    type        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;pty&#34;</span>
    target_port <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0&#34;</span>
    target_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;serial&#34;</span>
  }

  <span style="color:#66d9ef">console</span> {
    type        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;pty&#34;</span>
    target_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;virtio&#34;</span>
    target_port <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;1&#34;</span>
  }

  <span style="color:#66d9ef">disk</span> {
    volume_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">libvirt_volume</span>.<span style="color:#66d9ef">k8s</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">cp</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">01</span>.<span style="color:#66d9ef">id</span>
  }

  <span style="color:#66d9ef">graphics</span> {
    type        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vnc&#34;</span>
    listen_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;address&#34;</span>
    autoport    <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
  }
}
</code></pre></div><p>In this, we create our domain (with 4G of memory and 2 vCPUs), create a volume (that is cloned from the <code>debian10-kube.qcow2</code> image we created with Packer) and create our Cloud-Init configuration. We pass in our SSH public key, the hostname, and also the Vault app role Role ID and Secret ID for the <strong>kubeadm-write</strong> role into the Cloud-Init configuration template. This allows us to use a fairly generic Cloud-Init template for the Control Plane node.</p>
<h3 id="cloud-init-template">Cloud-Init Template</h3>
<p>Our template to take our Packer-created image and turn it into our first Control Plane node looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e">#cloud-config</span>
<span style="color:#66d9ef">ssh_pwauth</span>: <span style="color:#66d9ef">true</span>
<span style="color:#66d9ef">fqdn</span>: ${HOSTNAME}.behemoth.yetiops.lab

<span style="color:#66d9ef">packages</span>:
 - prometheus-node-exporter
 - sudo

<span style="color:#66d9ef">users</span>:
  - <span style="color:#66d9ef">name</span>: yetiops
    <span style="color:#66d9ef">groups</span>: sudo
    <span style="color:#66d9ef">shell</span>: /bin/bash
    <span style="color:#66d9ef">sudo</span>: [<span style="color:#e6db74">&#39;ALL=(ALL) NOPASSWD:ALL&#39;</span>]
    <span style="color:#66d9ef">ssh-authorized-keys</span>:
      - ${SSH_KEY}

<span style="color:#66d9ef">kubeadm_init</span>:
  - <span style="color:#75715e">&amp;kube_init</span> <span style="color:#e6db74">|
</span><span style="color:#e6db74">    kubeadm init --pod-network-cidr=10.244.0.0/16 --control-plane-endpoint &#34;k8s-api.behemoth.yetiops.lab&#34; --upload-certs &gt; /root/kubeadm_output</span>
    grep -Ei <span style="color:#e6db74">&#34;\-\-token|discovery-token-ca-cert-hash|\-\-certificate-key&#34;</span> /root/kubeadm_output | tr <span style="color:#e6db74">&#39;\n&#39;</span> <span style="color:#e6db74">&#39; &#39;</span> | sed -e <span style="color:#e6db74">&#39;s/kubeadm/\nkubeadm/g&#39;</span> | grep -i control | sed -e <span style="color:#e6db74">&#39;s/--/\n/g&#39;</span> -e <span style="color:#e6db74">&#39;s/ \\//g&#39;</span> | grep -Eiv <span style="color:#e6db74">&#34;control-plane|kubeadm join&#34;</span> &gt; /root/kubeadm_join


<span style="color:#66d9ef">vault_kv</span>:
  - <span style="color:#75715e">&amp;vaultkv</span> <span style="color:#e6db74">|
</span><span style="color:#e6db74">    export VAULT_ADDR=&#34;https://10.15.32.240:8200&#34;</span>
    export VAULT_SKIP_VERIFY=<span style="color:#66d9ef">true</span>
    export VAULT_TOKEN=$(vault write -field=token auth/approle/login role_id=${VAULT_KW_ROLE_ID} secret_id=${VAULT_KW_SECRET_ID})
    vault kv put secret/kubeadm_token token=$(grep -Ei <span style="color:#e6db74">&#34;^token&#34;</span> /root/kubeadm_join | awk <span style="color:#e6db74">&#39;{print $2}&#39;</span>) discovery-token-ca-cert-hash=$(grep -Ei <span style="color:#e6db74">&#34;^discovery-token-ca-cert-hash&#34;</span> /root/kubeadm_join | awk <span style="color:#e6db74">&#39;{print $2}&#39;</span>) certificate-key=$(grep -Ei <span style="color:#e6db74">&#34;^certificate-key&#34;</span> /root/kubeadm_join | awk <span style="color:#e6db74">&#39;{print $2}&#39;</span>)

<span style="color:#66d9ef">win_prep</span>:
  - <span style="color:#75715e">&amp;winprep</span> <span style="color:#e6db74">|
</span><span style="color:#e6db74">    export KUBECONFIG=/etc/kubernetes/admin.conf</span>
    curl -L https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml | sed <span style="color:#e6db74">&#39;s/vxlan&#34;/vxlan&#34;,\n        &#34;VNI&#34; : 4096,\n        &#34;Port&#34;: 4789/g&#39;</span> | kubectl apply -f <span style="color:#e6db74">-
</span><span style="color:#e6db74">    curl -L https://github.com/kubernetes-sigs/sig-windows-tools/releases/latest/download/kube-proxy.yml | sed &#39;s/VERSION/v1.19.3/g&#39; | kubectl apply -f -</span>
    kubectl apply -f https://github.com/kubernetes-sigs/sig-windows-tools/releases/latest/download/flannel-overlay.yml



<span style="color:#66d9ef">runcmd</span>:
 - [ sh, -c, <span style="color:#75715e">*kube_init</span> ]
 - [ sh, -c, <span style="color:#75715e">*vaultkv</span> ]
 - [ sh, -c, <span style="color:#75715e">*winprep</span> ]
</code></pre></div><p>The above template uses the <code>cloud-config</code> format. This allows us to use some in-built functions (e.g. user creation, setting hostnames, installing packages). We can then create our own functions (i.e. the <code>kubeadm_init</code>, <code>vault_kv</code> and <code>win_prep</code> sections) for more those not already covered by <code>cloud-config</code>.</p>
<p>The first function (<code>kubeadm_init</code>) has two parts to it.</p>
<p>First, it runs <code>kubeadm init</code>, supplying our Pod Network (using the common <code>10.244.0.0/16</code> subnet), specifies that the endpoint (i.e. the Kubernetes URL) will be <code>k8s-api.behemoth.yetiops.lab</code> (served by <code>relayd</code> on our OpenBSD router appliance in the lab setup), uses the <code>upload-certs</code> functionality to upload the Control Plane certificates to a Kubernetes secret), and then places the output of the command into a file called <code>/root/kubeadm_output</code>.</p>
<p>Once this is complete, it finds the lines containing our tokens, our certificate hashes and the certificate key, parses the output, removes any unneeded text. Finally, it creates a file called <code>/root/kubeadm_join</code> containing the values: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">token $TOKEN_CONTENTS
discovery-token-ca-cert-hash $DISCOVERY_CERT_HASH
certificate-key $CERTIFICATE-KEY
</code></pre></div><p>The second function (<code>vaultkv</code>) is where we add our details to Vault. We first specify the address of Vault, and to skip verification of the certificate (by default, Vault&rsquo;s certificate is self-signed). Next, we use the <code>vault write</code> command to login to Vault and retrieve our Vault API token, exporting it as the <code>VAULT_TOKEN</code> environment variable. The Vault CLI will look for this environment variable by default when performing any further operations.</p>
<p>Once we have the Vault token, we create a Secret in Vault called <code>kubeadm_token</code>. For each of the fields <strong>token</strong>, <strong>discovery-token-ca-cert-hash</strong> and <strong>certificate-key</strong> we retrieve the values from the <code>/root/kubeadm_join</code> file created in the previous step and then add them to the secret. In vault, this looks something like the below: -</p>
<p><img src="/img/kubernetes-win-linux/vault-kubeadm-token.png" alt="Vault UI - KubeADM Token"></p>
<p>Once this is done, we move onto the <code>win_prep</code> function. This section does the following: -</p>
<ul>
<li>Exports the variable KUBECONFIG, referencing the <code>admin.conf</code> configuration created by <strong>kubeadm</strong></li>
<li>We retrieve the Flannel CNI (network provider) YAML definition, add in a limit on the VNI (VXLAN network identifier) and port used for VxLAN communication, and apply it</li>
<li>We retrieve the Windows Kube Proxy YAML definition, update the version to match the Kubernetes version we are using (in this case, v1.19.3) and apply it</li>
<li>We apply the Flannel Overlay YAML definition directly</li>
</ul>
<p>These steps are taken from the <a href="https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/adding-windows-nodes/">Adding Windows Nodes</a> <strong>kubeadm</strong> documentation. Without these, Windows will not communicate with any of the Linux nodes in the cluster, meaning that it will not function as part of the cluster. As these are defined on the first Control Plane node before any other node joins, all the other nodes will not need to have the configuration reapplied.</p>
<h2 id="why-cloud-init-and-cloud-config">Why Cloud-Init and Cloud-Config?</h2>
<p>The reason for using Cloud-Init and <code>cloud-config</code> is for two reasons: -</p>
<ul>
<li>Simplicity - Any commands we could run at the command line can be ran inside of this template</li>
<li>Less dependencies - If we were to use another tool (e.g. Ansible), other dependencies would be required</li>
</ul>
<p>It does have the disadvantage of not being idempotent, in that even if some of the environment variables are already set, some of the configuration has already been applied, or packages have already been installed, it will try again anyway.</p>
<p>In a production scenario, a configuration management tool like Ansible would allow for idempotence, dependency checking, interacting directly with the Kubernetes API after the initialization stages are complete and more. However as this is simply to show the basics of getting this up and running, I have forgone this step for now.</p>
<h2 id="creating-the-other-control-plane-nodes">Creating the other Control Plane nodes</h2>
<p>The other Control Plane nodes are configured very similarly to the first Control Plane: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;libvirt_volume&#34; &#34;k8s-cp-02&#34;</span> {
  name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;k8s-cp-02&#34;</span>
  pool <span style="color:#f92672">=</span> <span style="color:#66d9ef">libvirt_pool</span>.<span style="color:#66d9ef">libvirt</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">default</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">pool</span>.<span style="color:#66d9ef">name</span>
  source <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/var/lib/libvirt/images/debian10-kube.qcow2&#34;</span>
  format <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;qcow2&#34;</span>
}

<span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;template_file&#34; &#34;k8s-cp-02&#34;</span> {
  template <span style="color:#f92672">=</span> <span style="color:#66d9ef">file</span>(<span style="color:#e6db74">&#34;${path.module}/cloud-init/debian-secondary.cfg&#34;</span>)
  vars <span style="color:#f92672">=</span> {
    SSH_KEY <span style="color:#f92672">=</span> <span style="color:#66d9ef">file</span>(<span style="color:#e6db74">&#34;~/.ssh/id_rsa.pub&#34;</span>)
    VAULT_KR_ROLE_ID <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault_approle_auth_backend_role</span>.<span style="color:#66d9ef">kubeadm_read</span>.<span style="color:#66d9ef">role_id</span>
    VAULT_KR_SECRET_ID <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault_approle_auth_backend_role_secret_id</span>.<span style="color:#66d9ef">kubeadm_read</span>.<span style="color:#66d9ef">secret_id</span>
    HOSTNAME <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;k8s-cp-02&#34;</span>
  }
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;libvirt_cloudinit_disk&#34; &#34;k8s-cp-02&#34;</span> {
  name           <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;k8s-cp-02.iso&#34;</span>
  user_data      <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">template_file</span>.<span style="color:#66d9ef">k8s</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">cp</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">02</span>.<span style="color:#66d9ef">rendered</span>
  pool           <span style="color:#f92672">=</span> <span style="color:#66d9ef">libvirt_pool</span>.<span style="color:#66d9ef">libvirt</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">default</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">pool</span>.<span style="color:#66d9ef">name</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;libvirt_domain&#34; &#34;k8s-cp-02&#34;</span> {
  name   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;k8s-cp-02&#34;</span>
  memory <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;4096&#34;</span>
  vcpu   <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>

  cloudinit <span style="color:#f92672">=</span> <span style="color:#66d9ef">libvirt_cloudinit_disk</span>.<span style="color:#66d9ef">k8s</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">cp</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">02</span>.<span style="color:#66d9ef">id</span>

  <span style="color:#66d9ef">network_interface</span> {
    network_name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;mgmt-bridge&#34;</span>
    mac          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;52:54:00:FE:80:02&#34;</span>
  }<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">  # IMPORTANT: this is a known bug on cloud images, since they expect a console
</span><span style="color:#75715e">  # we need to pass it
</span><span style="color:#75715e">  # https://bugs.launchpad.net/cloud-images/+bug/1573095
</span><span style="color:#75715e"></span>  <span style="color:#66d9ef">console</span> {
    type        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;pty&#34;</span>
    target_port <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0&#34;</span>
    target_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;serial&#34;</span>
  }

  <span style="color:#66d9ef">console</span> {
    type        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;pty&#34;</span>
    target_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;virtio&#34;</span>
    target_port <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;1&#34;</span>
  }

  <span style="color:#66d9ef">disk</span> {
    volume_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">libvirt_volume</span>.<span style="color:#66d9ef">k8s</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">cp</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">02</span>.<span style="color:#66d9ef">id</span>
  }

  <span style="color:#66d9ef">graphics</span> {
    type        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vnc&#34;</span>
    listen_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;address&#34;</span>
    autoport    <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
  }

  depends_on <span style="color:#f92672">=</span> [
    <span style="color:#66d9ef">libvirt_domain</span>.<span style="color:#66d9ef">k8s</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">cp</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">01</span>
  ]
}
</code></pre></div><p>The main difference you will see here is that we use a different Cloud-Init template (<code>debian-secondary.cfg</code> rather than <code>debian-primary.cfg</code>), and that we are supplying the Vault Role ID and Secret ID for the <strong>kubeadm-read</strong> app role. This is because the subsequent Control Plane nodes only need to read the secret we created in the previous step, not write or update it.</p>
<p>Other than that, all the other details are the same.</p>
<h3 id="cloud-init-template-1">Cloud-Init Template</h3>
<p>The template to build our subsequent Control Plane nodes looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e">#cloud-config</span>
<span style="color:#66d9ef">ssh_pwauth</span>: <span style="color:#66d9ef">true</span>
<span style="color:#66d9ef">fqdn</span>: ${HOSTNAME}.behemoth.yetiops.lab

<span style="color:#66d9ef">packages</span>:
 - prometheus-node-exporter
 - sudo

<span style="color:#66d9ef">users</span>:
  - <span style="color:#66d9ef">name</span>: yetiops
    <span style="color:#66d9ef">groups</span>: sudo
    <span style="color:#66d9ef">shell</span>: /bin/bash
    <span style="color:#66d9ef">sudo</span>: [<span style="color:#e6db74">&#39;ALL=(ALL) NOPASSWD:ALL&#39;</span>]
    <span style="color:#66d9ef">ssh-authorized-keys</span>:
      - ${SSH_KEY}


<span style="color:#66d9ef">write_files</span>:
  - <span style="color:#66d9ef">path</span>: /usr/local/bin/vault_kube.sh
    <span style="color:#66d9ef">owner</span>: root
    <span style="color:#66d9ef">permissions</span>: <span style="color:#e6db74">&#39;755&#39;</span>
    <span style="color:#66d9ef">content</span>: <span style="color:#e6db74">|
</span><span style="color:#e6db74">     #!/bin/bash</span>

     while <span style="color:#66d9ef">true</span>; do

     echo <span style="color:#e6db74">&#34;Check for token in Vault&#34;</span>

     vault kv get -field=discovery-token-ca-cert-hash secret/kubeadm_token

     if [ $? == <span style="color:#ae81ff">0</span> ]; then
     echo <span style="color:#e6db74">&#34;Token found&#34;</span>
     break
     fi

     echo <span style="color:#e6db74">&#34;Token not found&#34;</span>
     sleep <span style="color:#ae81ff">10</span>

     done

<span style="color:#66d9ef">vault_kubetoken</span>:
  - <span style="color:#75715e">&amp;vaultkubetoken</span> <span style="color:#e6db74">|
</span><span style="color:#e6db74">    export VAULT_ADDR=&#34;https://10.15.32.240:8200&#34;</span>
    export VAULT_SKIP_VERIFY=<span style="color:#66d9ef">true</span>
    export VAULT_TOKEN=$(vault write -field=token auth/approle/login role_id=${VAULT_KR_ROLE_ID} secret_id=${VAULT_KR_SECRET_ID})
    bash /usr/local/bin/vault_kube.sh


<span style="color:#66d9ef">kubeadm_join</span>:
  - <span style="color:#75715e">&amp;kubeadmjoin</span> <span style="color:#e6db74">|
</span><span style="color:#e6db74">    export VAULT_ADDR=&#34;https://10.15.32.240:8200&#34;</span>
    export VAULT_SKIP_VERIFY=<span style="color:#66d9ef">true</span>
    export VAULT_TOKEN=$(vault write -field=token auth/approle/login role_id=${VAULT_KR_ROLE_ID} secret_id=${VAULT_KR_SECRET_ID})
    export KUBEADM_DISCOVERY_TOKEN=$(vault kv get -field=discovery-token-ca-cert-hash secret/kubeadm_token)
    export KUBEADM_TOKEN=$(vault kv get -field=token secret/kubeadm_token)
    export KUBEADM_CERT_KEY=$(vault kv get -field=certificate-key secret/kubeadm_token)
    kubeadm join k8s-api.behemoth.yetiops.lab:<span style="color:#ae81ff">6443</span> --token $KUBEADM_TOKEN --discovery-token-ca-cert-hash $KUBEADM_DISCOVERY_TOKEN --control-plane --certificate-key $KUBEADM_CERT_KEY


<span style="color:#66d9ef">runcmd</span>:
 - [ sh, -c, <span style="color:#75715e">*vaultkubetoken</span> ]
 - [ sh, -c, <span style="color:#75715e">*kubeadmjoin</span> ]
</code></pre></div><p>In addition to the users, packages and setting the hostname of the node, we also create a script (using <code>write_files</code> called <code>vault_kube.sh</code>. This is a small script that checks the contents of the <code>kubeadm_token</code> secret in Vault. If it is empty, or the command fails for any other reason, it sleeps for 10 seconds and then checks the contents again. If the secret contains the <strong>discovery-token-ca-cert-hash</strong> field, the loop breaks and ends the script.</p>
<p>The reason for creating this script is so that rather than having to start the nodes in a certain order (i.e. create the Control Plane node, and then only bring up the other nodes when it is finished configuring itself), we can bring all the nodes up at once. Only once the Vault secret is populated will the subsequent nodes (Control Plane and Workers) join the cluster.</p>
<p>Before we run this script, we perform the same steps as we did in creating the first node to retrieve our Vault token.</p>
<p>After this, we then move on to joining the cluster. First, we login to Vault again (because the environment variables may have been cleared when the previous function ends). We then retrieve all of the fields and create corresponding environment variables. Finally, we use the <code>kubeadm join</code> command, supplying the <strong>token</strong>, <strong>discovery-token-ca-cert-hash</strong> and <strong>certicate-key</strong>, along with the <code>--control-plane</code> flag to note that this node will form part of the Control Plane (rather than being a worker).</p>
<p>Once all this is complete, the Control Plane nodes will join the cluster, providing a highly available Control Plane (i.e. if one goes down, the others still function as the Control Plane).</p>
<h2 id="creating-the-linux-workers">Creating the Linux Workers</h2>
<p>The Linux worker node configuration is very similar to what we use in the previous two steps: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl">  name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;k8s-worker-01&#34;</span>
  pool <span style="color:#f92672">=</span> <span style="color:#66d9ef">libvirt_pool</span>.<span style="color:#66d9ef">libvirt</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">default</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">pool</span>.<span style="color:#66d9ef">name</span>
  source <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/var/lib/libvirt/images/debian10-kube.qcow2&#34;</span>
  format <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;qcow2&#34;</span>
}

<span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;template_file&#34; &#34;k8s-worker-01&#34;</span> {
  template <span style="color:#f92672">=</span> <span style="color:#66d9ef">file</span>(<span style="color:#e6db74">&#34;${path.module}/cloud-init/debian-workers.cfg&#34;</span>)
  vars <span style="color:#f92672">=</span> {
    SSH_KEY <span style="color:#f92672">=</span> <span style="color:#66d9ef">file</span>(<span style="color:#e6db74">&#34;~/.ssh/id_rsa.pub&#34;</span>)
    VAULT_KR_ROLE_ID <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault_approle_auth_backend_role</span>.<span style="color:#66d9ef">kubeadm_read</span>.<span style="color:#66d9ef">role_id</span>
    VAULT_KR_SECRET_ID <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault_approle_auth_backend_role_secret_id</span>.<span style="color:#66d9ef">kubeadm_read</span>.<span style="color:#66d9ef">secret_id</span>
    HOSTNAME <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;k8s-worker-01&#34;</span>
  }
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;libvirt_cloudinit_disk&#34; &#34;k8s-worker-01&#34;</span> {
  name           <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;k8s-worker-01.iso&#34;</span>
  user_data      <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">template_file</span>.<span style="color:#66d9ef">k8s</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">worker</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">01</span>.<span style="color:#66d9ef">rendered</span>
  pool           <span style="color:#f92672">=</span> <span style="color:#66d9ef">libvirt_pool</span>.<span style="color:#66d9ef">libvirt</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">default</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">pool</span>.<span style="color:#66d9ef">name</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;libvirt_domain&#34; &#34;k8s-worker-01&#34;</span> {
  name   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;k8s-worker-01&#34;</span>
  memory <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;4096&#34;</span>
  vcpu   <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>

  cloudinit <span style="color:#f92672">=</span> <span style="color:#66d9ef">libvirt_cloudinit_disk</span>.<span style="color:#66d9ef">k8s</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">worker</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">01</span>.<span style="color:#66d9ef">id</span>

  <span style="color:#66d9ef">network_interface</span> {
    network_name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;mgmt-bridge&#34;</span>
    mac          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;52:54:00:FE:80:04&#34;</span>
  }<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">  # IMPORTANT: this is a known bug on cloud images, since they expect a console
</span><span style="color:#75715e">  # we need to pass it
</span><span style="color:#75715e">  # https://bugs.launchpad.net/cloud-images/+bug/1573095
</span><span style="color:#75715e"></span>  <span style="color:#66d9ef">console</span> {
    type        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;pty&#34;</span>
    target_port <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0&#34;</span>
    target_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;serial&#34;</span>
  }

  <span style="color:#66d9ef">console</span> {
    type        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;pty&#34;</span>
    target_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;virtio&#34;</span>
    target_port <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;1&#34;</span>
  }

  <span style="color:#66d9ef">disk</span> {
    volume_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">libvirt_volume</span>.<span style="color:#66d9ef">k8s</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">worker</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">01</span>.<span style="color:#66d9ef">id</span>
  }

  <span style="color:#66d9ef">graphics</span> {
    type        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vnc&#34;</span>
    listen_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;address&#34;</span>
    autoport    <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
  }

  depends_on <span style="color:#f92672">=</span> [
    <span style="color:#66d9ef">libvirt_domain</span>.<span style="color:#66d9ef">k8s</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">cp</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">01</span>
  ]
}
</code></pre></div><p>The only difference between this, and how we created the other control plane nodes (i.e. not the first node) is that we are using a slightly different Cloud-Init template (<code>debian-workers.cfg</code> rather than <code>debian-secondary.cfg</code>). All other details are the same.</p>
<h3 id="cloud-init-template-2">Cloud-Init Template</h3>
<p>The Cloud-Init template for the Workers is almost identical to what we use for the other control plane nodes: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e">#cloud-config</span>
<span style="color:#66d9ef">ssh_pwauth</span>: <span style="color:#66d9ef">true</span>
<span style="color:#66d9ef">fqdn</span>: ${HOSTNAME}.behemoth.yetiops.lab

<span style="color:#66d9ef">packages</span>:
 - prometheus-node-exporter
 - sudo

<span style="color:#66d9ef">users</span>:
  - <span style="color:#66d9ef">name</span>: yetiops
    <span style="color:#66d9ef">groups</span>: sudo
    <span style="color:#66d9ef">shell</span>: /bin/bash
    <span style="color:#66d9ef">sudo</span>: [<span style="color:#e6db74">&#39;ALL=(ALL) NOPASSWD:ALL&#39;</span>]
    <span style="color:#66d9ef">ssh-authorized-keys</span>:
      - ${SSH_KEY}


<span style="color:#66d9ef">write_files</span>:
  - <span style="color:#66d9ef">path</span>: /usr/local/bin/vault_kube.sh
    <span style="color:#66d9ef">owner</span>: root
    <span style="color:#66d9ef">permissions</span>: <span style="color:#e6db74">&#39;755&#39;</span>
    <span style="color:#66d9ef">content</span>: <span style="color:#e6db74">|
</span><span style="color:#e6db74">     #!/bin/bash</span>

     while <span style="color:#66d9ef">true</span>; do

     echo <span style="color:#e6db74">&#34;Check for token in Vault&#34;</span>

     vault kv get -field=discovery-token-ca-cert-hash secret/kubeadm_token

     if [ $? == <span style="color:#ae81ff">0</span> ]; then
     echo <span style="color:#e6db74">&#34;Token found&#34;</span>
     break
     fi

     echo <span style="color:#e6db74">&#34;Token not found&#34;</span>
     sleep <span style="color:#ae81ff">10</span>

     done

<span style="color:#66d9ef">vault_kubetoken</span>:
  - <span style="color:#75715e">&amp;vaultkubetoken</span> <span style="color:#e6db74">|
</span><span style="color:#e6db74">    export VAULT_ADDR=&#34;https://10.15.32.240:8200&#34;</span>
    export VAULT_SKIP_VERIFY=<span style="color:#66d9ef">true</span>
    export VAULT_TOKEN=$(vault write -field=token auth/approle/login role_id=${VAULT_KR_ROLE_ID} secret_id=${VAULT_KR_SECRET_ID})
    bash /usr/local/bin/vault_kube.sh


<span style="color:#66d9ef">kubeadm_join</span>:
  - <span style="color:#75715e">&amp;kubeadmjoin</span> <span style="color:#e6db74">|
</span><span style="color:#e6db74">    export VAULT_ADDR=&#34;https://10.15.32.240:8200&#34;</span>
    export VAULT_SKIP_VERIFY=<span style="color:#66d9ef">true</span>
    export VAULT_TOKEN=$(vault write -field=token auth/approle/login role_id=${VAULT_KR_ROLE_ID} secret_id=${VAULT_KR_SECRET_ID})
    export KUBEADM_DISCOVERY_TOKEN=$(vault kv get -field=discovery-token-ca-cert-hash secret/kubeadm_token)
    export KUBEADM_TOKEN=$(vault kv get -field=token secret/kubeadm_token)
    kubeadm join k8s-api.behemoth.yetiops.lab:<span style="color:#ae81ff">6443</span> --token $KUBEADM_TOKEN --discovery-token-ca-cert-hash $KUBEADM_DISCOVERY_TOKEN


<span style="color:#66d9ef">runcmd</span>:
 - [ sh, -c, <span style="color:#75715e">*vaultkubetoken</span> ]
 - [ sh, -c, <span style="color:#75715e">*kubeadmjoin</span> ]
</code></pre></div><p>The only difference is that we do not need to specify the certificate key at any point in the join command, so we do not retrieve it. As with the control plane nodes created in the previous section, the Vault secret is checked every 10 seconds, and when it is populated, it will begin the process of joining the cluster.</p>
<h2 id="creating-the-windows-workers">Creating the Windows workers</h2>
<p>Creating the Windows workers, at least in terms of Terraform configuration, is similar to what we use for our Linux workers: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;libvirt_volume&#34; &#34;k8s-winworker-01&#34;</span> {
  name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;k8s-winworker-01t&#34;</span>
  pool <span style="color:#f92672">=</span> <span style="color:#66d9ef">libvirt_pool</span>.<span style="color:#66d9ef">libvirt</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">default</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">pool</span>.<span style="color:#66d9ef">name</span>
  source <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/var/lib/libvirt/images/win2019-kube.qcow2&#34;</span>
  format <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;qcow2&#34;</span>
}

<span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;template_file&#34; &#34;k8s-winworker-01&#34;</span> {
  template <span style="color:#f92672">=</span> <span style="color:#66d9ef">file</span>(<span style="color:#e6db74">&#34;${path.module}/cloud-init/windows-workers.cfg&#34;</span>)
  vars <span style="color:#f92672">=</span> {
    VAULT_KR_ROLE_ID <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault_approle_auth_backend_role</span>.<span style="color:#66d9ef">kubeadm_read</span>.<span style="color:#66d9ef">role_id</span>
    VAULT_KR_SECRET_ID <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault_approle_auth_backend_role_secret_id</span>.<span style="color:#66d9ef">kubeadm_read</span>.<span style="color:#66d9ef">secret_id</span>
    HOSTNAME <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;k8s-winworker-01&#34;</span>
  }
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;libvirt_cloudinit_disk&#34; &#34;k8s-winworker-01&#34;</span> {
  name           <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;k8s-winworker-01.iso&#34;</span>
  user_data      <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">template_file</span>.<span style="color:#66d9ef">k8s</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">winworker</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">01</span>.<span style="color:#66d9ef">rendered</span>
  meta_data      <span style="color:#f92672">=</span> <span style="color:#66d9ef">jsonencode</span>({
    <span style="color:#e6db74">&#34;instance-id&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">random_id</span>.<span style="color:#66d9ef">instance_id</span>.<span style="color:#66d9ef">hex</span>,
  })
  pool           <span style="color:#f92672">=</span> <span style="color:#66d9ef">libvirt_pool</span>.<span style="color:#66d9ef">libvirt</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">default</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">pool</span>.<span style="color:#66d9ef">name</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;random_id&#34; &#34;instance_id&#34;</span> {
  byte_length <span style="color:#f92672">=</span> <span style="color:#ae81ff">10</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;libvirt_domain&#34; &#34;k8s-winworker-01&#34;</span> {
  name   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;k8s-winworker-01&#34;</span>
  memory <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;4096&#34;</span>
  vcpu   <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>

  cloudinit <span style="color:#f92672">=</span> <span style="color:#66d9ef">libvirt_cloudinit_disk</span>.<span style="color:#66d9ef">k8s</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">winworker</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">01</span>.<span style="color:#66d9ef">id</span>

  <span style="color:#66d9ef">network_interface</span> {
    network_name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;mgmt-bridge&#34;</span>
    mac          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;52:54:00:FE:80:07&#34;</span>
  }<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">  # IMPORTANT: this is a known bug on cloud images, since they expect a console
</span><span style="color:#75715e">  # we need to pass it
</span><span style="color:#75715e">  # https://bugs.launchpad.net/cloud-images/+bug/1573095
</span><span style="color:#75715e"></span>  <span style="color:#66d9ef">console</span> {
    type        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;pty&#34;</span>
    target_port <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0&#34;</span>
    target_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;serial&#34;</span>
  }

  <span style="color:#66d9ef">console</span> {
    type        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;pty&#34;</span>
    target_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;virtio&#34;</span>
    target_port <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;1&#34;</span>
  }

  <span style="color:#66d9ef">disk</span> {
    volume_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">libvirt_volume</span>.<span style="color:#66d9ef">k8s</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">winworker</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">01</span>.<span style="color:#66d9ef">id</span>
  }

  <span style="color:#66d9ef">graphics</span> {
    type        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vnc&#34;</span>
    listen_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;address&#34;</span>
    autoport    <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
  }
}
</code></pre></div><p>The only major differences here are: -</p>
<ul>
<li>We use a different source image for our volume (<code>win2019-kube.qcow2</code> rather than <code>deb10-kube.qcow2</code>)</li>
<li>We create a randomly generated instance ID</li>
<li>We supply this instance ID as part of the <code>meta_data</code> Cloud-Init configuration</li>
</ul>
<p>The reason for the latter is that <a href="https://cloudbase.it/cloudbase-init/">Cloudbase-Init</a> (the Windows-equivalent of Cloud-Init) expects to see an Instance ID in the metadata section of the Cloud-Init disk. Without this, Cloudbase-Init will fail. The instance ID is not used in our configuration, so creating a randomly-generated ID is fine.</p>
<p>One point to note with the hostname is that due to NETBIOS limitations, the hostname will be constrained to 15 characters. If your hostname is longer than this, it will be trimmed down to the first 15 characters.</p>
<h3 id="cloud-init-template-3">Cloud-Init Template</h3>
<p>In the Cloud-Init definition, we supply part of it in the <code>cloud-config</code> format, and most of it in the Powershell format. The reason is because the <code>PrepareNode.ps1</code> script provided by the Kubernetes Windows Special Interest Group (<strong>windows-sig</strong>) is what we based most of our Packer image upon. We can remove all the parts which install dependencies, create directories and such, and keep only the node-specific sections.</p>
<p>This could have been rewritten in <code>cloud-config</code>, or even using an Ansible script, but for the purposes of this post I just used a trimmed down version of the script. It also means that if it changes in future (i.e. the <strong>windows-sig</strong> team make improvements to it), it will be much easier to implement them as part of this.</p>
<p>The following template will configure our Windows 2019-based node as a Kubernetes worker in our cluster: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">Content-Type</span>: multipart/mixed; boundary=<span style="color:#e6db74">&#34;===============1598784645116016685==&#34;</span>
<span style="color:#66d9ef">MIME-Version</span>: <span style="color:#ae81ff">1.0</span>

--===============<span style="color:#ae81ff">1598784645116016685</span>==
<span style="color:#66d9ef">Content-Type</span>: text/cloud-config; charset=<span style="color:#e6db74">&#34;us-ascii&#34;</span>
<span style="color:#66d9ef">MIME-Version</span>: <span style="color:#ae81ff">1.0</span>
<span style="color:#66d9ef">Content-Transfer-Encoding</span>: 7bit
<span style="color:#66d9ef">Content-Disposition</span>: attachment; filename=<span style="color:#e6db74">&#34;cloud-config&#34;</span>
<span style="color:#75715e"># cloud-config</span>
<span style="color:#66d9ef">set_hostname</span>: ${HOSTNAME}

--===============<span style="color:#ae81ff">1598784645116016685</span>==
<span style="color:#66d9ef">Content-Type</span>: text/x-cfninitdata; charset=<span style="color:#e6db74">&#34;us-ascii&#34;</span>
<span style="color:#66d9ef">MIME-Version</span>: <span style="color:#ae81ff">1.0</span>
<span style="color:#66d9ef">Content-Transfer-Encoding</span>: 7bit
<span style="color:#66d9ef">Content-Disposition</span>: attachment; filename=<span style="color:#e6db74">&#34;cfn-userdata&#34;</span>
<span style="color:#75715e">#ps1</span>

$global:Powershell = (Get-Command powershell).Source
$global:PowershellArgs = <span style="color:#e6db74">&#34;-ExecutionPolicy Bypass -NoProfile&#34;</span>
$global:StartKubeletScript = <span style="color:#e6db74">&#34;C:\k\StartKubelet.ps1&#34;</span>

$StartKubeletFileContent = <span style="color:#e6db74">&#39;$FileContent = Get-Content -Path &#34;/var/lib/kubelet/kubeadm-flags.env&#34;
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">$global:KubeletArgs = $FileContent.TrimStart(&#39;&#39;KUBELET_KUBEADM_ARGS=&#39;&#39;).Trim(&#39;&#39;&#34;&#39;&#39;)
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">docker network create -d nat host
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">$cmd = &#34;C:\k\kubelet.exe $global:KubeletArgs --cert-dir=$env:SYSTEMDRIVE\var\lib\kubelet\pki --config=/var/lib/kubelet/config.yaml --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf --hostname-override=$(hostname) --pod-infra-container-image=`&#34;mcr.microsoft.com/oss/kubernetes/pause:1.4.1`&#34; --enable-debugging-handlers --cgroups-per-qos=false --enforce-node-allocatable=`&#34;`&#34; --network-plugin=cni --resolv-conf=`&#34;`&#34; --log-dir=/var/log/kubelet --logtostderr=false --image-pull-progress-deadline=20m&#34;
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">Invoke-Expression $cmd&#39;</span>
Set-Content -Path $global:StartKubeletScript -Value $StartKubeletFileContent

Write-Host <span style="color:#e6db74">&#34;Registering kubelet service&#34;</span>
nssm install kubelet $global:Powershell $global:PowershellArgs $global:StartKubeletScript
nssm set kubelet DependOnService docker

$env:VAULT_ADDR = <span style="color:#e6db74">&#34;https://10.15.32.240:8200&#34;</span>
$env:VAULT_SKIP_VERIFY = <span style="color:#e6db74">&#34;true&#34;</span>
$env:VAULT_TOKEN = vault write -field=token auth/approle/login role_id=${VAULT_KR_ROLE_ID} secret_id=${VAULT_KR_SECRET_ID}

while($<span style="color:#66d9ef">true</span>)
{
     vault kv get -field=discovery-token-ca-cert-hash secret/kubeadm_token
     If ($lastExitCode -eq <span style="color:#e6db74">&#34;0&#34;</span>) {
       break
     }
     Start-Sleep -s <span style="color:#ae81ff">10</span>
}

$env:KUBEADM_DISCOVERY_TOKEN = vault kv get -field=discovery-token-ca-cert-hash secret/kubeadm_token
$env:KUBEADM_TOKEN = vault kv get -field=token secret/kubeadm_token
kubeadm join --ignore-preflight-errors=SystemVerification k8s-api.behemoth.yetiops.lab:<span style="color:#ae81ff">6443</span> --token $env:KUBEADM_TOKEN --discovery-token-ca-cert-hash $env:KUBEADM_DISCOVERY_TOKEN
</code></pre></div><p>This uses the Cloudbase-Init <strong>multipart</strong> support, to allow multiple formats (i.e. <code>cloud-config</code> and <code>powershell</code>) in the same Cloud-Init script. Setting the hostname via <code>cloud-config</code> is quite straightforward, meaning that we do not need to use Powershell to do so.</p>
<p>After this, we use the relevant sections of the <code>PrepareNode.ps1</code> script: -</p>
<ul>
<li>We set the absolute path of Powershell as a variable</li>
<li>We set the Powershell arguments as a variable (bypassing Execution Policies) allowing us to avoid any prompts/administrative logins to allow certain commands to work</li>
<li>We create a <code>StartKubelet.ps1</code> script that contains our <code>kubeadm-flags</code>
<ul>
<li>This script ensures the Docker host network is created, and runs Kubelet with the correct arguments</li>
</ul>
</li>
<li>We create a service using <a href="https://nssm.cc/">nssm</a> that will start the Kubelet
<ul>
<li>This service depends upon the Docker service starting and running</li>
</ul>
</li>
</ul>
<p>Once all of the above is done, the following steps are similar to the previous templates (other than using Powershell syntax). We login to Vault, start a loop that checks to see if our Vault secret is populated, and break out of the loop once it is. We then retrieve the <strong>discovery-token-ca-cert-hash</strong>  and <strong>token</strong> fields from the secret and use them in our <code>kubeadm join</code> command.</p>
<p>The reason we use <code>--ignore-preflight-errors=SystemVerification</code> is because we have installed the latest version of Docker for Windows in our Packer image. This is a few point releases higher than the current verified version of Docker that <strong>kubeadm</strong> supports, meaning it will fail to run without this argument. In a production scenario, install the verified version and then pin it, so that you are always running the verified version.</p>
<p>Once this is complete, the Windows node will join the cluster.</p>
<h3 id="points-to-note">Points to note</h3>
<p>When joining a Windows node to Kubernetes, you&rsquo;ll need to be aware of a couple of things.</p>
<p>First, if you need to debug the node you will need to use RDP (unless you install <a href="https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse">OpenSSH</a> during the Packer image creation).</p>
<p>Secondly, Windows-based containers are usually quite large. Some of the images can be multiple gigabytes in size to run a basic web server. Compared to something like a CentOS, Ubuntu or Debian-based image that is in the hundreds of megabytes typically, or even a &ldquo;scratch&rdquo; container running a Go binary that is 15-20 megabytes in size, Windows containers are orders of magnitude larger. This includes the Kubernetes images (i.e. the <code>kube-proxy</code> and <code>kube-flannel</code> containers, which are both around 5Gb in size.</p>
<p>You can use the Windows Nano Server images, which are around a hundred megabytes in size. However, they run a very stripped down version of Windows. Depending on what your application requires, it may not be enough &ldquo;Windows&rdquo; (i.e. missing dependencies or APIs) to be able to run it.</p>
<p>It is also worth noting that if your applications are written in DotNet Core, they are compatible with Linux (see <a href="https://docs.microsoft.com/en-us/dotnet/architecture/microservices/net-core-net-framework-containers/official-net-docker-images">here</a> for official DotNet Linux Docker images). If this is the case, you may not need to run Windows workers at all.</p>
<h2 id="cluster-creation">Cluster Creation</h2>
<p>The below video covers the creation of the cluster. This includes the Terraform configuration and application, monitoring the Cloud-Init logs, and then monitoring <code>kubectl</code> to see when the other nodes join. There is also a comparison of how long it takes for a Linux node to be ready to serve traffic, and the same for a Windows node (note the image size discussion in the previous section!).</p>

<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe src="https://www.youtube.com/embed/ZmjvMm_r_rc" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allowfullscreen title="YouTube Video"></iframe>
</div>

<h2 id="summary">Summary</h2>
<p>We now have a working Kubernetes cluster, and can begin to think about how to add workloads to it. As we will see in the next part of this series, there are still a few extra steps required to enable access to services outside of the cluster. Once these are complete, we can serve our applications, regardless of whether it is NGINX, IIS, Go or DotNet.</p>
<p>Being able to use the <strong>libvirt</strong> Terraform provider to achieve all this means that we have very few (if any) manual steps to bring up a multi-operating system Kubernetes cluster.</p>
]]></content>
        </item>
        
        <item>
            <title>Kubernetes - Building a Mixed Linux and Windows Cluster using Packer, Terraform, Ansible and KVM - Part 1: Image Preparation</title>
            <link>https://yetiops.net/posts/kubernetes-terraform-kvm-linux-windows-part-1/</link>
            <pubDate>Sun, 25 Oct 2020 19:25:36 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/kubernetes-terraform-kvm-linux-windows-part-1/</guid>
            <description>Kubernetes has established itself as the container orchestration platform of choice across a number of organisations, and is well supported on a number of different cloud providers. Everywhere from AWS and Google Cloud Platform to Linode and Digital Ocean has their own Kubernetes offering.
Kubernetes began as a Linux-only project. However a number of improvements within the Windows operating system (including native Container support) mean that it is now possible to run Kubernetes workloads on either Linux or Windows.</description>
            <content type="html"><![CDATA[<p>Kubernetes has established itself as the container orchestration platform of choice across a number of organisations, and is well supported on a number of different cloud providers. Everywhere from AWS and Google Cloud Platform to Linode and Digital Ocean has their own Kubernetes offering.</p>
<p>Kubernetes began as a Linux-only project. However a number of improvements within the Windows operating system (including native Container support) mean that it is now possible to run Kubernetes workloads on either Linux or Windows. Windows is currently not supported as the control plane (i.e. what controls the container orchestration), but can run as a Kubernetes worker.</p>
<p>In a short series of posts, I am going to cover how to build a Kubernetes cluster that includes both Linux and Windows workers. This series will cover: -</p>
<ul>
<li>Initial image creation (this post)</li>
<li>Deploying the images and creating a cluster with Terraform, KVM and Cloud-Init</li>
<li>Deploying applications to the cluster and making them externally available</li>
</ul>
<h2 id="environment">Environment</h2>
<p>Rather than running this on a specific cloud provider (and getting into specifics about each provider&rsquo;s managed Kubernetes implementation), this will be deployed on the KVM virtualisation environment on Linux. This means that while we may need to do a little more work up front, many of the lessons learned will be applicable to all Kubernetes implementations.</p>
<p>We will use Packer and Ansible to generate the base images for both Linux and Windows. Packer will create the images, whereas Ansible will manage the configuration and dependencies. This will  decrease the time it takes to spin up a new node into the cluster, as all the necessary features and applications are installed. They will only require the cluster-specific details to be able to join.</p>
<p>We will also make use of Terraform (with the <a href="https://github.com/dmacvicar/terraform-provider-libvirt">Terraform Libvirt provider</a> and <a href="https://cloud-init.io/">Cloud-Init</a> (native Cloud-Init for Linux, <a href="/posts/proxmox-terraform-cloudinit-windows/#cloudbase-init">Cloudbase-Init</a> for Windows) to deploy the control plane nodes and the workers.</p>
<p>To provide a highly-available control plane we will use OpenBSD&rsquo;s <a href="https://man.openbsd.org/relayd.8">relayd(8)</a> to provide basic loadbalancing for the Kubernetes API. The reason for using OpenBSD is that I always have an OpenBSD virtual machine running in my lab to provide DNS, DHCP and basic firewalling, so adding basic loadbalancing functionality here makes sense. In your own scenarios you can use any loadbalancer of your choice (NGINX, HAProxy, Cloud load balancers etc).</p>
<h3 id="why-packer">Why Packer?</h3>
<p>Packer can build images across cloud providers, virtualisation environments, Docker, Vagrant and more. This means that the Packer configuration we will use will be easily transferable to your provider of choice. If your preferred platform is VMWare, then you can adapt the configuration to see fit. If you choose AWS or Azure, then you can remove most of the configuration and still build a similar cluster.</p>
<p>You can look at the list of Packer builders <a href="https://www.packer.io/docs/builders">here</a>.</p>
<h3 id="why-ansible">Why Ansible?</h3>
<p>Ansible was built from day one as an agentless configuration management system. Given that the Packer images are going to cloned, we do not want to register the images with a configuration management system only to then require unregistering afterwards.</p>
<p>Also, Ansible has very good support for managing Windows as well as Linux, which works for our scenario.</p>
<p>Other configuration management tools (Chef, Puppet and Saltstack) do support agentless operation now, so if you are more comfortable with them then you can use them instead with Packer.</p>
<h3 id="why-cloud-init-and-cloudbase-init">Why Cloud-Init and Cloudbase-Init?</h3>
<p>Rather than creating each individual node&rsquo;s image separately in Packer, we can create two images (a Linux image and a Windows image) that can be customized at first boot. The images will be prepared with all the common dependencies (i.e. base package installation, container support, Kubernetes binaries), and then any cluster-specific details can be supplied on the next boot.</p>
<p>This means that the images are reusable across many clusters, rather than being specific to one cluster.</p>
<h2 id="packer">Packer</h2>
<p>I have covered Packer <a href="https://yetiops.net/posts/packer-ansible-windows-aws/">previously</a> as well as how to use Ansible to customize the images that Packer builds.</p>
<p>With this being the case, we will cover the specifics for how to use it with KVM, as well as some of the quirks with building Windows images for KVM using Packer.</p>
<h3 id="qemu-builder">QEMU Builder</h3>
<p>To build images for KVM, we use the <a href="https://www.packer.io/docs/builders/qemu">Packer QEMU Builder</a>. This will generate a <code>qcow2</code>-based image that KVM will use as the root disk of the Kubernetes virtual machines.</p>
<p>Unlike the builders for AWS, Google Cloud, Azure, DigitalOcean or otherwise, we do not use a pre-installed image that you can then customize. Instead we treat the installation as if it was a headless install of the machine from an ISO. This requires the use of <strong>preseed</strong> files (Debian <a href="https://wiki.debian.org/DebianInstaller/Preseed">Preseed</a>, CentOS/RHEL <a href="https://docs.centos.org/en-US/centos/install-guide/Kickstart2/">Kickstart</a> or Ubuntu <a href="https://ubuntu.com/server/docs/install/autoinstall">Autoinstall</a>) to provide answers to the questions in the ISO installers.</p>
<p>On Windows, this requires using <a href="https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/automate-windows-setup">Autounattend</a> files. Also because Windows does not have <code>virtio</code> drivers by default, we need to provide drivers at install time so that it can access the storage controller.</p>
<h3 id="linux-build">Linux Build</h3>
<p>We build the Linux images using Debian Buster. Most of the Packer configuration file was sourced from <a href="https://github.com/goffinet/packer-kvm">this</a> repository by <a href="https://twitter.com/fegoffinet">Goffinet</a>, with some changes based upon my environment.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;builders&#34;</span>: [
    {
      <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;debian10&#34;</span>,
      <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;qemu&#34;</span>,
      <span style="color:#f92672">&#34;format&#34;</span>: <span style="color:#e6db74">&#34;qcow2&#34;</span>,
      <span style="color:#f92672">&#34;accelerator&#34;</span>: <span style="color:#e6db74">&#34;kvm&#34;</span>,
      <span style="color:#f92672">&#34;qemu_binary&#34;</span>: <span style="color:#e6db74">&#34;/usr/bin/qemu-system-x86_64&#34;</span>,
      <span style="color:#f92672">&#34;net_device&#34;</span>: <span style="color:#e6db74">&#34;virtio-net&#34;</span>,
      <span style="color:#f92672">&#34;disk_interface&#34;</span>: <span style="color:#e6db74">&#34;virtio&#34;</span>,
      <span style="color:#f92672">&#34;disk_cache&#34;</span>: <span style="color:#e6db74">&#34;none&#34;</span>,
      <span style="color:#f92672">&#34;cpus:&#34;</span>: <span style="color:#e6db74">&#34;2&#34;</span>,
      <span style="color:#f92672">&#34;memory&#34;</span>: <span style="color:#e6db74">&#34;2048&#34;</span>,
      <span style="color:#f92672">&#34;ssh_wait_timeout&#34;</span>: <span style="color:#e6db74">&#34;30m&#34;</span>,
      <span style="color:#f92672">&#34;http_directory&#34;</span>: <span style="color:#e6db74">&#34;http&#34;</span>,
      <span style="color:#f92672">&#34;ssh_username&#34;</span>: <span style="color:#e6db74">&#34;root&#34;</span>,
      <span style="color:#f92672">&#34;ssh_password&#34;</span>: <span style="color:#e6db74">&#34;testtest&#34;</span>,
      <span style="color:#f92672">&#34;iso_urls&#34;</span>: <span style="color:#e6db74">&#34;https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-10.6.0-amd64-netinst.iso&#34;</span>,
      <span style="color:#f92672">&#34;iso_checksum&#34;</span>: <span style="color:#e6db74">&#34;2af8f43d4a7ab852151a7f630ba596572213e17d3579400b5648eba4cc974ed0&#34;</span>,
      <span style="color:#f92672">&#34;boot_wait&#34;</span>: <span style="color:#e6db74">&#34;15s&#34;</span>,
      <span style="color:#f92672">&#34;boot_command&#34;</span>: [
                <span style="color:#e6db74">&#34;&lt;esc&gt;&lt;wait&gt;&#34;</span>,
                <span style="color:#e6db74">&#34;auto &lt;wait&gt;&#34;</span>,
                <span style="color:#e6db74">&#34;console-keymaps-at/keymap=us &lt;wait&gt;&#34;</span>,
                <span style="color:#e6db74">&#34;console-setup/ask_detect=false &lt;wait&gt;&#34;</span>,
                <span style="color:#e6db74">&#34;debconf/frontend=noninteractive &lt;wait&gt;&#34;</span>,
                <span style="color:#e6db74">&#34;debian-installer=en_US &lt;wait&gt;&#34;</span>,
                <span style="color:#e6db74">&#34;fb=false &lt;wait&gt;&#34;</span>,
                <span style="color:#e6db74">&#34;install &lt;wait&gt;&#34;</span>,
                <span style="color:#e6db74">&#34;kbd-chooser/method=us &lt;wait&gt;&#34;</span>,
                <span style="color:#e6db74">&#34;keyboard-configuration/xkb-keymap=us &lt;wait&gt;&#34;</span>,
                <span style="color:#e6db74">&#34;locale=en_US &lt;wait&gt;&#34;</span>,
                <span style="color:#e6db74">&#34;netcfg/get_hostname=debian10 &lt;wait&gt;&#34;</span>,
                <span style="color:#e6db74">&#34;preseed/url=http://{{ .HTTPIP }}:{{ .HTTPPort }}/deb10-kube/debian-preseed.cfg &lt;wait&gt;&#34;</span>,
                <span style="color:#e6db74">&#34;&lt;enter&gt;&lt;wait&gt;&#34;</span>
      ],
      <span style="color:#f92672">&#34;disk_size&#34;</span>: <span style="color:#e6db74">&#34;40000&#34;</span>,
      <span style="color:#f92672">&#34;disk_discard&#34;</span>: <span style="color:#e6db74">&#34;unmap&#34;</span>,
      <span style="color:#f92672">&#34;disk_compression&#34;</span>: <span style="color:#66d9ef">true</span>,
      <span style="color:#f92672">&#34;headless&#34;</span>: <span style="color:#e6db74">&#34;true&#34;</span>,
      <span style="color:#f92672">&#34;vnc_bind_address&#34;</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>,
      <span style="color:#f92672">&#34;shutdown_command&#34;</span>: <span style="color:#e6db74">&#34;echo &#39;testtest&#39; | sudo -S shutdown -P now&#34;</span>,
      <span style="color:#f92672">&#34;output_directory&#34;</span>: <span style="color:#e6db74">&#34;artifacts/qemu/debian10-kube&#34;</span>
    }
  ],
  <span style="color:#f92672">&#34;provisioners&#34;</span>: [
    {
         <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;ansible&#34;</span>,
         <span style="color:#f92672">&#34;extra_arguments&#34;</span>: [
                <span style="color:#e6db74">&#34;--extra-vars&#34;</span>,
                <span style="color:#e6db74">&#34;ansible_python_interpreter=/usr/bin/python&#34;</span>
              ],
         <span style="color:#f92672">&#34;playbook_file&#34;</span>: <span style="color:#e6db74">&#34;ansible/deb10-kubernetes/main.yml&#34;</span>,
         <span style="color:#f92672">&#34;user&#34;</span>: <span style="color:#e6db74">&#34;root&#34;</span>
    }
  ]
}

</code></pre></div><p>Some points to note in this are: -</p>
<ul>
<li>We set the <strong>type</strong> to <strong>qemu</strong> (which informs Packer which builder to use)</li>
<li>We set the <strong>format</strong> to <strong>qcow2</strong> (the native disk format for KVM)</li>
<li>We set the <strong>accelerator</strong> to <strong>KVM</strong> so that the machine can make native use of the (much quicker) KVM acceleration than the native QEMU emulation for building our image</li>
<li>The <strong>qemu_binary</strong> location may differ depending upon your distribution (check the location with <code>which qemu-system-x86_64</code>)</li>
<li>We use <strong>virtio</strong> based network and disks as they are much quicker than any other emulated interface</li>
<li>We supply the ISO and checksum so that Packer can source the ISO from Debian directly, rather than relying on a local copy</li>
<li>We supply the <strong>vnc_bind_address</strong> as <code>0.0.0.0</code> so that we can monitor the build even if running on a headless server</li>
</ul>
<p>The major part that automates the installation is the <code>boot_command</code> section. The list of commands provide keyboard input to the installer (as if someone was typing these commands) so that it knows to use the preseed file, as well as setting keymaps, the hostname and more.</p>
<p>When supplying the <code>http_directory</code> field, Packer will run a HTTP server that exposes the directories (and contents) provided. This means that you can serve a <code>preseed.cfg</code> file over HTTP (in our case, this is called <code>debian-preseed.cfg</code>, n the <code>http</code> directory) rather than having to supply it on a disk image.</p>
<p>The contents of the preseed file are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">d-i pkgsel/install-language-support boolean false

# locale
d-i debian-installer/locale string en_GB.UTF-8

# keyboard
d-i keyboard-configuration/xkb-keymap   select  us

# timezone
d-i time/zone string Europe/London

# Controls whether to use NTP to set the clock during the install
d-i clock-setup/ntp boolean true

# apt
d-i mirror/country string manual
d-i mirror/http/hostname string deb.debian.org
d-i mirror/http/directory string /debian
d-i mirror/http/proxy string
d-i apt-setup/use_mirror boolean true

# users
d-i passwd/root-password password testtest
d-i passwd/root-password-again password testtest
d-i passwd/make-user boolean false

# partitioning
d-i partman-auto/method string regular
d-i partman-auto/choose_recipe select atomic
d-i partman-partitioning/confirm_write_new_label boolean true
d-i partman/choose_partition select finish
d-i partman/confirm boolean true
d-i partman/confirm_nooverwrite boolean true

apt-cdrom-setup apt-setup/cdrom/set-first boolean false

# software
tasksel tasksel/first multiselect standard
d-i pkgsel/include string qemu-guest-agent wget openssh-server vim sudo cloud-init
popularity-contest popularity-contest/participate boolean false

# grub
d-i grub-installer/only_debian boolean true
d-i grub-installer/bootdev  string /dev/vda

# script
d-i preseed/late_command in-target sed -i &#39;s/#PermitRootLogin prohibit-password/PermitRootLogin yes/&#39; /target/etc/ssh/sshd_config

# finish
d-i finish-install/reboot_in_progress note
</code></pre></div><p>To build this with Packer, you use <code>packer build $NAME_OF_FILE.json</code>. This will start the build process, which you can then view over VNC: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ packer build debian-10-kube.json
debian10: output will be in this color.

<span style="color:#f92672">==</span>&gt; debian10: Retrieving ISO
<span style="color:#f92672">==</span>&gt; debian10: Trying https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-10.6.0-amd64-netinst.iso
<span style="color:#f92672">==</span>&gt; debian10: Trying https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-10.6.0-amd64-netinst.iso?checksum<span style="color:#f92672">=</span>sha256%3A2af8f43d4a7ab852151a7f630ba596572213e17d3579400b5648eba4cc974ed0
<span style="color:#f92672">==</span>&gt; debian10: https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-10.6.0-amd64-netinst.iso?checksum<span style="color:#f92672">=</span>sha256%3A2af8f43d4a7ab852151a7f630ba596572213e17d3579400b5648eba4cc974ed0 <span style="color:#f92672">=</span>&gt; /home/stuh84/git/packer/packer_cache/c9149179a5e3b2d2360476806e9a5d30854c84ea.iso
<span style="color:#f92672">==</span>&gt; debian10: Creating required virtual machine disks
<span style="color:#f92672">==</span>&gt; debian10: Starting HTTP server on port 8435
<span style="color:#f92672">==</span>&gt; debian10: Found port <span style="color:#66d9ef">for</span> communicator <span style="color:#f92672">(</span>SSH, WinRM, etc<span style="color:#f92672">)</span>: 3270.
<span style="color:#f92672">==</span>&gt; debian10: Looking <span style="color:#66d9ef">for</span> available port between <span style="color:#ae81ff">5900</span> and <span style="color:#ae81ff">6000</span> on 0.0.0.0
<span style="color:#f92672">==</span>&gt; debian10: Starting VM, booting from CD-ROM
    debian10: The VM will be run headless, without a GUI. If you want to
    debian10: view the screen of the VM, connect via VNC without a password to
    debian10: vnc://0.0.0.0:5924
</code></pre></div><p>Connect over VNC to this port, and we can see the image being built: -</p>
<p><img src="/img/kubernetes-win-linux/vnc-packer-debian.png" alt="VNC to Packer image - Debian"></p>
<p>Once the install is finished, Ansible will apply the steps to customize the image and install the relevant dependencies.</p>
<h3 id="windows-build">Windows Build</h3>
<p>Windows Container support for Kubernetes was first made available in Windows Server 2019, so we target that as our base operating system.</p>
<p>Most of the configuration was sourced from Stefan Scherer&rsquo;s <a href="https://github.com/StefanScherer/packer-windows">packer-windows</a> repository, with a few changes made to suit my environment.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;builders&#34;</span>: [
    {
      <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;qemu&#34;</span>,
      <span style="color:#f92672">&#34;accelerator&#34;</span>: <span style="color:#e6db74">&#34;kvm&#34;</span>,
      <span style="color:#f92672">&#34;communicator&#34;</span>: <span style="color:#e6db74">&#34;winrm&#34;</span>,
      <span style="color:#f92672">&#34;format&#34;</span>:<span style="color:#e6db74">&#34;qcow2&#34;</span>,
      <span style="color:#f92672">&#34;cpus&#34;</span>: <span style="color:#e6db74">&#34;2&#34;</span>,
      <span style="color:#f92672">&#34;disk_size&#34;</span>: <span style="color:#e6db74">&#34;51200&#34;</span>,
      <span style="color:#f92672">&#34;disk_interface&#34;</span>:<span style="color:#e6db74">&#34;virtio&#34;</span>,
      <span style="color:#f92672">&#34;floppy_files&#34;</span>: [
        <span style="color:#e6db74">&#34;http/windows-2019/Autounattend.xml&#34;</span>,
        <span style="color:#e6db74">&#34;scripts/win-common/SetUpWinRM.ps1&#34;</span>
      ],
      <span style="color:#f92672">&#34;qemuargs&#34;</span>: [
        [
          <span style="color:#e6db74">&#34;-drive&#34;</span>,
          <span style="color:#e6db74">&#34;file=artifacts/qemu/win2019-kube/{{ .Name }},if=virtio,cache=writeback,discard=ignore,format=qcow2,index=1&#34;</span>
        ],
        [
          <span style="color:#e6db74">&#34;-drive&#34;</span>,
          <span style="color:#e6db74">&#34;file=isos/17763.737.190906-2324.rs5_release_svc_refresh_SERVER_EVAL_x64FRE_en-us_1.iso,media=cdrom,index=2&#34;</span>
        ],
        [
          <span style="color:#e6db74">&#34;-drive&#34;</span>,
          <span style="color:#e6db74">&#34;file=isos/virtio-win.iso,media=cdrom,index=3&#34;</span>
        ]
      ],
      <span style="color:#f92672">&#34;headless&#34;</span>: <span style="color:#66d9ef">true</span>,
      <span style="color:#f92672">&#34;vnc_bind_address&#34;</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>,
      <span style="color:#f92672">&#34;iso_checksum&#34;</span>: <span style="color:#e6db74">&#34;549bca46c055157291be6c22a3aaaed8330e78ef4382c99ee82c896426a1cee1&#34;</span>,
      <span style="color:#f92672">&#34;iso_url&#34;</span>: <span style="color:#e6db74">&#34;https://software-download.microsoft.com/download/pr/17763.737.190906-2324.rs5_release_svc_refresh_SERVER_EVAL_x64FRE_en-us_1.iso&#34;</span>,
      <span style="color:#f92672">&#34;iso_target_path&#34;</span>: <span style="color:#e6db74">&#34;isos/17763.737.190906-2324.rs5_release_svc_refresh_SERVER_EVAL_x64FRE_en-us_1.iso&#34;</span>,
      <span style="color:#f92672">&#34;memory&#34;</span>: <span style="color:#e6db74">&#34;4096&#34;</span>,
      <span style="color:#f92672">&#34;output_directory&#34;</span>: <span style="color:#e6db74">&#34;artifacts/qemu/win2019-kube&#34;</span>,
      <span style="color:#f92672">&#34;shutdown_command&#34;</span>: <span style="color:#e6db74">&#34;shutdown /s /t 30 /f&#34;</span>,
      <span style="color:#f92672">&#34;shutdown_timeout&#34;</span>: <span style="color:#e6db74">&#34;15m&#34;</span>,
      <span style="color:#f92672">&#34;vm_name&#34;</span>: <span style="color:#e6db74">&#34;windows-server-2019-standard-x64-eval&#34;</span>,
      <span style="color:#f92672">&#34;winrm_password&#34;</span>: <span style="color:#e6db74">&#34;adm!nadm!n&#34;</span>,
      <span style="color:#f92672">&#34;winrm_username&#34;</span>: <span style="color:#e6db74">&#34;Administrator&#34;</span>,
      <span style="color:#f92672">&#34;winrm_timeout&#34;</span>: <span style="color:#e6db74">&#34;2h&#34;</span>,
      <span style="color:#f92672">&#34;winrm_insecure&#34;</span>: <span style="color:#66d9ef">true</span>,
      <span style="color:#f92672">&#34;winrm_use_ssl&#34;</span>: <span style="color:#66d9ef">true</span>
    }
  ],
  <span style="color:#f92672">&#34;provisioners&#34;</span>: [
    {
     <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;ansible&#34;</span>,
     <span style="color:#f92672">&#34;extra_arguments&#34;</span>: [
       <span style="color:#e6db74">&#34;-e&#34;</span>,
       <span style="color:#e6db74">&#34;kubernetes_version=v1.19.1&#34;</span>,
       <span style="color:#e6db74">&#34;-e&#34;</span>,
       <span style="color:#e6db74">&#34;ansible_shell_type=powershell&#34;</span>,
       <span style="color:#e6db74">&#34;-e&#34;</span>,
       <span style="color:#e6db74">&#34;ansible_shell_executable=None&#34;</span>,
       <span style="color:#e6db74">&#34;-e&#34;</span>,
       <span style="color:#e6db74">&#34;ansible_winrm_server_cert_validation=ignore&#34;</span>
     ],
     <span style="color:#f92672">&#34;use_proxy&#34;</span>: <span style="color:#66d9ef">false</span>,
     <span style="color:#f92672">&#34;playbook_file&#34;</span>: <span style="color:#e6db74">&#34;ansible/windows-2019-kubernetes/main.yml&#34;</span>,
     <span style="color:#f92672">&#34;user&#34;</span>: <span style="color:#e6db74">&#34;Administrator&#34;</span>
    },
    {
      <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;powershell&#34;</span>,
      <span style="color:#f92672">&#34;inline&#34;</span>: [
        <span style="color:#e6db74">&#34;C:\\Windows\\System32\\Sysprep\\sysprep.exe /quiet /generalize /oobe  /unattend:C:\\Program Files\\Cloudbase Solutions\\Cloudbase-Init\\conf\\Unattend.xml&#34;</span>
      ]
    }
  ]
}
</code></pre></div><p>A lot of this is very similar to the Debian image, with some exceptions. Some important points to note in this are: -</p>
<ul>
<li>We use the <code>floppy_files</code> directive for supplying the <code>Autounattend</code> file
<ul>
<li>We cannot guarantee that the network drivers work correctly pre-install (due to using VirtIO drivers)</li>
</ul>
</li>
<li>We supply a script to setup WinRM (taken from <a href="https://github.com/SkeltonThatcher/packer-bamboo/blob/master/scripts/SetUpWinRM.ps1">here</a>)
<ul>
<li>Ansible uses WinRM for Windows, so this is required for us to make changes to the machine post-install</li>
</ul>
</li>
<li>We present the <code>virtio-win.iso</code> ISO so that the machine can load drivers at install time</li>
<li>We customize the <code>winrm</code> parameters (including timeouts and whether we use SSL, and also ignoring if a cert is unsigned)</li>
</ul>
<p>One interesting point for the Ansible stage is that we use <code>use_proxy: false</code>. This means that Ansible bypasses the Packer connection to the machine, and talks to it directly. This is based upon a comment I received on my <a href="/posts/packer-ansible-windows-aws/">Building Windows AWS AMIs using Packer and Ansible</a> from <a href="https://github.com/jsturtevant">James Sturtevant</a>. Previously I used an Ansible connection plugin to effectively piggyback onto the Packer connection to the virtual machine. Avoiding the Packer proxy means that we do not need any custom plugins to interact with the machine.</p>
<p>Finally, we add a step at the end to <strong>sysprep</strong> the machine (i.e. generalize it ready for being cloned) using the Cloudbase-Init provided <code>Unattend.xml</code>. This ensures that Cloudbase-Init will run on the next boot of the machine (i.e. when it is cloned) so that we can customize the image with cluster-specific parameters.</p>
<p>The contents of the <code>Autounattend.xml</code> file that is used to install Windows in the first place looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml"><span style="color:#75715e">&lt;?xml version=&#34;1.0&#34; encoding=&#34;utf-8&#34;?&gt;</span>
<span style="color:#f92672">&lt;unattend</span> <span style="color:#a6e22e">xmlns=</span><span style="color:#e6db74">&#34;urn:schemas-microsoft-com:unattend&#34;</span><span style="color:#f92672">&gt;</span>
    <span style="color:#f92672">&lt;settings</span> <span style="color:#a6e22e">pass=</span><span style="color:#e6db74">&#34;windowsPE&#34;</span><span style="color:#f92672">&gt;</span>
        <span style="color:#f92672">&lt;component</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;Microsoft-Windows-PnpCustomizationsWinPE&#34;</span>
            <span style="color:#a6e22e">publicKeyToken=</span><span style="color:#e6db74">&#34;31bf3856ad364e35&#34;</span> <span style="color:#a6e22e">language=</span><span style="color:#e6db74">&#34;neutral&#34;</span>
            <span style="color:#a6e22e">versionScope=</span><span style="color:#e6db74">&#34;nonSxS&#34;</span> <span style="color:#a6e22e">processorArchitecture=</span><span style="color:#e6db74">&#34;amd64&#34;</span>
            <span style="color:#a6e22e">xmlns:wcm=</span><span style="color:#e6db74">&#34;http://schemas.microsoft.com/WMIConfig/2002/State&#34;</span><span style="color:#f92672">&gt;</span>

            <span style="color:#75715e">&lt;!--
</span><span style="color:#75715e">                 This makes the VirtIO drivers available to Windows, assuming that
</span><span style="color:#75715e">                 the VirtIO driver disk at https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso
</span><span style="color:#75715e">                 (see https://docs.fedoraproject.org/en-US/quick-docs/creating-windows-virtual-machines-using-virtio-drivers/index.html#virtio-win-direct-downloads)
</span><span style="color:#75715e">                 is available as drive E:
</span><span style="color:#75715e">            --&gt;</span>
            <span style="color:#f92672">&lt;DriverPaths&gt;</span>
                <span style="color:#f92672">&lt;PathAndCredentials</span> <span style="color:#a6e22e">wcm:action=</span><span style="color:#e6db74">&#34;add&#34;</span> <span style="color:#a6e22e">wcm:keyValue=</span><span style="color:#e6db74">&#34;2&#34;</span><span style="color:#f92672">&gt;</span>
                    <span style="color:#f92672">&lt;Path&gt;</span>E:\viostor\2k19\amd64<span style="color:#f92672">&lt;/Path&gt;</span>
                <span style="color:#f92672">&lt;/PathAndCredentials&gt;</span>

                <span style="color:#f92672">&lt;PathAndCredentials</span> <span style="color:#a6e22e">wcm:action=</span><span style="color:#e6db74">&#34;add&#34;</span> <span style="color:#a6e22e">wcm:keyValue=</span><span style="color:#e6db74">&#34;3&#34;</span><span style="color:#f92672">&gt;</span>
                    <span style="color:#f92672">&lt;Path&gt;</span>E:\NetKVM\2k19\amd64<span style="color:#f92672">&lt;/Path&gt;</span>
                <span style="color:#f92672">&lt;/PathAndCredentials&gt;</span>

                <span style="color:#f92672">&lt;PathAndCredentials</span> <span style="color:#a6e22e">wcm:action=</span><span style="color:#e6db74">&#34;add&#34;</span> <span style="color:#a6e22e">wcm:keyValue=</span><span style="color:#e6db74">&#34;4&#34;</span><span style="color:#f92672">&gt;</span>
                    <span style="color:#f92672">&lt;Path&gt;</span>E:\Balloon\2k19\amd64<span style="color:#f92672">&lt;/Path&gt;</span>
                <span style="color:#f92672">&lt;/PathAndCredentials&gt;</span>

                <span style="color:#f92672">&lt;PathAndCredentials</span> <span style="color:#a6e22e">wcm:action=</span><span style="color:#e6db74">&#34;add&#34;</span> <span style="color:#a6e22e">wcm:keyValue=</span><span style="color:#e6db74">&#34;5&#34;</span><span style="color:#f92672">&gt;</span>
                    <span style="color:#f92672">&lt;Path&gt;</span>E:\pvpanic\2k19\amd64<span style="color:#f92672">&lt;/Path&gt;</span>
                <span style="color:#f92672">&lt;/PathAndCredentials&gt;</span>

                <span style="color:#f92672">&lt;PathAndCredentials</span> <span style="color:#a6e22e">wcm:action=</span><span style="color:#e6db74">&#34;add&#34;</span> <span style="color:#a6e22e">wcm:keyValue=</span><span style="color:#e6db74">&#34;6&#34;</span><span style="color:#f92672">&gt;</span>
                    <span style="color:#f92672">&lt;Path&gt;</span>E:\qemupciserial\2k19\amd64<span style="color:#f92672">&lt;/Path&gt;</span>
                <span style="color:#f92672">&lt;/PathAndCredentials&gt;</span>

                <span style="color:#f92672">&lt;PathAndCredentials</span> <span style="color:#a6e22e">wcm:action=</span><span style="color:#e6db74">&#34;add&#34;</span> <span style="color:#a6e22e">wcm:keyValue=</span><span style="color:#e6db74">&#34;7&#34;</span><span style="color:#f92672">&gt;</span>
                    <span style="color:#f92672">&lt;Path&gt;</span>E:\qxldod\2k19\amd64<span style="color:#f92672">&lt;/Path&gt;</span>
                <span style="color:#f92672">&lt;/PathAndCredentials&gt;</span>

                <span style="color:#f92672">&lt;PathAndCredentials</span> <span style="color:#a6e22e">wcm:action=</span><span style="color:#e6db74">&#34;add&#34;</span> <span style="color:#a6e22e">wcm:keyValue=</span><span style="color:#e6db74">&#34;8&#34;</span><span style="color:#f92672">&gt;</span>
                    <span style="color:#f92672">&lt;Path&gt;</span>E:\vioinput\2k19\amd64<span style="color:#f92672">&lt;/Path&gt;</span>
                <span style="color:#f92672">&lt;/PathAndCredentials&gt;</span>

                <span style="color:#f92672">&lt;PathAndCredentials</span> <span style="color:#a6e22e">wcm:action=</span><span style="color:#e6db74">&#34;add&#34;</span> <span style="color:#a6e22e">wcm:keyValue=</span><span style="color:#e6db74">&#34;9&#34;</span><span style="color:#f92672">&gt;</span>
                    <span style="color:#f92672">&lt;Path&gt;</span>E:\viorng\2k19\amd64<span style="color:#f92672">&lt;/Path&gt;</span>
                <span style="color:#f92672">&lt;/PathAndCredentials&gt;</span>

                <span style="color:#f92672">&lt;PathAndCredentials</span> <span style="color:#a6e22e">wcm:action=</span><span style="color:#e6db74">&#34;add&#34;</span> <span style="color:#a6e22e">wcm:keyValue=</span><span style="color:#e6db74">&#34;10&#34;</span><span style="color:#f92672">&gt;</span>
                    <span style="color:#f92672">&lt;Path&gt;</span>E:\vioscsi\2k19\amd64<span style="color:#f92672">&lt;/Path&gt;</span>
                <span style="color:#f92672">&lt;/PathAndCredentials&gt;</span>

                <span style="color:#f92672">&lt;PathAndCredentials</span> <span style="color:#a6e22e">wcm:action=</span><span style="color:#e6db74">&#34;add&#34;</span> <span style="color:#a6e22e">wcm:keyValue=</span><span style="color:#e6db74">&#34;11&#34;</span><span style="color:#f92672">&gt;</span>
                    <span style="color:#f92672">&lt;Path&gt;</span>E:\vioserial\2k19\amd64<span style="color:#f92672">&lt;/Path&gt;</span>
                <span style="color:#f92672">&lt;/PathAndCredentials&gt;</span>
            <span style="color:#f92672">&lt;/DriverPaths&gt;</span>
        <span style="color:#f92672">&lt;/component&gt;</span>

        <span style="color:#f92672">&lt;component</span> <span style="color:#a6e22e">xmlns:wcm=</span><span style="color:#e6db74">&#34;http://schemas.microsoft.com/WMIConfig/2002/State&#34;</span> <span style="color:#a6e22e">xmlns:xsi=</span><span style="color:#e6db74">&#34;http://www.w3.org/2001/XMLSchema-instance&#34;</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;Microsoft-Windows-International-Core-WinPE&#34;</span> <span style="color:#a6e22e">processorArchitecture=</span><span style="color:#e6db74">&#34;amd64&#34;</span> <span style="color:#a6e22e">publicKeyToken=</span><span style="color:#e6db74">&#34;31bf3856ad364e35&#34;</span> <span style="color:#a6e22e">language=</span><span style="color:#e6db74">&#34;neutral&#34;</span> <span style="color:#a6e22e">versionScope=</span><span style="color:#e6db74">&#34;nonSxS&#34;</span><span style="color:#f92672">&gt;</span>
            <span style="color:#f92672">&lt;SetupUILanguage&gt;</span>
                <span style="color:#f92672">&lt;UILanguage&gt;</span>en-US<span style="color:#f92672">&lt;/UILanguage&gt;</span>
            <span style="color:#f92672">&lt;/SetupUILanguage&gt;</span>
            <span style="color:#f92672">&lt;InputLocale&gt;</span>en-US<span style="color:#f92672">&lt;/InputLocale&gt;</span>
            <span style="color:#f92672">&lt;SystemLocale&gt;</span>en-US<span style="color:#f92672">&lt;/SystemLocale&gt;</span>
            <span style="color:#f92672">&lt;UILanguage&gt;</span>en-US<span style="color:#f92672">&lt;/UILanguage&gt;</span>
            <span style="color:#f92672">&lt;UILanguageFallback&gt;</span>en-US<span style="color:#f92672">&lt;/UILanguageFallback&gt;</span>
            <span style="color:#f92672">&lt;UserLocale&gt;</span>en-US<span style="color:#f92672">&lt;/UserLocale&gt;</span>
        <span style="color:#f92672">&lt;/component&gt;</span>
        <span style="color:#f92672">&lt;component</span> <span style="color:#a6e22e">xmlns:wcm=</span><span style="color:#e6db74">&#34;http://schemas.microsoft.com/WMIConfig/2002/State&#34;</span> <span style="color:#a6e22e">xmlns:xsi=</span><span style="color:#e6db74">&#34;http://www.w3.org/2001/XMLSchema-instance&#34;</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;Microsoft-Windows-Setup&#34;</span> <span style="color:#a6e22e">processorArchitecture=</span><span style="color:#e6db74">&#34;amd64&#34;</span> <span style="color:#a6e22e">publicKeyToken=</span><span style="color:#e6db74">&#34;31bf3856ad364e35&#34;</span> <span style="color:#a6e22e">language=</span><span style="color:#e6db74">&#34;neutral&#34;</span> <span style="color:#a6e22e">versionScope=</span><span style="color:#e6db74">&#34;nonSxS&#34;</span><span style="color:#f92672">&gt;</span>
            <span style="color:#f92672">&lt;DiskConfiguration&gt;</span>
                <span style="color:#f92672">&lt;Disk</span> <span style="color:#a6e22e">wcm:action=</span><span style="color:#e6db74">&#34;add&#34;</span><span style="color:#f92672">&gt;</span>
                    <span style="color:#f92672">&lt;CreatePartitions&gt;</span>
                        <span style="color:#f92672">&lt;CreatePartition</span> <span style="color:#a6e22e">wcm:action=</span><span style="color:#e6db74">&#34;add&#34;</span><span style="color:#f92672">&gt;</span>
                            <span style="color:#f92672">&lt;Type&gt;</span>Primary<span style="color:#f92672">&lt;/Type&gt;</span>
                            <span style="color:#f92672">&lt;Order&gt;</span>1<span style="color:#f92672">&lt;/Order&gt;</span>
                            <span style="color:#f92672">&lt;Size&gt;</span>350<span style="color:#f92672">&lt;/Size&gt;</span>
                        <span style="color:#f92672">&lt;/CreatePartition&gt;</span>
                        <span style="color:#f92672">&lt;CreatePartition</span> <span style="color:#a6e22e">wcm:action=</span><span style="color:#e6db74">&#34;add&#34;</span><span style="color:#f92672">&gt;</span>
                            <span style="color:#f92672">&lt;Order&gt;</span>2<span style="color:#f92672">&lt;/Order&gt;</span>
                            <span style="color:#f92672">&lt;Type&gt;</span>Primary<span style="color:#f92672">&lt;/Type&gt;</span>
                            <span style="color:#f92672">&lt;Extend&gt;</span>true<span style="color:#f92672">&lt;/Extend&gt;</span>
                        <span style="color:#f92672">&lt;/CreatePartition&gt;</span>
                    <span style="color:#f92672">&lt;/CreatePartitions&gt;</span>
                    <span style="color:#f92672">&lt;ModifyPartitions&gt;</span>
                        <span style="color:#f92672">&lt;ModifyPartition</span> <span style="color:#a6e22e">wcm:action=</span><span style="color:#e6db74">&#34;add&#34;</span><span style="color:#f92672">&gt;</span>
                            <span style="color:#f92672">&lt;Active&gt;</span>true<span style="color:#f92672">&lt;/Active&gt;</span>
                            <span style="color:#f92672">&lt;Format&gt;</span>NTFS<span style="color:#f92672">&lt;/Format&gt;</span>
                            <span style="color:#f92672">&lt;Label&gt;</span>boot<span style="color:#f92672">&lt;/Label&gt;</span>
                            <span style="color:#f92672">&lt;Order&gt;</span>1<span style="color:#f92672">&lt;/Order&gt;</span>
                            <span style="color:#f92672">&lt;PartitionID&gt;</span>1<span style="color:#f92672">&lt;/PartitionID&gt;</span>
                        <span style="color:#f92672">&lt;/ModifyPartition&gt;</span>
                        <span style="color:#f92672">&lt;ModifyPartition</span> <span style="color:#a6e22e">wcm:action=</span><span style="color:#e6db74">&#34;add&#34;</span><span style="color:#f92672">&gt;</span>
                            <span style="color:#f92672">&lt;Format&gt;</span>NTFS<span style="color:#f92672">&lt;/Format&gt;</span>
                            <span style="color:#f92672">&lt;Label&gt;</span>Windows 2019<span style="color:#f92672">&lt;/Label&gt;</span>
                            <span style="color:#f92672">&lt;Letter&gt;</span>C<span style="color:#f92672">&lt;/Letter&gt;</span>
                            <span style="color:#f92672">&lt;Order&gt;</span>2<span style="color:#f92672">&lt;/Order&gt;</span>
                            <span style="color:#f92672">&lt;PartitionID&gt;</span>2<span style="color:#f92672">&lt;/PartitionID&gt;</span>
                        <span style="color:#f92672">&lt;/ModifyPartition&gt;</span>
                    <span style="color:#f92672">&lt;/ModifyPartitions&gt;</span>
                    <span style="color:#f92672">&lt;DiskID&gt;</span>0<span style="color:#f92672">&lt;/DiskID&gt;</span>
                    <span style="color:#f92672">&lt;WillWipeDisk&gt;</span>true<span style="color:#f92672">&lt;/WillWipeDisk&gt;</span>
                <span style="color:#f92672">&lt;/Disk&gt;</span>
            <span style="color:#f92672">&lt;/DiskConfiguration&gt;</span>
            <span style="color:#f92672">&lt;ImageInstall&gt;</span>
                <span style="color:#f92672">&lt;OSImage&gt;</span>
                    <span style="color:#f92672">&lt;InstallFrom&gt;</span>
                        <span style="color:#f92672">&lt;MetaData</span> <span style="color:#a6e22e">wcm:action=</span><span style="color:#e6db74">&#34;add&#34;</span><span style="color:#f92672">&gt;</span>
                            <span style="color:#f92672">&lt;Key&gt;</span>/IMAGE/NAME<span style="color:#f92672">&lt;/Key&gt;</span>
                            <span style="color:#f92672">&lt;Value&gt;</span>Windows Server 2019 SERVERDATACENTERCORE<span style="color:#f92672">&lt;/Value&gt;</span>
                        <span style="color:#f92672">&lt;/MetaData&gt;</span>
                    <span style="color:#f92672">&lt;/InstallFrom&gt;</span>
                    <span style="color:#f92672">&lt;InstallTo&gt;</span>
                        <span style="color:#f92672">&lt;DiskID&gt;</span>0<span style="color:#f92672">&lt;/DiskID&gt;</span>
                        <span style="color:#f92672">&lt;PartitionID&gt;</span>2<span style="color:#f92672">&lt;/PartitionID&gt;</span>
                    <span style="color:#f92672">&lt;/InstallTo&gt;</span>
                <span style="color:#f92672">&lt;/OSImage&gt;</span>
            <span style="color:#f92672">&lt;/ImageInstall&gt;</span>
            <span style="color:#f92672">&lt;UserData&gt;</span>
                <span style="color:#f92672">&lt;ProductKey&gt;</span>
                    <span style="color:#75715e">&lt;!--
</span><span style="color:#75715e">                        Windows Server Insider product key
</span><span style="color:#75715e">                        See https://blogs.windows.com/windowsexperience/2017/07/13/announcing-windows-server-insider-preview-build-16237/
</span><span style="color:#75715e">                    --&gt;</span>
                    <span style="color:#75715e">&lt;!--&lt;Key&gt;&lt;/Key&gt;--&gt;</span>
                    <span style="color:#f92672">&lt;WillShowUI&gt;</span>OnError<span style="color:#f92672">&lt;/WillShowUI&gt;</span>
                <span style="color:#f92672">&lt;/ProductKey&gt;</span>
                <span style="color:#f92672">&lt;AcceptEula&gt;</span>true<span style="color:#f92672">&lt;/AcceptEula&gt;</span>
                <span style="color:#f92672">&lt;FullName&gt;</span>Vagrant<span style="color:#f92672">&lt;/FullName&gt;</span>
                <span style="color:#f92672">&lt;Organization&gt;</span>Vagrant<span style="color:#f92672">&lt;/Organization&gt;</span>
            <span style="color:#f92672">&lt;/UserData&gt;</span>
        <span style="color:#f92672">&lt;/component&gt;</span>
    <span style="color:#f92672">&lt;/settings&gt;</span>
    <span style="color:#f92672">&lt;settings</span> <span style="color:#a6e22e">pass=</span><span style="color:#e6db74">&#34;specialize&#34;</span><span style="color:#f92672">&gt;</span>
        <span style="color:#f92672">&lt;component</span> <span style="color:#a6e22e">xmlns:wcm=</span><span style="color:#e6db74">&#34;http://schemas.microsoft.com/WMIConfig/2002/State&#34;</span> <span style="color:#a6e22e">xmlns:xsi=</span><span style="color:#e6db74">&#34;http://www.w3.org/2001/XMLSchema-instance&#34;</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;Microsoft-Windows-Shell-Setup&#34;</span> <span style="color:#a6e22e">processorArchitecture=</span><span style="color:#e6db74">&#34;amd64&#34;</span> <span style="color:#a6e22e">publicKeyToken=</span><span style="color:#e6db74">&#34;31bf3856ad364e35&#34;</span> <span style="color:#a6e22e">language=</span><span style="color:#e6db74">&#34;neutral&#34;</span> <span style="color:#a6e22e">versionScope=</span><span style="color:#e6db74">&#34;nonSxS&#34;</span><span style="color:#f92672">&gt;</span>
            <span style="color:#f92672">&lt;OEMInformation&gt;</span>
                <span style="color:#f92672">&lt;HelpCustomized&gt;</span>false<span style="color:#f92672">&lt;/HelpCustomized&gt;</span>
            <span style="color:#f92672">&lt;/OEMInformation&gt;</span>
            <span style="color:#f92672">&lt;ComputerName&gt;</span>vagrant-2019<span style="color:#f92672">&lt;/ComputerName&gt;</span>
            <span style="color:#f92672">&lt;TimeZone&gt;</span>Pacific Standard Time<span style="color:#f92672">&lt;/TimeZone&gt;</span>
            <span style="color:#f92672">&lt;RegisteredOwner/&gt;</span>
        <span style="color:#f92672">&lt;/component&gt;</span>
        <span style="color:#f92672">&lt;component</span> <span style="color:#a6e22e">xmlns:wcm=</span><span style="color:#e6db74">&#34;http://schemas.microsoft.com/WMIConfig/2002/State&#34;</span> <span style="color:#a6e22e">xmlns:xsi=</span><span style="color:#e6db74">&#34;http://www.w3.org/2001/XMLSchema-instance&#34;</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;Microsoft-Windows-ServerManager-SvrMgrNc&#34;</span> <span style="color:#a6e22e">processorArchitecture=</span><span style="color:#e6db74">&#34;amd64&#34;</span> <span style="color:#a6e22e">publicKeyToken=</span><span style="color:#e6db74">&#34;31bf3856ad364e35&#34;</span> <span style="color:#a6e22e">language=</span><span style="color:#e6db74">&#34;neutral&#34;</span> <span style="color:#a6e22e">versionScope=</span><span style="color:#e6db74">&#34;nonSxS&#34;</span><span style="color:#f92672">&gt;</span>
            <span style="color:#f92672">&lt;DoNotOpenServerManagerAtLogon&gt;</span>true<span style="color:#f92672">&lt;/DoNotOpenServerManagerAtLogon&gt;</span>
        <span style="color:#f92672">&lt;/component&gt;</span>
        <span style="color:#f92672">&lt;component</span> <span style="color:#a6e22e">xmlns:wcm=</span><span style="color:#e6db74">&#34;http://schemas.microsoft.com/WMIConfig/2002/State&#34;</span> <span style="color:#a6e22e">xmlns:xsi=</span><span style="color:#e6db74">&#34;http://www.w3.org/2001/XMLSchema-instance&#34;</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;Microsoft-Windows-IE-ESC&#34;</span> <span style="color:#a6e22e">processorArchitecture=</span><span style="color:#e6db74">&#34;amd64&#34;</span> <span style="color:#a6e22e">publicKeyToken=</span><span style="color:#e6db74">&#34;31bf3856ad364e35&#34;</span> <span style="color:#a6e22e">language=</span><span style="color:#e6db74">&#34;neutral&#34;</span> <span style="color:#a6e22e">versionScope=</span><span style="color:#e6db74">&#34;nonSxS&#34;</span><span style="color:#f92672">&gt;</span>
            <span style="color:#f92672">&lt;IEHardenAdmin&gt;</span>false<span style="color:#f92672">&lt;/IEHardenAdmin&gt;</span>
            <span style="color:#f92672">&lt;IEHardenUser&gt;</span>false<span style="color:#f92672">&lt;/IEHardenUser&gt;</span>
        <span style="color:#f92672">&lt;/component&gt;</span>
        <span style="color:#f92672">&lt;component</span> <span style="color:#a6e22e">xmlns:wcm=</span><span style="color:#e6db74">&#34;http://schemas.microsoft.com/WMIConfig/2002/State&#34;</span> <span style="color:#a6e22e">xmlns:xsi=</span><span style="color:#e6db74">&#34;http://www.w3.org/2001/XMLSchema-instance&#34;</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;Microsoft-Windows-OutOfBoxExperience&#34;</span> <span style="color:#a6e22e">processorArchitecture=</span><span style="color:#e6db74">&#34;amd64&#34;</span> <span style="color:#a6e22e">publicKeyToken=</span><span style="color:#e6db74">&#34;31bf3856ad364e35&#34;</span> <span style="color:#a6e22e">language=</span><span style="color:#e6db74">&#34;neutral&#34;</span> <span style="color:#a6e22e">versionScope=</span><span style="color:#e6db74">&#34;nonSxS&#34;</span><span style="color:#f92672">&gt;</span>
            <span style="color:#f92672">&lt;DoNotOpenInitialConfigurationTasksAtLogon&gt;</span>true<span style="color:#f92672">&lt;/DoNotOpenInitialConfigurationTasksAtLogon&gt;</span>
        <span style="color:#f92672">&lt;/component&gt;</span>
        <span style="color:#f92672">&lt;component</span> <span style="color:#a6e22e">xmlns:wcm=</span><span style="color:#e6db74">&#34;http://schemas.microsoft.com/WMIConfig/2002/State&#34;</span> <span style="color:#a6e22e">xmlns:xsi=</span><span style="color:#e6db74">&#34;http://www.w3.org/2001/XMLSchema-instance&#34;</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;Microsoft-Windows-Security-SPP-UX&#34;</span> <span style="color:#a6e22e">processorArchitecture=</span><span style="color:#e6db74">&#34;amd64&#34;</span> <span style="color:#a6e22e">publicKeyToken=</span><span style="color:#e6db74">&#34;31bf3856ad364e35&#34;</span> <span style="color:#a6e22e">language=</span><span style="color:#e6db74">&#34;neutral&#34;</span> <span style="color:#a6e22e">versionScope=</span><span style="color:#e6db74">&#34;nonSxS&#34;</span><span style="color:#f92672">&gt;</span>
            <span style="color:#f92672">&lt;SkipAutoActivation&gt;</span>true<span style="color:#f92672">&lt;/SkipAutoActivation&gt;</span>
        <span style="color:#f92672">&lt;/component&gt;</span>
    <span style="color:#f92672">&lt;/settings&gt;</span>
    <span style="color:#f92672">&lt;settings</span> <span style="color:#a6e22e">pass=</span><span style="color:#e6db74">&#34;oobeSystem&#34;</span><span style="color:#f92672">&gt;</span>
        <span style="color:#f92672">&lt;component</span> <span style="color:#a6e22e">xmlns:wcm=</span><span style="color:#e6db74">&#34;http://schemas.microsoft.com/WMIConfig/2002/State&#34;</span> <span style="color:#a6e22e">xmlns:xsi=</span><span style="color:#e6db74">&#34;http://www.w3.org/2001/XMLSchema-instance&#34;</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;Microsoft-Windows-Shell-Setup&#34;</span> <span style="color:#a6e22e">processorArchitecture=</span><span style="color:#e6db74">&#34;amd64&#34;</span> <span style="color:#a6e22e">publicKeyToken=</span><span style="color:#e6db74">&#34;31bf3856ad364e35&#34;</span> <span style="color:#a6e22e">language=</span><span style="color:#e6db74">&#34;neutral&#34;</span> <span style="color:#a6e22e">versionScope=</span><span style="color:#e6db74">&#34;nonSxS&#34;</span><span style="color:#f92672">&gt;</span>
            <span style="color:#f92672">&lt;AutoLogon&gt;</span>
                <span style="color:#f92672">&lt;Password&gt;</span>
                    <span style="color:#f92672">&lt;Value&gt;</span>adm!nadm!n<span style="color:#f92672">&lt;/Value&gt;</span>
                    <span style="color:#f92672">&lt;PlainText&gt;</span>true<span style="color:#f92672">&lt;/PlainText&gt;</span>
                <span style="color:#f92672">&lt;/Password&gt;</span>
                <span style="color:#f92672">&lt;Enabled&gt;</span>true<span style="color:#f92672">&lt;/Enabled&gt;</span>
                <span style="color:#f92672">&lt;Username&gt;</span>Administrator<span style="color:#f92672">&lt;/Username&gt;</span>
            <span style="color:#f92672">&lt;/AutoLogon&gt;</span>
            <span style="color:#f92672">&lt;FirstLogonCommands&gt;</span>
                <span style="color:#f92672">&lt;SynchronousCommand</span> <span style="color:#a6e22e">wcm:action=</span><span style="color:#e6db74">&#34;add&#34;</span><span style="color:#f92672">&gt;</span>
                    <span style="color:#f92672">&lt;CommandLine&gt;</span>cmd.exe /c powershell -Command &#34;Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Force&#34;<span style="color:#f92672">&lt;/CommandLine&gt;</span>
                    <span style="color:#f92672">&lt;Description&gt;</span>Set Execution Policy 64 Bit<span style="color:#f92672">&lt;/Description&gt;</span>
                    <span style="color:#f92672">&lt;Order&gt;</span>1<span style="color:#f92672">&lt;/Order&gt;</span>
                    <span style="color:#f92672">&lt;RequiresUserInput&gt;</span>true<span style="color:#f92672">&lt;/RequiresUserInput&gt;</span>
                <span style="color:#f92672">&lt;/SynchronousCommand&gt;</span>
                <span style="color:#f92672">&lt;SynchronousCommand</span> <span style="color:#a6e22e">wcm:action=</span><span style="color:#e6db74">&#34;add&#34;</span><span style="color:#f92672">&gt;</span>
                    <span style="color:#f92672">&lt;CommandLine&gt;</span>C:\Windows\SysWOW64\cmd.exe /c powershell -Command &#34;Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Force&#34;<span style="color:#f92672">&lt;/CommandLine&gt;</span>
                    <span style="color:#f92672">&lt;Description&gt;</span>Set Execution Policy 32 Bit<span style="color:#f92672">&lt;/Description&gt;</span>
                    <span style="color:#f92672">&lt;Order&gt;</span>2<span style="color:#f92672">&lt;/Order&gt;</span>
                    <span style="color:#f92672">&lt;RequiresUserInput&gt;</span>true<span style="color:#f92672">&lt;/RequiresUserInput&gt;</span>
                <span style="color:#f92672">&lt;/SynchronousCommand&gt;</span>
                <span style="color:#f92672">&lt;SynchronousCommand</span> <span style="color:#a6e22e">wcm:action=</span><span style="color:#e6db74">&#34;add&#34;</span><span style="color:#f92672">&gt;</span>
                    <span style="color:#f92672">&lt;CommandLine&gt;</span>C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -File a:\SetUpWinRM.ps1<span style="color:#f92672">&lt;/CommandLine&gt;</span>
                    <span style="color:#f92672">&lt;Description&gt;</span>Setup WinRM<span style="color:#f92672">&lt;/Description&gt;</span>
                    <span style="color:#f92672">&lt;Order&gt;</span>3<span style="color:#f92672">&lt;/Order&gt;</span>
                    <span style="color:#f92672">&lt;RequiresUserInput&gt;</span>true<span style="color:#f92672">&lt;/RequiresUserInput&gt;</span>
                <span style="color:#f92672">&lt;/SynchronousCommand&gt;</span>
                <span style="color:#f92672">&lt;SynchronousCommand</span> <span style="color:#a6e22e">wcm:action=</span><span style="color:#e6db74">&#34;add&#34;</span><span style="color:#f92672">&gt;</span>
                    <span style="color:#f92672">&lt;CommandLine&gt;</span>%SystemRoot%\System32\reg.exe ADD HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\ /v HideFileExt /t REG_DWORD /d 0 /f<span style="color:#f92672">&lt;/CommandLine&gt;</span>
                    <span style="color:#f92672">&lt;Order&gt;</span>4<span style="color:#f92672">&lt;/Order&gt;</span>
                    <span style="color:#f92672">&lt;Description&gt;</span>Show file extensions in Explorer<span style="color:#f92672">&lt;/Description&gt;</span>
                <span style="color:#f92672">&lt;/SynchronousCommand&gt;</span>
                <span style="color:#f92672">&lt;SynchronousCommand</span> <span style="color:#a6e22e">wcm:action=</span><span style="color:#e6db74">&#34;add&#34;</span><span style="color:#f92672">&gt;</span>
                    <span style="color:#f92672">&lt;CommandLine&gt;</span>%SystemRoot%\System32\reg.exe ADD HKCU\Console /v QuickEdit /t REG_DWORD /d 1 /f<span style="color:#f92672">&lt;/CommandLine&gt;</span>
                    <span style="color:#f92672">&lt;Order&gt;</span>5<span style="color:#f92672">&lt;/Order&gt;</span>
                    <span style="color:#f92672">&lt;Description&gt;</span>Enable QuickEdit mode<span style="color:#f92672">&lt;/Description&gt;</span>
                <span style="color:#f92672">&lt;/SynchronousCommand&gt;</span>
                <span style="color:#f92672">&lt;SynchronousCommand</span> <span style="color:#a6e22e">wcm:action=</span><span style="color:#e6db74">&#34;add&#34;</span><span style="color:#f92672">&gt;</span>
                    <span style="color:#f92672">&lt;CommandLine&gt;</span>%SystemRoot%\System32\reg.exe ADD HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\ /v Start_ShowRun /t REG_DWORD /d 1 /f<span style="color:#f92672">&lt;/CommandLine&gt;</span>
                    <span style="color:#f92672">&lt;Order&gt;</span>6<span style="color:#f92672">&lt;/Order&gt;</span>
                    <span style="color:#f92672">&lt;Description&gt;</span>Show Run command in Start Menu<span style="color:#f92672">&lt;/Description&gt;</span>
                <span style="color:#f92672">&lt;/SynchronousCommand&gt;</span>
                <span style="color:#f92672">&lt;SynchronousCommand</span> <span style="color:#a6e22e">wcm:action=</span><span style="color:#e6db74">&#34;add&#34;</span><span style="color:#f92672">&gt;</span>
                    <span style="color:#f92672">&lt;CommandLine&gt;</span>%SystemRoot%\System32\reg.exe ADD HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\ /v StartMenuAdminTools /t REG_DWORD /d 1 /f<span style="color:#f92672">&lt;/CommandLine&gt;</span>
                    <span style="color:#f92672">&lt;Order&gt;</span>7<span style="color:#f92672">&lt;/Order&gt;</span>
                    <span style="color:#f92672">&lt;Description&gt;</span>Show Administrative Tools in Start Menu<span style="color:#f92672">&lt;/Description&gt;</span>
                <span style="color:#f92672">&lt;/SynchronousCommand&gt;</span>
                <span style="color:#f92672">&lt;SynchronousCommand</span> <span style="color:#a6e22e">wcm:action=</span><span style="color:#e6db74">&#34;add&#34;</span><span style="color:#f92672">&gt;</span>
                    <span style="color:#f92672">&lt;CommandLine&gt;</span>%SystemRoot%\System32\reg.exe ADD HKLM\SYSTEM\CurrentControlSet\Control\Power\ /v HibernateFileSizePercent /t REG_DWORD /d 0 /f<span style="color:#f92672">&lt;/CommandLine&gt;</span>
                    <span style="color:#f92672">&lt;Order&gt;</span>8<span style="color:#f92672">&lt;/Order&gt;</span>
                    <span style="color:#f92672">&lt;Description&gt;</span>Zero Hibernation File<span style="color:#f92672">&lt;/Description&gt;</span>
                <span style="color:#f92672">&lt;/SynchronousCommand&gt;</span>
                <span style="color:#f92672">&lt;SynchronousCommand</span> <span style="color:#a6e22e">wcm:action=</span><span style="color:#e6db74">&#34;add&#34;</span><span style="color:#f92672">&gt;</span>
                    <span style="color:#f92672">&lt;CommandLine&gt;</span>%SystemRoot%\System32\reg.exe ADD HKLM\SYSTEM\CurrentControlSet\Control\Power\ /v HibernateEnabled /t REG_DWORD /d 0 /f<span style="color:#f92672">&lt;/CommandLine&gt;</span>
                    <span style="color:#f92672">&lt;Order&gt;</span>9<span style="color:#f92672">&lt;/Order&gt;</span>
                    <span style="color:#f92672">&lt;Description&gt;</span>Disable Hibernation Mode<span style="color:#f92672">&lt;/Description&gt;</span>
                <span style="color:#f92672">&lt;/SynchronousCommand&gt;</span>
                <span style="color:#75715e">&lt;!-- END WITH WINDOWS UPDATES --&gt;</span>
            <span style="color:#f92672">&lt;/FirstLogonCommands&gt;</span>
            <span style="color:#f92672">&lt;OOBE&gt;</span>
                <span style="color:#f92672">&lt;HideEULAPage&gt;</span>true<span style="color:#f92672">&lt;/HideEULAPage&gt;</span>
                <span style="color:#f92672">&lt;HideLocalAccountScreen&gt;</span>true<span style="color:#f92672">&lt;/HideLocalAccountScreen&gt;</span>
                <span style="color:#f92672">&lt;HideOEMRegistrationScreen&gt;</span>true<span style="color:#f92672">&lt;/HideOEMRegistrationScreen&gt;</span>
                <span style="color:#f92672">&lt;HideOnlineAccountScreens&gt;</span>true<span style="color:#f92672">&lt;/HideOnlineAccountScreens&gt;</span>
                <span style="color:#f92672">&lt;HideWirelessSetupInOOBE&gt;</span>true<span style="color:#f92672">&lt;/HideWirelessSetupInOOBE&gt;</span>
                <span style="color:#f92672">&lt;NetworkLocation&gt;</span>Home<span style="color:#f92672">&lt;/NetworkLocation&gt;</span>
                <span style="color:#f92672">&lt;ProtectYourPC&gt;</span>1<span style="color:#f92672">&lt;/ProtectYourPC&gt;</span>
            <span style="color:#f92672">&lt;/OOBE&gt;</span>
            <span style="color:#f92672">&lt;UserAccounts&gt;</span>
                <span style="color:#f92672">&lt;AdministratorPassword&gt;</span>
                    <span style="color:#f92672">&lt;Value&gt;</span>adm!nadm!n<span style="color:#f92672">&lt;/Value&gt;</span>
                    <span style="color:#f92672">&lt;PlainText&gt;</span>true<span style="color:#f92672">&lt;/PlainText&gt;</span>
                <span style="color:#f92672">&lt;/AdministratorPassword&gt;</span>
            <span style="color:#f92672">&lt;/UserAccounts&gt;</span>
            <span style="color:#f92672">&lt;RegisteredOwner/&gt;</span>
        <span style="color:#f92672">&lt;/component&gt;</span>
    <span style="color:#f92672">&lt;/settings&gt;</span>
    <span style="color:#f92672">&lt;settings</span> <span style="color:#a6e22e">pass=</span><span style="color:#e6db74">&#34;offlineServicing&#34;</span><span style="color:#f92672">&gt;</span>
        <span style="color:#f92672">&lt;component</span> <span style="color:#a6e22e">xmlns:wcm=</span><span style="color:#e6db74">&#34;http://schemas.microsoft.com/WMIConfig/2002/State&#34;</span> <span style="color:#a6e22e">xmlns:xsi=</span><span style="color:#e6db74">&#34;http://www.w3.org/2001/XMLSchema-instance&#34;</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;Microsoft-Windows-LUA-Settings&#34;</span> <span style="color:#a6e22e">processorArchitecture=</span><span style="color:#e6db74">&#34;amd64&#34;</span> <span style="color:#a6e22e">publicKeyToken=</span><span style="color:#e6db74">&#34;31bf3856ad364e35&#34;</span> <span style="color:#a6e22e">language=</span><span style="color:#e6db74">&#34;neutral&#34;</span> <span style="color:#a6e22e">versionScope=</span><span style="color:#e6db74">&#34;nonSxS&#34;</span><span style="color:#f92672">&gt;</span>
            <span style="color:#f92672">&lt;EnableLUA&gt;</span>false<span style="color:#f92672">&lt;/EnableLUA&gt;</span>
        <span style="color:#f92672">&lt;/component&gt;</span>
    <span style="color:#f92672">&lt;/settings&gt;</span>
    <span style="color:#f92672">&lt;cpi:offlineImage</span> <span style="color:#a6e22e">xmlns:cpi=</span><span style="color:#e6db74">&#34;urn:schemas-microsoft-com:cpi&#34;</span> <span style="color:#a6e22e">cpi:source=</span><span style="color:#e6db74">&#34;wim:c:/wim/install.wim#Windows Server 2019 SERVERSTANDARD&#34;</span><span style="color:#f92672">/&gt;</span>
<span style="color:#f92672">&lt;/unattend&gt;</span>
</code></pre></div><p>This was again taken from Stefan Scherer&rsquo;s repository (<a href="https://github.com/StefanScherer/packer-windows/blob/main/answer_files/2019_core/Autounattend.xml">here</a>) but with a few changes (removing some of the scripts used, and making sure we run the <code>SetUpWinRM.ps1</code> script).</p>
<p>We can now build this with Packer, and then watch the install take place over VNC: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ packer build windows-2019-kube.json
qemu: output will be in this color.

<span style="color:#f92672">==</span>&gt; qemu: Retrieving ISO
<span style="color:#f92672">==</span>&gt; qemu: Trying https://software-download.microsoft.com/download/pr/17763.737.190906-2324.rs5_release_svc_refresh_SERVER_EVAL_x64FRE_en-us_1.iso
<span style="color:#f92672">==</span>&gt; qemu: Trying https://software-download.microsoft.com/download/pr/17763.737.190906-2324.rs5_release_svc_refresh_SERVER_EVAL_x64FRE_en-us_1.iso?checksum<span style="color:#f92672">=</span>sha256%3A549bca46c055157291be6c22a3aaaed8330e78ef4382c99ee82c896426a1cee1
<span style="color:#f92672">==</span>&gt; qemu: https://software-download.microsoft.com/download/pr/17763.737.190906-2324.rs5_release_svc_refresh_SERVER_EVAL_x64FRE_en-us_1.iso?checksum<span style="color:#f92672">=</span>sha256%3A549bca46c055157291be6c22a3aaaed8330e78ef4382c99ee82c896426a1cee1 <span style="color:#f92672">=</span>&gt; isos/17763.737.190906-2324.rs5_release_svc_refresh_SERVER_EVAL_x64FRE_en-us_1.iso
<span style="color:#f92672">==</span>&gt; qemu: Creating floppy disk...
    qemu: Copying files flatly from floppy_files
    qemu: Copying file: http/windows-2019/Autounattend.xml
    qemu: Copying file: scripts/win-common/SetUpWinRM.ps1
    qemu: Done copying files from floppy_files
    qemu: Collecting paths from floppy_dirs
    qemu: Resulting paths from floppy_dirs : <span style="color:#f92672">[]</span>
    qemu: Done copying paths from floppy_dirs
<span style="color:#f92672">==</span>&gt; qemu: Creating required virtual machine disks
<span style="color:#f92672">==</span>&gt; qemu: Found port <span style="color:#66d9ef">for</span> communicator <span style="color:#f92672">(</span>SSH, WinRM, etc<span style="color:#f92672">)</span>: 3887.
<span style="color:#f92672">==</span>&gt; qemu: Looking <span style="color:#66d9ef">for</span> available port between <span style="color:#ae81ff">5900</span> and <span style="color:#ae81ff">6000</span> on 0.0.0.0
<span style="color:#f92672">==</span>&gt; qemu: Starting VM, booting from CD-ROM
    qemu: The VM will be run headless, without a GUI. If you want to
    qemu: view the screen of the VM, connect via VNC without a password to
    qemu: vnc://0.0.0.0:5987
<span style="color:#f92672">==</span>&gt; qemu: Overriding default Qemu arguments with QemuArgs...
<span style="color:#f92672">==</span>&gt; qemu: Waiting 10s <span style="color:#66d9ef">for</span> boot...
<span style="color:#f92672">==</span>&gt; qemu: Connecting to VM via VNC <span style="color:#f92672">(</span>0.0.0.0:5987<span style="color:#f92672">)</span>
<span style="color:#f92672">==</span>&gt; qemu: Typing the boot command over VNC...
    qemu: Not using a NetBridge -- skipping StepWaitGuestAddress
<span style="color:#f92672">==</span>&gt; qemu: Using winrm communicator to connect: 127.0.0.1
<span style="color:#f92672">==</span>&gt; qemu: Waiting <span style="color:#66d9ef">for</span> WinRM to become available...
</code></pre></div><p><img src="/img/kubernetes-win-linux/vnc-packer-windows.png" alt="VNC to Packer image - Windows"></p>
<p>Once the install is finished, Ansible will apply the steps to customize the image.</p>
<h2 id="ansible">Ansible</h2>
<p>As noted, we are using Ansible to customize the images after the operating system has been installed. This installs the correct dependencies (e.g. Docker, Kubernetes, Cloudbase-Init for Windows), ensures we have the correct paths and creates the relevant configuration files.</p>
<p>We make use of roles so that they can be reused for other purposes. Each role is created using <code>ansible-galaxy init $ROLE_NAME</code> (although we are not submitting these to <a href="https://galaxy.ansible.com/">Ansible Galaxy</a>), which will create the following directory structure: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ROLE_NAME
├── defaults
├── files
├── handlers
├── meta
├── README.md
├── tasks
├── templates
├── tests
└── vars
</code></pre></div><p>This allows us to create our tasks in the correct place, create handlers, files and tests should we wish in the correct structure.</p>
<h3 id="linux-ansible-roles">Linux Ansible Roles</h3>
<p>The Linux deployment uses the following roles: -</p>
<ul>
<li><code>base</code> - This covers base packages that will be used either during install, or during day-to-day management of the cluster</li>
<li><code>docker</code> - This installs the Docker engine which Kubernetes will use for deploying containers</li>
<li><code>kubernetes</code> - This installs all the relevant Kubernetes dependencies</li>
</ul>
<h4 id="base">Base</h4>
<p>For the base role, we have the following tasks in our <code>tasks/main.yml</code> file: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e"># tasks file for docker</span>
- <span style="color:#66d9ef">name</span>: Install base dependencies
  <span style="color:#66d9ef">package</span>:
    <span style="color:#66d9ef">name</span>:
      - apt-transport-https 
      - ca-certificates
      - curl 
      - gnupg2 
      - software-properties-common
      - tcpdump
      - wget
      - telnet
      - vim
      - dnsutils
      - mtr-tiny
      - python3-pip
    <span style="color:#66d9ef">state</span>: present
    <span style="color:#66d9ef">update_cache</span>: yes
</code></pre></div><p>The first 5 dependencies are used when adding other repositories (in the <strong>docker</strong> and <strong>kubernetes</strong> roles), whereas the rest are useful if any troubleshooting of the cluster is required.</p>
<h4 id="docker">Docker</h4>
<p>The Docker role has the following tasks in our <code>tasks/main.yml</code> file: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yml" data-lang="yml">---
<span style="color:#75715e"># tasks file for docker</span>
- <span style="color:#66d9ef">name</span>: Add Docker Repository key
  <span style="color:#66d9ef">apt_key</span>:
    <span style="color:#66d9ef">url</span>: https://download.docker.com/linux/debian/gpg
    <span style="color:#66d9ef">state</span>: present

- <span style="color:#66d9ef">name</span>: Add Docker Repository
  <span style="color:#66d9ef">apt_repository</span>:
    <span style="color:#66d9ef">repo</span>: deb [arch=amd64] https://download.docker.com/linux/debian buster stable
    <span style="color:#66d9ef">state</span>: present

- <span style="color:#66d9ef">name</span>: Install Docker
  <span style="color:#66d9ef">package</span>:
    <span style="color:#66d9ef">name</span>:
      - docker-ce
      - docker-ce-cli 
      - containerd.io
    <span style="color:#66d9ef">state</span>: present
    <span style="color:#66d9ef">update_cache</span>: yes
</code></pre></div><p>This adds the Docker APT repository GPG key, adds the Debian Buster Docker repository, and then installs the latest version of Docker Community Edition, the Docker CLI and <code>containerd</code>.</p>
<h4 id="kubernetes">Kubernetes</h4>
<p>The Kubernetes role has the following tasks in our <code>tasks/main.yml</code> file: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e"># tasks file for kubernetes</span>
- <span style="color:#66d9ef">name</span>: Add Kubernetes Repository key
  <span style="color:#66d9ef">apt_key</span>:
    <span style="color:#66d9ef">url</span>: https://packages.cloud.google.com/apt/doc/apt-key.gpg
    <span style="color:#66d9ef">state</span>: present

- <span style="color:#66d9ef">name</span>: Add Kubernetes
  <span style="color:#66d9ef">apt_repository</span>:
    <span style="color:#66d9ef">repo</span>: deb https://apt.kubernetes.io/ kubernetes-xenial main 
    <span style="color:#66d9ef">state</span>: present

- <span style="color:#66d9ef">name</span>: Install Kubernetes
  <span style="color:#66d9ef">package</span>:
    <span style="color:#66d9ef">name</span>:
      - kubelet
      - kubeadm
      - kubectl 
    <span style="color:#66d9ef">state</span>: present
    <span style="color:#66d9ef">update_cache</span>: yes

- <span style="color:#66d9ef">name</span>: Disable Swap
  <span style="color:#66d9ef">shell</span>: <span style="color:#e6db74">|
</span><span style="color:#e6db74">    swapoff -a</span>

- <span style="color:#66d9ef">name</span>: Remove swap in fstab
  <span style="color:#66d9ef">replace</span>:
    <span style="color:#66d9ef">path</span>: /etc/fstab
    <span style="color:#66d9ef">regexp</span>: <span style="color:#e6db74">&#39;^([^#].*?\sswap\s+sw\s+.*)$&#39;</span>
    <span style="color:#66d9ef">replace</span>: <span style="color:#e6db74">&#39;# \1&#39;</span>

- <span style="color:#66d9ef">name</span>: Bridge NF Filter sysctl IPv4
  <span style="color:#66d9ef">ansible.posix.sysctl</span>:
    <span style="color:#66d9ef">name</span>: net.bridge.bridge-nf-call-iptables
    <span style="color:#66d9ef">value</span>: <span style="color:#e6db74">&#39;1&#39;</span>
    <span style="color:#66d9ef">state</span>: present

- <span style="color:#66d9ef">name</span>: Bridge NF Filter sysctl IPv6
  <span style="color:#66d9ef">ansible.posix.sysctl</span>:
    <span style="color:#66d9ef">name</span>: net.bridge.bridge-nf-call-ip6tables
    <span style="color:#66d9ef">value</span>: <span style="color:#e6db74">&#39;1&#39;</span>
    <span style="color:#66d9ef">state</span>: present
</code></pre></div><p>Similar to the Docker role, we add the Kubernetes APT repository GPG key, add the Kubernetes repository (the <code>kubernetes-xenial</code> repository works for all recent versions of Debian and Ubuntu), and installs <code>kubelet</code> (the Kubernetes agent), <code>kubeadm</code> (the Admin tool to create Kubernetes clusters with) and <code>kubectl</code> (the Kubernetes CLI tool).</p>
<p>After this, we disable swap and comment out any swap entries in <code>/etc/fstab</code> as Kubernetes does not allow running with swap partitions enabled.</p>
<p>Finally, we use enable packets that traverse bridges in Linux to be evaluated by IPTables for both IPv4 and IPv6. A lot of the Docker and Kubernetes networking relies on IPTables manipulation, so the packets traversing bridges must go through IPTables as well.</p>
<h3 id="windows-ansible-roles">Windows Ansible Roles</h3>
<p>The Windows deployment uses the following roles: -</p>
<ul>
<li><code>base</code> - This covers setting some default values in the Windows registry for TLS</li>
<li><code>containers</code> - This installs the Windows Containers feature and the Docker engine which Kubernetes will use for deploying containers</li>
<li><code>kubernetes</code> - This installs all the relevant Kubernetes dependencies</li>
<li><code>cloudbase-init</code> - This installs Cloudbase-Init so that we can leverage <code>cloud-config</code> and <code>cloud-init</code> to customize the machine on first boot</li>
</ul>
<h4 id="base-1">Base</h4>
<p>For the base role, we have the following tasks in our <code>tasks/main.yml</code> file: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e"># tasks file for base</span>
  <span style="color:#75715e"># Enable Strong security for Powershell (required for NuGet Provider)</span>
  - <span style="color:#66d9ef">name</span>: Enable TLS <span style="color:#ae81ff">1.2</span> for <span style="color:#ae81ff">64</span>-bit .Net Framework
    <span style="color:#66d9ef">win_regedit</span>:
      <span style="color:#66d9ef">path</span>: HKLM:\SOFTWARE\Wow6432Node\Microsoft\.NetFramework\v4<span style="color:#ae81ff">.0.30319</span>
      <span style="color:#66d9ef">name</span>: SchUseStrongCrypto
      <span style="color:#66d9ef">data</span>: <span style="color:#ae81ff">1</span>
      <span style="color:#66d9ef">type</span>: dword

  - <span style="color:#66d9ef">name</span>: Enable TLS <span style="color:#ae81ff">1.2</span> for <span style="color:#ae81ff">32</span>-bit .Net Framework
    <span style="color:#66d9ef">win_regedit</span>:
      <span style="color:#66d9ef">path</span>: HKLM:\SOFTWARE\Microsoft\.NetFramework\v4<span style="color:#ae81ff">.0.30319</span>
      <span style="color:#66d9ef">name</span>: SchUseStrongCrypto
      <span style="color:#66d9ef">data</span>: <span style="color:#ae81ff">1</span>
      <span style="color:#66d9ef">type</span>: dword
</code></pre></div><p>This task enables TLS 1.2 support for DotNet in Windows. Without this, NuGet and <code>Invoke-WebRequest</code> will often fail. Powershell by default supports SSLv3 or lower, which many sites now have disabled.</p>
<h4 id="containers">Containers</h4>
<p>For the containers role, we have the following tasks in our <code>tasks/main.yml</code> file: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e"># tasks file for containers</span>
  <span style="color:#75715e"># Enable Containers feature</span>
  - <span style="color:#66d9ef">name</span>: features | Enabling Containers Feature
    <span style="color:#66d9ef">win_feature</span>:
      <span style="color:#66d9ef">name</span>: Containers
      <span style="color:#66d9ef">state</span>: present
    <span style="color:#66d9ef">register</span>: windows_container_feature

  - <span style="color:#66d9ef">name</span>: reboot | Rebooting Server
    <span style="color:#66d9ef">win_reboot</span>:
      <span style="color:#66d9ef">reboot_timeout_sec</span>: <span style="color:#ae81ff">3600</span>
    <span style="color:#66d9ef">when</span>: windows_container_feature.changed

  <span style="color:#75715e"># Enable Docker for Windows</span>
  - <span style="color:#66d9ef">name</span>: Add Nuget Package provider
    <span style="color:#66d9ef">win_shell</span>: Install-PackageProvider -Name NuGet -MinimumVersion <span style="color:#ae81ff">2.8.5.201</span> -Force

  - <span style="color:#66d9ef">name</span>: Install Docker Powershell Module Repository for Windows
    <span style="color:#66d9ef">win_psmodule</span>:
      <span style="color:#66d9ef">name</span>: DockerMsftProvider
      <span style="color:#66d9ef">state</span>: present

  - <span style="color:#66d9ef">name</span>: Install Docker for Windows
    <span style="color:#66d9ef">win_shell</span>: Install-Package -Name docker -ProviderName DockerMsftProvider -Force
    <span style="color:#66d9ef">register</span>: docker_installed

  - <span style="color:#66d9ef">name</span>: Enable Docker service
    <span style="color:#66d9ef">win_service</span>:
      <span style="color:#66d9ef">name</span>: docker
      <span style="color:#66d9ef">start_mode</span>: auto
      <span style="color:#66d9ef">state</span>: started
    <span style="color:#66d9ef">when</span>: docker_installed.changed

  - <span style="color:#66d9ef">name</span>: Reboot Windows
    <span style="color:#66d9ef">win_reboot</span>:
      <span style="color:#66d9ef">reboot_timeout_sec</span>: <span style="color:#ae81ff">3600</span>
    <span style="color:#66d9ef">when</span>: docker_installed.changed
</code></pre></div><p>The above does the following: -</p>
<ul>
<li>Enables the Containers feature</li>
<li>Reboots the server if the Containers feature was installed (otherwise there is no need for a reboot)</li>
<li>Adds the ability to use NuGet packages</li>
<li>Adds the Microsoft Docker provider</li>
<li>Installs Docker for Windows</li>
<li>Enables Docker for Windows</li>
<li>Reboots the machine</li>
</ul>
<p>After this, the machine will now have Docker installed, using native Windows containers. This is not the same as Docker Desktop, which runs a lightweight Linux virtual machine for creating and interacting with Linux-based containers.</p>
<h4 id="kubernetes-1">Kubernetes</h4>
<p>For the Kubernetes role, we have the following tasks in our <code>tasks/main.yml</code> file: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e"># tasks file for kubernetes</span>
- <span style="color:#66d9ef">name</span>: Create Kubernetes directory
  <span style="color:#66d9ef">win_file</span>:
    <span style="color:#66d9ef">path</span>: C:\k
    <span style="color:#66d9ef">state</span>: directory

- <span style="color:#66d9ef">name</span>: Add Kubernetes directory to path
  <span style="color:#66d9ef">ansible.windows.win_path</span>:
    <span style="color:#66d9ef">elements</span>:
      - <span style="color:#e6db74">&#39;C:\k&#39;</span>

- <span style="color:#66d9ef">name</span>: Download kubelet
  <span style="color:#66d9ef">win_get_url</span>:
    <span style="color:#66d9ef">url</span>: https://dl.k8s.io/{{ kubernetes_version }}/bin/windows/amd64/kubelet.exe
    <span style="color:#66d9ef">dest</span>: C:\k\kubelet.exe

- <span style="color:#66d9ef">name</span>: Download kubeadm
  <span style="color:#66d9ef">win_get_url</span>:
    <span style="color:#66d9ef">url</span>: https://dl.k8s.io/{{ kubernetes_version }}/bin/windows/amd64/kubeadm.exe
    <span style="color:#66d9ef">dest</span>: C:\k\kubeadm.exe

- <span style="color:#66d9ef">name</span>: Download wins
  <span style="color:#66d9ef">win_get_url</span>:
    <span style="color:#66d9ef">url</span>: https://github.com/rancher/wins/releases/download/v0<span style="color:#ae81ff">.0.4</span>/wins.exe
    <span style="color:#66d9ef">dest</span>: C:\k\wins.exe

- <span style="color:#66d9ef">name</span>: Create Docker host network
  <span style="color:#66d9ef">ansible.windows.win_shell</span>: docker network create -d nat host

- <span style="color:#66d9ef">name</span>: Create wins service
  <span style="color:#66d9ef">ansible.windows.win_shell</span>: wins.exe srv app run --register

- <span style="color:#66d9ef">name</span>: Start wins service
  <span style="color:#66d9ef">ansible.windows.win_service</span>:
    <span style="color:#66d9ef">name</span>: rancher-wins
    <span style="color:#66d9ef">state</span>: restarted

- <span style="color:#66d9ef">name</span>: Create other Kubernetes (and related) directories
  <span style="color:#66d9ef">win_file</span>:
    <span style="color:#66d9ef">path</span>: <span style="color:#e6db74">&#34;{{ item }}&#34;</span>
    <span style="color:#66d9ef">state</span>: directory
  <span style="color:#66d9ef">loop</span>:
    - <span style="color:#e6db74">&#39;C:\var\log\kubelet&#39;</span>
    - <span style="color:#e6db74">&#39;C:\var\lib\kubelet\etc\kubernetes&#39;</span>
    - <span style="color:#e6db74">&#39;C:\etc\kubernetes\pki&#39;</span>
    - <span style="color:#e6db74">&#39;C:\Program Files\nssm&#39;</span>
    - <span style="color:#e6db74">&#39;C:\tmp&#39;</span>

- <span style="color:#66d9ef">name</span>: Symbolic Link for PKI directory
  <span style="color:#66d9ef">ansible.windows.win_shell</span>: New-Item -path C:\var\lib\kubelet\etc\kubernetes\pki -type SymbolicLink -value C:\etc\kubernetes\pki\

- <span style="color:#66d9ef">name</span>: Download nssm
  <span style="color:#66d9ef">win_get_url</span>:
    <span style="color:#66d9ef">url</span>: https://k8stestinfrabinaries.blob.core.windows.net/nssm-mirror/nssm<span style="color:#ae81ff">-2.24</span>.zip
    <span style="color:#66d9ef">dest</span>: <span style="color:#e6db74">&#34;C:\\nssm.zip&#34;</span>

- <span style="color:#66d9ef">name</span>: Unzip nssm
  <span style="color:#66d9ef">community.windows.win_unzip</span>:
    <span style="color:#66d9ef">src</span>: <span style="color:#e6db74">&#34;C:\\nssm.zip&#34;</span>
    <span style="color:#66d9ef">dest</span>: <span style="color:#e6db74">&#34;C:\\tmp&#34;</span>

- <span style="color:#66d9ef">name</span>: Move nssm executable to correct location
  <span style="color:#66d9ef">win_copy</span>:
    <span style="color:#66d9ef">src</span>: <span style="color:#e6db74">&#34;C:\\tmp\\nssm-2.24\\win64\\nssm.exe&#34;</span>
    <span style="color:#66d9ef">dest</span>: <span style="color:#e6db74">&#34;C:\\Program Files\\nssm\\nssm.exe&#34;</span>
    <span style="color:#66d9ef">remote_src</span>: <span style="color:#66d9ef">true</span>

- <span style="color:#66d9ef">name</span>: Add NSSM directory to path
  <span style="color:#66d9ef">ansible.windows.win_path</span>:
    <span style="color:#66d9ef">elements</span>:
      - <span style="color:#e6db74">&#34;C:\\Program Files\\nssm\\&#34;</span>

- <span style="color:#66d9ef">name</span>: Create Kubelet firewall rule
  <span style="color:#66d9ef">community.windows.win_firewall_rule</span>:
    <span style="color:#66d9ef">name</span>: Kubelet
    <span style="color:#66d9ef">localport</span>: <span style="color:#ae81ff">10250</span>
    <span style="color:#66d9ef">action</span>: allow
    <span style="color:#66d9ef">direction</span>: in
    <span style="color:#66d9ef">protocol</span>: tcp
    <span style="color:#66d9ef">state</span>: present
    <span style="color:#66d9ef">enabled</span>: yes
</code></pre></div><p>Most of the steps above are taken from the Windows node preparation Powershell scripts in the Windows SIG Tools repository for Kubernetes. This can be seen <a href="https://github.com/kubernetes-sigs/sig-windows-tools/releases/latest/download/PrepareNode.ps1">here</a>.</p>
<p>Some of the steps differ, in that the Powershell script also starts and registers the Kubelet as a service. It also prepares the Kubelet configuration. These tasks can take place when the node boots for the first time, so they are omitted from this playbook.</p>
<p>To summarize what it does: -</p>
<ul>
<li>Creates a directory for Kubernetes binaries called <code>C:\k</code></li>
<li>Adds it to the <code>$PATH</code> variable</li>
<li>Downloads the Windows version of <code>kubelet</code> and <code>kubeadm</code></li>
<li>Downloads <code>wins</code>
<ul>
<li><code>wins</code> is a tool from Rancher that allows containers to talk to the Windows host they are running on</li>
</ul>
</li>
<li>Creates a Docker network in <code>nat</code> mode so that containers can talk to the internet initially</li>
<li>Starts the <code>wins</code> service</li>
<li>Creates all the directories we need</li>
<li>Links <code>C:\var\lib\kubelet\etc\kubernetes\pki</code> to <code>C:\etc\kubernetes\pki\</code></li>
<li>Downloads <code>nssm</code> (the <a href="https://nssm.cc/">Non-Sucking Service Manager</a>)</li>
<li>Unzips <code>nssm</code>, moves it to the correct directory, and adds the directory to the <code>$PATH</code> variable</li>
<li>Adds a firewall entry in for the Kubelet on <strong>TCP:10250</strong> so that the Control Plane nodes can talk to it</li>
</ul>
<p>After this, all the relevant dependencies are installed, ready for joining a Kubernetes cluster.</p>
<h4 id="cloudbase-init">Cloudbase-Init</h4>
<p>As noted, Cloudbase-Init provides <code>cloud-init</code> functionality to Windows. This means that we can run commands on first boot of the image, providing customization that is only required once (e.g. setting the hostname, configuring the Kubernetes cluster it will connect to, etc).</p>
<p>For the Cloudbase-Init role, we have the following tasks in our <code>tasks/main.yml</code> file: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e"># tasks file for cloudbase-init</span>
  - <span style="color:#66d9ef">name</span>: Get Cloudbase-Init
    <span style="color:#66d9ef">win_get_url</span>:
      <span style="color:#66d9ef">url</span>: https://cloudbase.it/downloads/CloudbaseInitSetup_x64.msi
      <span style="color:#66d9ef">dest</span>: C:\Users\Administrator\cloudbase-init.msi

  - <span style="color:#66d9ef">name</span>: Install Cloudbase-Init
    <span style="color:#66d9ef">win_package</span>:
      <span style="color:#66d9ef">path</span>: C:\Users\Administrator\cloudbase-init.msi

  - <span style="color:#66d9ef">name</span>: Add Cloudbase Init config files
    <span style="color:#66d9ef">win_copy</span>:
      <span style="color:#66d9ef">src</span>: <span style="color:#e6db74">&#34;{{ item }}&#34;</span>
      <span style="color:#66d9ef">dest</span>: <span style="color:#e6db74">&#34;C:\\Program Files\\Cloudbase Solutions\\cloudbase-init\\conf\\{{ item }}&#34;</span>
    <span style="color:#66d9ef">loop</span>:
      - cloudbase-init.conf
      - cloudbase-init-unattend.conf
</code></pre></div><p>In this we download the Cloudbase-Init MSI, install it, and then provide configuration files that are customized for our purpose. These configuration files customize what kind of <code>cloud-init</code> format, what <code>cloud-init</code> modules are available and more.</p>
<p>For more details on these configuration files, see the <a href="/posts/proxmox-terraform-cloudinit-windows/#configuring-cloudbase-init">Configuring Cloudbase-Init</a> of my &ldquo;Using Terraform and Cloud-Init to deploy and automatically monitor Proxmox instances - Windows&rdquo; post.</p>
<p>Once this is installed, Packer will then be able to run the <strong>sysprep</strong> command using the Cloudbase-Init included <code>Unattend.xml</code> file: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-powershell" data-lang="powershell">C:\Windows\System32\Sysprep\sysprep.exe 
    /quiet /generalize /oobe  
    /unattend<span style="color:#960050;background-color:#1e0010">:</span>C:\Program Files\Cloudbase Solutions\Cloudbase-Init\conf\Unattend.xml
</code></pre></div><h2 id="videos">Videos</h2>
<p>Below are a couple of videos/screen captures of the process. Both are quite long, with each install taking around 15 minutes, so feel free to skip to the parts that are relevant to you! Both are best viewed in 1080p resolution, and full screen where possible.</p>
<h3 id="building-a-linux-based-kubernetes-image-for-kvm-using-packer-and-ansible">Building a Linux-based Kubernetes image for KVM using Packer and Ansible</h3>

<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe src="https://www.youtube.com/embed/9lo5FAikSno" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allowfullscreen title="YouTube Video"></iframe>
</div>

<h3 id="building-a-windows-based-kubernetes-image-for-kvm-using-packer-and-ansible">Building a Windows-based Kubernetes image for KVM using Packer and Ansible</h3>

<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe src="https://www.youtube.com/embed/XUwexO4zpv0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allowfullscreen title="YouTube Video"></iframe>
</div>

<h2 id="next-steps">Next steps</h2>
<p>The next step after this will be to move the generated images to your KVM storage location. For me this is in <code>/var/lib/libvirt/images</code> (the default install location).</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># Move the Linux image</span>
$ mv artifacts/qemu/debian10-kube/debian10 /var/lib/libvirt/images/debian10-kube.qcow2

<span style="color:#75715e"># Move the Windows image</span>
$ mv artifacts/qemu/win2019-kube/windows-server-2019-standard-x64-eval /var/lib/libvirt/images/debian2019-kube.qcow2
</code></pre></div><p>Once this is done, we can create our Terraform configuration to deploy the Kubernetes cluster. This will be covered in Part 2 of this series.</p>
<h2 id="summary">Summary</h2>
<p>In this, we&rsquo;ve seen how we can use Packer and Ansible to create both Linux and Windows images. While Ansible is well known for configuration management of Linux instances, it is just as capable of managing Windows, BSD, networking equipment and more.</p>
<p>The next part will use our images to create three Kubernetes Control Plane nodes (running Linux), three Linux Kubernetes workers and one Windows Kubernetes worker. This will all be managed by Terraform and Cloud-Init (using the native Cloud-Init packages in Debian, and Cloudbase-Init for Windows).</p>
<p>To see the JSON files, Ansible roles and everything else used in this post, see my <a href="https://gitlab.com/stuh84/packer">Packer Repository</a> on Gitlab</p>
]]></content>
        </item>
        
        <item>
            <title>Ansible for Networking - Part 7: VyOS</title>
            <link>https://yetiops.net/posts/ansible-for-networking-part-7-vyos/</link>
            <pubDate>Fri, 25 Sep 2020 14:00:41 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/ansible-for-networking-part-7-vyos/</guid>
            <description>The sixth part of my ongoing series of posts on Ansible for Networking will cover VyOS. You can view the other posts in the series below: -
 Part 1 - Start of the series Part 2 - The Lab Environment Part 3 - Cisco IOS Part 4 - Juniper JunOS Part 5 - Arista EOS Part 6 - Mikrotik RouterOS  All the playbooks, roles and variables used in this article are available in my Network Automation with Ansible repository.</description>
            <content type="html"><![CDATA[<p>The sixth part of my ongoing series of posts on Ansible for Networking will cover VyOS. You can view the other posts in the series below: -</p>
<ul>
<li><a href="/posts/ansible-for-networking-series">Part 1 - Start of the series</a></li>
<li><a href="/posts/ansible-for-networking-part-2-lab">Part 2 - The Lab Environment</a></li>
<li><a href="/posts/ansible-for-networking-part-3-cisco-ios">Part 3 - Cisco IOS</a></li>
<li><a href="/posts/ansible-for-networking-part-4-juniper-junos">Part 4 - Juniper JunOS</a></li>
<li><a href="/posts/ansible-for-networking-part-5-arista-eos">Part 5 - Arista EOS</a></li>
<li><a href="/posts/ansible-for-networking-part-6-mikrotik-routeros">Part 6 - Mikrotik RouterOS</a></li>
</ul>
<p>All the playbooks, roles and variables used in this article are available in my <a href="https://gitlab.com/stuh84/network-automation-ansible">Network Automation with Ansible repository</a>.</p>
<h2 id="why-vyos">Why VyOS?</h2>
<p>VyOS is a fork of Vyatta. Vyatta was started in 2006 to compete with the likes of Cisco and Juniper by providing a Linux-based (Debian specifically) network platform which can be run on bare metal, or in virtual machines. It can be used anywhere from a home router, <a href="https://www.vyos.io/solutions/vyos-on-aws/">in the cloud</a> to the edge of a <a href="https://faelix.net/news/201910/vyos-salt-netbox-peering-and-transit-upgrade/">service provider network</a> and everywhere in between.</p>
<p>When Vyatta was acquired in by Brocade in 2012, a group of developers forked the last community release of Vyatta and created the VyOS project. Also, Ubiquiti Networks forked Vyatta themselves, creating <a href="https://www.ui.com/edgemax/edgerouter/">EdgeOS</a>. While VyOS and EdgeOS no longer share the same codebase, there are enough similarities that switching between the two is not too difficult.</p>
<p>VyOS is well suited to use either in virtualisation environments, labs, clouds, low powered hardware, or as the primary routing platform in a network. This versatility is why I have chosen to cover it.</p>
<h3 id="configuration-style">Configuration Style</h3>
<p>The configuration approach within VyOS is similar to Juniper&rsquo;s JunOS. For example, to configure an interface in JunOS, you would use something like: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ansible@junos-01# set interfaces fxp0 unit 0 family inet address 10.15.30.33/24
</code></pre></div><p>The equivalent in VyOS is: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">vyos@vyos-01# set interfaces ethernet eth1 address &#39;10.15.30.63/24&#39;
</code></pre></div><p>Also, VyOS uses a similar candidate configuration system, where changes are made to the candidate configuration and then committed to the running configuration. You can check what changes will be made using <code>show | compare</code> (just like in JunOS).</p>
<p>The VyOS syntax does have many similarities to IOS though. For example, VyOS uses route maps (like IOS) rather than route policies (like JunOS).</p>
<p>Many of the verification commands are identical to Cisco: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">vyos@vyos-01:~$ show ip bgp summary

IPv4 Unicast Summary:
BGP router identifier 192.0.2.105, local AS number 65105 vrf-id 0
BGP table version 8
RIB entries 15, using 2760 bytes of memory
Peers 2, using 41 KiB of memory

Neighbor        V         AS MsgRcvd MsgSent   TblVer  InQ OutQ  Up/Down State/PfxRcd
10.100.105.254  4      65430      36      36        0    0    0 00:30:08            1
192.0.2.205     4      65105      32      35        0    0    0 00:29:19            0

Total number of neighbors 2
vyos@vyos-01:~$ show ip ospf neighbor

Neighbor ID     Pri State           Dead Time Address         Interface                        RXmtL RqstL DBsmL
192.0.2.205       1 Full/DR           33.863s 10.100.205.253  eth2.205:10.100.205.254              0     0     0
</code></pre></div><p>If you are well versed in both IOS and JunOS, VyOS will feel very familiar.</p>
<h2 id="objectives">Objectives</h2>
<p>For each vendor, I will be using Ansible to configure two routers/switches/firewalls/appliances.</p>
<p>One will serve as the <em>Edge</em> router, connecting to the Internet and also via BGP to the <strong>Net Server</strong>. The <strong>Net Server</strong> is a CentOS 8 Virtual Machine acting as a route server, syslog collector and TACACS+ server (detailed in <a href="/posts/ansible-for-networking-part-2-lab">The Lab Environment</a>)</p>
<p>The other will be an <em>internal</em> router, performing <strong>core</strong> functions (i.e. internal routing rather than external).</p>
<p>This lab is based upon the rolling release version of VyOS.</p>
<h3 id="edge-router">Edge router</h3>
<p>The edge router will run the following: -</p>
<ul>
<li><strong>External BGP</strong> (eBGP) to the <strong>Net Server</strong>
<ul>
<li>Advertising internal networks</li>
</ul>
</li>
<li><strong>Internal BGP</strong> (iBGP) to the <strong>Internal</strong> router
<ul>
<li>Advertising any routes received from the <strong>Net Server</strong></li>
<li>Advertising a default route (for internet access)</li>
</ul>
</li>
<li><strong>OSPF</strong>
<ul>
<li>Advertising loopbacks and internal networks between both routers</li>
</ul>
</li>
<li><strong>IPv4</strong> and <strong>IPv6</strong> routing
<ul>
<li>Using OSPFv3 (for <strong>IPv6</strong> support)</li>
<li>Using the IPv6 <em>Address Family</em> for BGP</li>
</ul>
</li>
<li><strong>SNMPv3</strong> for monitoring</li>
<li><strong>IPv4 NAT</strong> to allow internet access
<ul>
<li>I cannot run IPv6 for internet access, as my current ISP does not support IPv6</li>
</ul>
</li>
<li><strong>Logging</strong> via Syslog to the <strong>Net Server</strong></li>
<li><strong>Authentication, Authorization and Accounting</strong> (AAA) via <del>TACACS+</del> RADIUS to the <strong>Net Server</strong></li>
<li><strong>Zones</strong> to place interfaces in, for zone-based firewalling</li>
<li><strong>Firewall Rules</strong> to allow traffic to/from the <strong>Net Server</strong>, and between the two routers</li>
</ul>
<p>Notice in the AAA objective, we are using RADIUS, rather than TACACS+. This is because VyOS (like RouterOS) does not support TACACS+.</p>
<h3 id="internal-router">Internal router</h3>
<p>The internal router runs a subset of the functions that the edge router does: -</p>
<ul>
<li><strong>Internal BGP</strong> (iBGP) to the <strong>Edge</strong> router
<ul>
<li>Receiving any routes received from the <strong>Net Server</strong></li>
<li>Receiving a default route (for internet access)</li>
</ul>
</li>
<li><strong>OSPF</strong>
<ul>
<li>Advertising loopbacks and internal networks between both routers</li>
</ul>
</li>
<li><strong>IPv4</strong> and <strong>IPv6</strong> routing
<ul>
<li>Using OSPFv3 (for <strong>IPv6</strong> support)</li>
<li>Using the IPv6 <em>Address Family</em> for BGP</li>
</ul>
</li>
<li><strong>SNMPv3</strong> for monitoring</li>
<li><strong>Logging</strong> via Syslog to the <strong>Net Server</strong></li>
<li><strong>Authentication, Authorization and Accounting</strong> (AAA) via <del>TACACS+</del> RADIUS to the <strong>Net Server</strong></li>
</ul>
<h2 id="prerequisites">Prerequisites</h2>
<p>To manage a VyOS device with Ansible, the following steps are required. We also make some changes to the default Ansible connection configuration.</p>
<h3 id="ansible-configuration">Ansible Configuration</h3>
<p>The following defaults are required to use Ansible with VyOS: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">nsible_connection</span>: network_cli
<span style="color:#66d9ef">ansible_network_os</span>: vyos
<span style="color:#66d9ef">ansible_user</span>: vyos
</code></pre></div><p>As with JunOS, there is no <strong>enable</strong> or <strong>privileged</strong> mode within VyOS. Instead, the user privileges determine whether a user can run commands or make changes.</p>
<p>We do not need to supply a password, as authentication is via SSH public keys.</p>
<h3 id="vyos-configuration">VyOS Configuration</h3>
<p>To allow Ansible access to the VyOS routers, either use an existing SSH key pair, or create a new SSH key pair. Take the contents of the public key, and add them to the routers like so: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt"># View SSH key
$ less ~/.ssh/id_ed25519.pub 
ssh-ed25519 ###REDACTED### stuh84@symphonyx

# Go into configuration mode on the router
vyos@vyos-01:~$ configure

[edit]
vyos@vyos-01# set system login user vyos authentication public-keys stuh84@symphonyx key &#39;###REDACTED###&#39;

[edit]
vyos@vyos-01# set system login user vyos authentication public-keys stuh84@symphonyx type &#39;ssh-ed25519&#39;

# Set the management IP
[edit]
vyos@vyos-01# set interfaces ethernet eth1 address &#39;10.15.30.63/24&#39;

[edit]
vyos@vyos-01# set interfaces ethernet eth1 description &#39;Management&#39;
</code></pre></div><p>You should now be able to SSH into the router without a password.</p>
<p>Our inventory file looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">[vyos]
vyos<span style="color:#ae81ff">-01</span> ansible_host=<span style="color:#ae81ff">10.15.30.63</span>
vyos<span style="color:#ae81ff">-02</span> ansible_host=<span style="color:#ae81ff">10.15.30.64</span>
</code></pre></div><h4 id="verification">Verification</h4>
<p>Can we contact both devices?</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ansible vyos -m vyos_facts --ask-vault-pass | grep -i hostname
Vault password:
        <span style="color:#e6db74">&#34;ansible_net_hostname&#34;</span>: <span style="color:#e6db74">&#34;vyos-01&#34;</span>,
        <span style="color:#e6db74">&#34;ansible_net_hostname&#34;</span>: <span style="color:#e6db74">&#34;vyos-02&#34;</span>,
</code></pre></div><h2 id="setup">Setup</h2>
<p>The setup is identical to the IOS and Juniper lab, with a management interface to access the devices, a VLAN bridge for inter-device communication, and an interface on the edge router attached to the KVM NAT bridge for DHCP/Internet access. Unlike the Arista vEOS images, VLANs work correctly in the lab.</p>
<h3 id="vlans-ip-addressing-and-autonomous-system-numbers">VLANs, IP addressing and Autonomous System numbers</h3>
<p>The ID chosen for VyOS is <code>05</code>.</p>
<h4 id="vlans">VLANs</h4>
<p>The VLANs used will be: -</p>
<ul>
<li>VLAN105 between the <em>edge</em> router and <em>netsvr-01</em></li>
<li>VLAN205 between the <em>edge</em> router and <em>internal</em> router</li>
</ul>
<h4 id="ip-addressing">IP Addressing</h4>
<ul>
<li>IPv4 Subnet on VLAN105: <code>10.100.105.0/24</code>
<ul>
<li><em>edge</em> router - <code>10.100.105.253/24</code></li>
<li><em>netsvr-01</em> - <code>10.100.105.254/24</code></li>
</ul>
</li>
<li>IPv4 Subnet on VLAN205: 10.100.205.0/24
<ul>
<li><em>edge</em> router - <code>10.100.205.254/24</code></li>
<li><em>internal</em> router - <code>10.100.205.253/24</code></li>
</ul>
</li>
<li>IPv6 Subnet on VLAN105: <code>2001:db8:105::/64</code>
<ul>
<li><em>edge</em> router - <code>2001:db8:105::f/64</code></li>
<li><em>netsvr-01</em> - <code>2001:db8:105:ffff/64</code></li>
</ul>
</li>
<li>IPv6 Subnet on VLAN205: 2001:db8:205::/64
<ul>
<li><em>edge</em> router - <code>2001:db8:205::a/64</code></li>
<li><em>internal</em> router - <code>2001:db8:205:f/64</code></li>
</ul>
</li>
<li>IPv4 Loopback Addressing
<ul>
<li><em>edge</em> router - <code>192.0.2.105/32</code></li>
<li><em>internal</em> router - <code>192.0.2.205/32</code></li>
</ul>
</li>
<li>IPv6 Loopback Address
<ul>
<li><em>edge</em> router - <code>2001:db8:905:beef::1/128</code></li>
<li><em>internal</em> router - <code>2001:db8:905:beef::2/128</code></li>
</ul>
</li>
</ul>
<h4 id="bgp-autonomous-system">BGP Autonomous System</h4>
<p>The BGP <a href="https://en.wikipedia.org/wiki/Autonomous_system_(Internet)">Autonomous System</a> number will be <code>AS65105</code>.</p>
<h2 id="configuration">Configuration</h2>
<p>Unlike RouterOS, there are quite a few Ansible modules for VyOS. It does not have as many as something like Cisco NX-OS, but has more than RouterOS or Extreme EXOS.</p>
<h3 id="system-tasks">System Tasks</h3>
<p>As noted in previous parts, the system tasks setup basic logging, banners and the hostname.</p>
<h4 id="playbook">Playbook</h4>
<p>The contents of the playbook are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e"># tasks file for system</span>
- <span style="color:#66d9ef">name</span>: Set hostname
  <span style="color:#66d9ef">vyos_system</span>:
    <span style="color:#66d9ef">host_name</span>: <span style="color:#e6db74">&#34;{{ inventory_hostname }}&#34;</span>

- <span style="color:#66d9ef">name</span>: Remove unneeded banners
  <span style="color:#66d9ef">vyos_banner</span>:
    <span style="color:#66d9ef">banner</span>: <span style="color:#e6db74">&#34;{{ item }}&#34;</span>
    <span style="color:#66d9ef">state</span>: absent
  <span style="color:#66d9ef">loop</span>:
  - post-login

- <span style="color:#66d9ef">name</span>: Update login banner
  <span style="color:#66d9ef">vyos_banner</span>:
    <span style="color:#66d9ef">banner</span>: pre-login
    <span style="color:#66d9ef">text</span>: <span style="color:#e6db74">|
</span><span style="color:#e6db74">      ----------------------------------------</span>
      <span style="color:#e6db74">|
</span><span style="color:#e6db74">      | This banner was generated by Ansible</span>
      <span style="color:#e6db74">|
</span><span style="color:#e6db74">      ----------------------------------------</span>
      <span style="color:#e6db74">|
</span><span style="color:#e6db74">      | You are logged into {{ inventory_hostname }}</span>
      <span style="color:#e6db74">|
</span><span style="color:#e6db74">      ----------------------------------------</span>
      <span style="color:#e6db74">|
</span><span style="color:#e6db74">    state: present</span>

- <span style="color:#66d9ef">name</span>: Configure syslog
  <span style="color:#66d9ef">vyos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#e6db74">&#34;set system syslog host {{ log_host }} facility all level info&#34;</span>
</code></pre></div><h5 id="setting-hostname">Setting Hostname</h5>
<p><strong>Ansible module</strong>: <code>vyos_system</code></p>
<p>This task sets the hostname of the device. The generated configuration is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set system host-name vyos-01
</code></pre></div><h5 id="removing-unneeded-banners">Removing unneeded banners</h5>
<p><strong>Ansible module</strong>: <code>vyos_banner</code></p>
<p>This removes the post-login banner, as we only use a pre-login banner.</p>
<h5 id="update-the-login-banner">Update the login banner</h5>
<p><strong>Ansible module</strong>: <code>vyos_banner</code></p>
<p>This task generates a banner for when you login to a device, which references the hostname. As in the Cisco IOS version, you could use a template file to generate this, especially if you have to provide specific information for legal/compliance reasons.</p>
<p>The generated configuration looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set system login banner pre-login &#39;----------------------------------------\n|\n| This banner was generated by Ansible \n|\n----------------------------------------\n|\n| You are logged into vyos-01\n| \n----------------------------------------\n|&#39;
</code></pre></div><p>When you login to the device, this looks like: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">$ ssh vyos@10.15.30.63
----------------------------------------
|
| This banner was generated by Ansible
|
----------------------------------------
|
| You are logged into vyos-01
|
----------------------------------------
|
Linux vyos-01 4.19.136-amd64-vyos #1 SMP Sat Aug 1 08:40:04 UTC 2020 x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.

Last login: Mon Aug  3 19:02:18 2020 from 10.15.30.1
vyos@vyos-01:~$
</code></pre></div><p>You also get the standard Debian MOTD banner.</p>
<h5 id="configure-syslog">Configure syslog</h5>
<p><strong>Ansible module</strong>: <code>vyos_config</code></p>
<p>Unlike JunOS, there is no <code>vyos_logging</code> module, so we use <code>vyos_config</code>. Like in IOS, JunOS and EOS, <code>vyos_config</code> can apply lines of configuration (using the <code>set</code> syntax) or it can apply commands from a template.</p>
<p>As we are only apply one line of configuration, we use the <code>lines</code> option to apply it. The generated configuration is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set system syslog host 10.100.105.254 facility all level &#39;info&#39;
</code></pre></div><h3 id="interfaces">Interfaces</h3>
<p>This role configures all the interfaces being used on the VyOS device. VyOS calls VLAN subinterfaces <strong>vifs</strong>, rather than <strong>units</strong> (like in JunOS) or <strong>subinterfaces</strong> (like in IOS/EOS).</p>
<h4 id="playbook-1">Playbook</h4>
<p>The contents of the Playbook are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e"># tasks file for interfaces</span>
- <span style="color:#66d9ef">name</span>: Configure interfaces - Status and Descriptions
  <span style="color:#66d9ef">vyos_interfaces</span>:
    <span style="color:#66d9ef">config</span>:
      - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;{{ item.vyos_if }}&#34;</span>
        <span style="color:#66d9ef">description</span>: <span style="color:#e6db74">&#34;{{ item.desc }}&#34;</span>
        <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;{{ item.enabled }}&#34;</span>
  <span style="color:#66d9ef">when</span>: item.vif is not defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>

- <span style="color:#66d9ef">name</span>: Configure interfaces - Status and Descriptions - vifs
  <span style="color:#66d9ef">vyos_interfaces</span>:
    <span style="color:#66d9ef">config</span>:
      - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;{{ item.vyos_if }}&#34;</span>
        <span style="color:#66d9ef">vifs</span>:
          - <span style="color:#66d9ef">description</span>: <span style="color:#e6db74">&#34;{{ item.desc }}&#34;</span>
            <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;{{ item.enabled }}&#34;</span>
            <span style="color:#66d9ef">vlan_id</span>: <span style="color:#e6db74">&#34;{{ item.vif }}&#34;</span>
  <span style="color:#66d9ef">when</span>: item.vif is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>

- <span style="color:#66d9ef">name</span>: Configure interfaces - L3 IPv4
  <span style="color:#66d9ef">vyos_l3_interfaces</span>:
    <span style="color:#66d9ef">config</span>:
      - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;{{ item.vyos_if }}&#34;</span>
        <span style="color:#66d9ef">ipv4</span>:
        - <span style="color:#66d9ef">address</span>: <span style="color:#e6db74">&#34;{{ item.ipv4_addr }}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - item.ipv4_addr is defined
    - item.vif is not defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>

- <span style="color:#66d9ef">name</span>: Configure interfaces - L3 IPv4 - vifs
  <span style="color:#66d9ef">vyos_l3_interfaces</span>:
    <span style="color:#66d9ef">config</span>:
      - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;{{ item.vyos_if }}&#34;</span>
        <span style="color:#66d9ef">vifs</span>:
          - <span style="color:#66d9ef">ipv4</span>:
            - <span style="color:#66d9ef">address</span>: <span style="color:#e6db74">&#34;{{ item.ipv4_addr }}&#34;</span>
            <span style="color:#66d9ef">vlan_id</span>: <span style="color:#e6db74">&#34;{{ item.vif }}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - item.ipv4_addr is defined
    - item.vif is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>

- <span style="color:#66d9ef">name</span>: Configure interfaces - L3 IPv6
  <span style="color:#66d9ef">vyos_l3_interfaces</span>:
    <span style="color:#66d9ef">config</span>:
      - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;{{ item.vyos_if }}&#34;</span>
        <span style="color:#66d9ef">ipv6</span>:
        - <span style="color:#66d9ef">address</span>: <span style="color:#e6db74">&#34;{{ item.ipv6_addr }}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - item.ipv6_addr is defined
    - item.vif is not defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>

- <span style="color:#66d9ef">name</span>: Configure interfaces - L3 IPv6 - vifs
  <span style="color:#66d9ef">vyos_l3_interfaces</span>:
    <span style="color:#66d9ef">config</span>:
      - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;{{ item.vyos_if }}&#34;</span>
        <span style="color:#66d9ef">vifs</span>:
          - <span style="color:#66d9ef">ipv6</span>:
             - <span style="color:#66d9ef">address</span>: <span style="color:#e6db74">&#34;{{ item.ipv6_addr }}&#34;</span>
            <span style="color:#66d9ef">vlan_id</span>: <span style="color:#e6db74">&#34;{{ item.vif }}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - item.ipv6_addr is defined
    - item.vif is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
</code></pre></div><p>Unlike in JunOS, we can configure all VLAN interfaces and non-VLAN interfaces using Ansible modules. Both <code>vyos_interfaces</code> and <code>vyos_l3_interfaces</code> support <strong>vif</strong> configuration.</p>
<h5 id="configure-interfaces---status-and-descriptions">Configure Interfaces - Status And Descriptions</h5>
<p><strong>Ansible module</strong>: <code>vyos_interfaces</code></p>
<p>This task configures the descriptions and status (i.e. enabled or disabled) of each non-VLAN interface. For any interface without the <code>vif</code> value defined, the description and status will be updated. For example, the below is a summarized version of the <code>host_vars</code> for <code>vyos-01</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth1&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Management&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth2&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;VLAN Bridge&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth2&#34;</span>
    <span style="color:#66d9ef">vif</span>: <span style="color:#ae81ff">105</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To netsvr&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth2&#34;</span>
    <span style="color:#66d9ef">vif</span>: <span style="color:#ae81ff">205</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To vyos-02&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth0&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To the Internet&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;lo&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
</code></pre></div><p>This task would apply to: -</p>
<ul>
<li><code>eth0</code></li>
<li><code>eth1</code></li>
<li><code>eth2</code></li>
<li><code>lo</code> (the loopback interface)</li>
</ul>
<p>The following configuration is generated: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set interfaces ethernet eth0 description &#39;To the Internet&#39;
set interfaces ethernet eth1 description &#39;Management&#39;
set interfaces ethernet eth2 description &#39;VLAN Bridge&#39;
set interfaces loopback lo description &#39;Loopback&#39;
</code></pre></div><h5 id="configure-interfaces---status-and-descriptions-vlans">Configure Interfaces - Status And Descriptions (VLANs)</h5>
<p><strong>Ansible module</strong>: <code>vyos_interfaces</code></p>
<p>This task is the same as the previous task, except it applies the changes for <code>vif</code> interfaces (i.e. VLAN interfaces). Based upon the previous <code>host_vars</code>, we would generate configuration for: -</p>
<ul>
<li><code>eth2.105</code></li>
<li><code>eth2.205</code></li>
</ul>
<p>The following configuration is generated: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set interfaces ethernet eth2 vif 105 description &#39;To netsvr&#39;
set interfaces ethernet eth2 vif 205 description &#39;To vyos-02&#39;
</code></pre></div><h5 id="configure-interfaces---l3-ipv4">Configure interfaces - L3 IPv4</h5>
<p><strong>Ansible module</strong>: <code>vyos_l3_interfaces</code></p>
<p>This task configures the IPv4 addressing of each non-VLAN interface. This goes through the <code>host_vars</code>, and for every interface that has an IPv4 address (and no <code>vif</code> value), an IPv4 address is configured. The relevant <code>host_vars</code> are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth1&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Management&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.15.30.63/24&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth2&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;VLAN Bridge&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth2&#34;</span>
    <span style="color:#66d9ef">vif</span>: <span style="color:#ae81ff">105</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To netsvr&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.100.105.253/24&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth2&#34;</span>
    <span style="color:#66d9ef">vif</span>: <span style="color:#ae81ff">205</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To vyos-02&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.100.205.254/24&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth0&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To the Internet&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;dhcp&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;lo&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;192.0.2.105/32&#34;</span>
</code></pre></div><p>This task would then configure: -</p>
<ul>
<li><code>eth0</code> (using DHCP)</li>
<li><code>eth1</code></li>
<li><code>lo</code></li>
</ul>
<p>This generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set interfaces ethernet eth0 address &#39;dhcp&#39;
set interfaces ethernet eth1 address &#39;10.15.30.63/24&#39;
set interfaces loopback lo address &#39;192.0.2.105/32&#39;
</code></pre></div><h5 id="configure-interfaces---l3-ipv4-vlans">Configure interfaces - L3 IPv4 (VLANs)</h5>
<p><strong>Ansible module</strong>: <code>vyos_l3_interfaces</code></p>
<p>This task is the same as the previous one, except it applies to  VLAN interfaces (<code>vif</code>). Based upon the previous <code>host_vars</code>, the following interfaces would be configured: -</p>
<ul>
<li><code>eth2.105</code></li>
<li><code>eth2.205</code></li>
</ul>
<p>This generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set interfaces ethernet eth2 vif 105 address &#39;10.100.105.253/24&#39;
set interfaces ethernet eth2 vif 205 address &#39;10.100.205.254/24&#39;
</code></pre></div><h5 id="configure-interfaces---l3-ipv6">Configure interfaces - L3 IPv6</h5>
<p><strong>Ansible module</strong>: <code>vyos_l3_interfaces</code></p>
<p>This task is identical to the one for IPv4, except it applies IPv6 addresses instead. The following are the relevant <code>host_vars</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth1&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Management&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.15.30.63/24&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth2&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;VLAN Bridge&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth2&#34;</span>
    <span style="color:#66d9ef">vif</span>: <span style="color:#ae81ff">105</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To netsvr&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.100.105.253/24&#34;</span>
    <span style="color:#66d9ef">ipv6_addr</span>: <span style="color:#e6db74">&#34;2001:db8:105::f/64&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth2&#34;</span>
    <span style="color:#66d9ef">vif</span>: <span style="color:#ae81ff">205</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To vyos-02&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.100.205.254/24&#34;</span>
    <span style="color:#66d9ef">ipv6_addr</span>: <span style="color:#e6db74">&#34;2001:db8:205::a/64&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth0&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To the Internet&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;dhcp&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;lo&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;192.0.2.105/32&#34;</span>
    <span style="color:#66d9ef">ipv6_addr</span>: <span style="color:#e6db74">&#34;2001:db8:905:beef::1/128&#34;</span>
</code></pre></div><p>We would then configure the following interfaces: -</p>
<ul>
<li><code>lo</code></li>
</ul>
<p>This would generate the following configuration :-</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set interfaces loopback lo address &#39;2001:db8:905:beef::1/128&#39;
</code></pre></div><h5 id="configure-interfaces---l3-ipv6-vlans">Configure interfaces - L3 IPv6 (VLANs)</h5>
<p><strong>Ansible module</strong>: <code>vyos_l3_interfaces</code></p>
<p>As with the previous task, this is identical to the IPv4 task, except applying IPv6 addresses. Based upon the previous <code>host_vars</code>, we would configure: -</p>
<ul>
<li><code>eth2.105</code></li>
<li><code>eth2.205</code></li>
</ul>
<p>This would generate the following configuration :-</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set interfaces ethernet eth2 vif 105 address &#39;2001:db8:105::f/64&#39;
set interfaces ethernet eth2 vif 205 address &#39;2001:db8:205::a/64&#39;
</code></pre></div><h4 id="verification-1">Verification</h4>
<p><strong>vyos-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show IPs (IPv4 and IPv6), interface status and descriptions
vyos@vyos-01:~$ show interfaces
Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down
Interface        IP Address                        S/L  Description
---------        ----------                        ---  -----------
eth0             192.168.122.182/24                u/u  To the Internet
eth1             10.15.30.63/24                    u/u  Management
eth2             -                                 u/u  VLAN Bridge
eth2.105         10.100.105.253/24                 u/u  To netsvr
                 2001:db8:105::f/64
eth2.205         10.100.205.254/24                 u/u  To vyos-02
                 2001:db8:205::a/64
eth3             192.168.30.10/24                  u/u
lo               127.0.0.1/8                       u/u  Loopback
                 192.0.2.105/32
                 2001:db8:905:beef::1/128
                 ::1/128

! Ping to netsvr-01 on IPv4 and IPv6
vyos@vyos-01:~$ ping 10.100.105.254
PING 10.100.105.254 (10.100.105.254) 56(84) bytes of data.
64 bytes from 10.100.105.254: icmp_seq=1 ttl=64 time=0.580 ms
64 bytes from 10.100.105.254: icmp_seq=2 ttl=64 time=0.881 ms
^C
--- 10.100.105.254 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 7ms
rtt min/avg/max/mdev = 0.580/0.730/0.881/0.152 ms
vyos@vyos-01:~$ ping 2001:db8:105::ffff
PING 2001:db8:105::ffff(2001:db8:105::ffff) 56 data bytes
64 bytes from 2001:db8:105::ffff: icmp_seq=1 ttl=64 time=0.518 ms
64 bytes from 2001:db8:105::ffff: icmp_seq=2 ttl=64 time=1.07 ms
64 bytes from 2001:db8:105::ffff: icmp_seq=3 ttl=64 time=1.10 ms
^C
--- 2001:db8:105::ffff ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 28ms
rtt min/avg/max/mdev = 0.518/0.898/1.103/0.270 ms

! Ping to vyos-02 on IPv4 and IPv6
vyos@vyos-01:~$ ping 10.100.205.253
PING 10.100.205.253 (10.100.205.253) 56(84) bytes of data.
64 bytes from 10.100.205.253: icmp_seq=1 ttl=64 time=0.557 ms
64 bytes from 10.100.205.253: icmp_seq=2 ttl=64 time=0.955 ms
^C
--- 10.100.205.253 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 20ms
rtt min/avg/max/mdev = 0.557/0.756/0.955/0.199 ms
vyos@vyos-01:~$ ping 2001:db8:205::f
PING 2001:db8:205::f(2001:db8:205::f) 56 data bytes
64 bytes from 2001:db8:205::f: icmp_seq=1 ttl=64 time=0.845 ms
64 bytes from 2001:db8:205::f: icmp_seq=2 ttl=64 time=1.05 ms
64 bytes from 2001:db8:205::f: icmp_seq=3 ttl=64 time=0.988 ms
^C
--- 2001:db8:205::f ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 5ms
rtt min/avg/max/mdev = 0.845/0.962/1.053/0.086 ms
</code></pre></div><p><strong>vyos-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show IPs (IPv4 and IPv6), interface status and descriptions
vyos@vyos-02:~$ show interfaces
Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down
Interface        IP Address                        S/L  Description
---------        ----------                        ---  -----------
eth0             -                                 u/u
eth1             10.15.30.64/24                    u/u  Management
                 10.15.30.34/24
eth2             -                                 u/u  VLAN Bridge
eth2.205         10.100.205.253/24                 u/u  To vyos-01
                 2001:db8:205::f/64
lo               127.0.0.1/8                       u/u  Loopback
                 192.0.2.205/32
                 2001:db8:905:beef::2/128
                 ::1/128

! Ping to vyos-01 on IPv4 and IPv6
vyos@vyos-02:~$ ping 10.100.205.254
PING 10.100.205.254 (10.100.205.254) 56(84) bytes of data.
64 bytes from 10.100.205.254: icmp_seq=1 ttl=64 time=0.545 ms
64 bytes from 10.100.205.254: icmp_seq=2 ttl=64 time=1.05 ms
64 bytes from 10.100.205.254: icmp_seq=3 ttl=64 time=0.880 ms
^C
--- 10.100.205.254 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 5ms
rtt min/avg/max/mdev = 0.545/0.826/1.054/0.212 ms
vyos@vyos-02:~$ ping 2001:db8:205::a
PING 2001:db8:205::a(2001:db8:205::a) 56 data bytes
64 bytes from 2001:db8:205::a: icmp_seq=1 ttl=64 time=0.570 ms
64 bytes from 2001:db8:205::a: icmp_seq=2 ttl=64 time=0.850 ms
^C
--- 2001:db8:205::a ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 6ms
rtt min/avg/max/mdev = 0.570/0.710/0.850/0.140 ms
</code></pre></div><p>Looking good so far!</p>
<h3 id="firewall">Firewall</h3>
<p>Like JunOS, VyOS can be used as a zone-based firewall. It can also work in stateless or stateful modes, meaning you either need to match traffic in both directions (i.e. any reply traffic needs to be matched by a separate rule, stateless), or you can define rules only in one direction (stateful).</p>
<h4 id="playbook-2">Playbook</h4>
<p>The contents of the playbook are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e"># tasks file for firewall</span>
<span style="color:#75715e">#</span>
- <span style="color:#66d9ef">name</span>: Allow all local ICMP
  <span style="color:#66d9ef">vyos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - set firewall all-ping enable
  <span style="color:#66d9ef">tags</span>:
    - firewall

- <span style="color:#66d9ef">name</span>: Allow stateful traffic
  <span style="color:#66d9ef">vyos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - set firewall state-policy established action accept
      - set firewall state-policy related action accept
  <span style="color:#66d9ef">tags</span>:
    - firewall

- <span style="color:#66d9ef">name</span>: Define Addresses
  <span style="color:#66d9ef">vyos_config</span>:
    <span style="color:#66d9ef">src</span>: addressbook.j2
  <span style="color:#66d9ef">when</span>:
    - fw_addresses is defined
  <span style="color:#66d9ef">tags</span>:
    - firewall

- <span style="color:#66d9ef">name</span>: Define zones
  <span style="color:#66d9ef">vyos_config</span>:
    <span style="color:#66d9ef">src</span>: zones.j2
  <span style="color:#66d9ef">when</span>:
    - zones is defined
  <span style="color:#66d9ef">tags</span>:
    - firewall

- <span style="color:#66d9ef">name</span>: Define Zone Policies - IPv4
  <span style="color:#66d9ef">vyos_config</span>:
    <span style="color:#66d9ef">src</span>: policy_v4.j2
  <span style="color:#66d9ef">when</span>:
    - fw_policies is defined
    - fw_policies.ipv4 is defined
  <span style="color:#66d9ef">tags</span>:
    - firewall

- <span style="color:#66d9ef">name</span>: Define Zone Policies - IPv6
  <span style="color:#66d9ef">vyos_config</span>:
    <span style="color:#66d9ef">src</span>: policy_v6.j2
  <span style="color:#66d9ef">when</span>:
    - fw_policies is defined
    - fw_policies.ipv6 is defined
  <span style="color:#66d9ef">tags</span>:
    - firewall
</code></pre></div><p>There are no Ansible modules for VyOS firewalling, so we use the <code>vyos_config</code> module for all of the configuration.</p>
<h5 id="allowing-icmp">Allowing ICMP</h5>
<p><strong>Ansible module</strong>: <code>vyos_config</code></p>
<p>This task allows all local ping traffic. Without this, any pings to addresses configured on the router will be dropped.</p>
<h5 id="allow-stateful-traffic">Allow stateful traffic</h5>
<p><strong>Ansible module</strong>: <code>vyos_config</code></p>
<p>This task allows VyOS to work in stateful mode across all firewall policies, rather than needing to apply it on a per-zone/policy basis. You could also set all stateful traffic to be rejected by default, and then allow it on a per zone/policy basis.</p>
<h5 id="define-addresses">Define addresses</h5>
<p><strong>Ansible module</strong>: <code>vyos_config</code></p>
<p>This task defines address groups. Address groups are a way of grouping together multiple hosts so that we can apply the same firewall rules and policies to them. Unlike JunOS, address groups can only be defined globally.</p>
<p>The template used is below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% for address in fw_addresses %}
{% if address['groups'] is defined %}
{% for group in address['groups'] %}
{% if address['ip'] is defined %}
set firewall group address-group {{ group }} address {{ address['ip'] }}
{% endif %}
{% if address['ipv6'] is defined %}
set firewall group ipv6-address-group {{ group }} address {{ address['ipv6'] }}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
</code></pre><p>IPv4 and IPv6 address groups use different syntax, so we build them separately. The relevant <code>host_vars</code> are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">fw_addresses</span>:
  - <span style="color:#66d9ef">name</span>: netsvr
    <span style="color:#66d9ef">ip</span>: <span style="color:#ae81ff">10.100.105.254</span>
    <span style="color:#66d9ef">groups</span>:
      - external-bgp-peers-v4
      - netsvr-direct-v4
  - <span style="color:#66d9ef">name</span>: netsvr-v6
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:105::ffff&#34;</span>
    <span style="color:#66d9ef">groups</span>:
      - external-bgp-peers-v6
      - netsvr-direct-v6
  - <span style="color:#66d9ef">name</span>: netsvr-lo
    <span style="color:#66d9ef">ip</span>: <span style="color:#ae81ff">192.0.2.1</span>
    <span style="color:#66d9ef">groups</span>:
    - netsvr-loop-v4
  - <span style="color:#66d9ef">zone</span>: edge
    <span style="color:#66d9ef">name</span>: netsvr-lo-v6
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:999:beef::1&#34;</span>
    <span style="color:#66d9ef">groups</span>:
    - netsvr-loop-v6
  - <span style="color:#66d9ef">name</span>: internal-rtr
    <span style="color:#66d9ef">ip</span>: <span style="color:#ae81ff">192.0.2.205</span>
    <span style="color:#66d9ef">groups</span>:
    - internal-bgp-peers-v4
    - internal-rtr-loop-v4
  - <span style="color:#66d9ef">name</span>: internal-rtr-v6
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:905:beef::2&#34;</span>
    <span style="color:#66d9ef">groups</span>:
    - internal-bgp-peers-v6
    - internal-rtr-loop-v6
</code></pre></div><p>This would then generate the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set firewall group address-group external-bgp-peers-v4 address &#39;10.100.105.254&#39;
set firewall group address-group internal-bgp-peers-v4 address &#39;192.0.2.205&#39;
set firewall group address-group internal-rtr-loop-v4 address &#39;192.0.2.205&#39;
set firewall group address-group netsvr-direct-v4 address &#39;10.100.105.254&#39;
set firewall group address-group netsvr-loop-v4 address &#39;192.0.2.1&#39;
set firewall group ipv6-address-group external-bgp-peers-v6 address &#39;2001:db8:105::ffff&#39;
set firewall group ipv6-address-group internal-bgp-peers-v6 address &#39;2001:db8:905:beef::2&#39;
set firewall group ipv6-address-group internal-rtr-loop-v6 address &#39;2001:db8:905:beef::2&#39;
set firewall group ipv6-address-group netsvr-direct-v6 address &#39;2001:db8:105::ffff&#39;
set firewall group ipv6-address-group netsvr-loop-v6 address &#39;2001:db8:999:beef::1&#39;
</code></pre></div><h5 id="define-zones">Define zones</h5>
<p><strong>Ansible module</strong>: <code>vyos_config</code></p>
<p>In this section, we define the zones that our interfaces are placed in. We also define our management configuration. This is because if we create the zones and apply them without any rules, traffic to that zone is dropped by default. It would cause Ansible to lose connectivity during configuration, and also drop any SSH access to be able to rectify the issue.</p>
<p>The template used is below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% for zone in zones %}
set zone-policy zone {{ zone['name'] }} description &quot;{{ zone['description'] }}&quot;
{% if zone['local'] is defined %}
set zone-policy zone {{ zone['name'] }} local-zone
{% endif %}
{% endfor %}
{% for interface in interfaces %}
{% if interface['zone'] is defined %}
{% if interface['vif'] is defined %}
set zone-policy zone {{ interface['zone'] }} interface {{ interface['vyos_if'] }}.{{ interface['vif'] }}
{% else %}
set zone-policy zone {{ interface['zone'] }} interface {{ interface['vyos_if'] }}
{% endif %}
{% endif %}
{% endfor %}
{% if fw_policies['mgmt'] is defined %}
{% for policy in fw_policies['mgmt']['ipv4'] %}
{% for rule in policy['rules'] %}
{% if policy['source_group'] is defined %}
set firewall name {{ policy['name'] }} rule {{ loop.index }} source group address-group {{ rule['source_group'] }}
{% endif %}
{% if rule['dest_group'] is defined %}
set firewall name {{ policy['name'] }} rule {{ loop.index }} destination group address-group {{ rule['dest_group'] }}
{% endif %}
{% if rule['protocol'] is defined %}
set firewall name {{ policy['name'] }} rule {{ loop.index }} protocol {{ rule['protocol'] }}
{% endif %}
{% if rule['port'] is defined %}
set firewall name {{ policy['name'] }} rule {{ loop.index }} destination port {{ rule['port'] }}
{% endif %}
{% if rule['state'] is defined %}
set firewall name {{ policy['name'] }} rule {{ loop.index }} state {{ rule['state'] }}
{% endif %}
{% if rule['action'] is defined %}
set firewall name {{ policy['name'] }} rule {{ loop.index }} action {{ rule['action'] }}
{% endif %}
{% endfor %}
set zone-policy zone {{ policy['zones']['to'] }} from {{ policy['zones']['from'] }} firewall name {{ policy['name'] }}
{% endfor %}
{% endif %}
</code></pre><p>This template can be seen as two parts. Everything before <code>{% if fw_policies['mgmt'] is defined %}</code> creates zones based upon the following <code>host_vars</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">zones</span>:
  - <span style="color:#66d9ef">name</span>: external
    <span style="color:#66d9ef">description</span>: <span style="color:#e6db74">&#34;External facing interfaces&#34;</span>
  - <span style="color:#66d9ef">name</span>: internal
    <span style="color:#66d9ef">description</span>: <span style="color:#e6db74">&#34;Internal facing interfaces&#34;</span>
  - <span style="color:#66d9ef">name</span>: local
    <span style="color:#66d9ef">description</span>: <span style="color:#e6db74">&#34;Local router zone&#34;</span>
    <span style="color:#66d9ef">local</span>: <span style="color:#66d9ef">true</span>
  - <span style="color:#66d9ef">name</span>: mgmt
    <span style="color:#66d9ef">description</span>: <span style="color:#e6db74">&#34;Management zone&#34;</span>
<span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth1&#34;</span>
    <span style="color:#66d9ef">zone</span>: <span style="color:#e6db74">&#34;mgmt&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth2&#34;</span>
    <span style="color:#66d9ef">zone</span>: <span style="color:#e6db74">&#34;external&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth2&#34;</span>
    <span style="color:#66d9ef">zone</span>: <span style="color:#e6db74">&#34;internal&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth0&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;lo&#34;</span>
</code></pre></div><p>This will then generate the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set zone-policy zone external default-action &#39;drop&#39;
set zone-policy zone external description &#39;External facing interfaces&#39;
set zone-policy zone external interface &#39;eth2.105&#39;
set zone-policy zone internal default-action &#39;drop&#39;
set zone-policy zone internal description &#39;Internal facing interfaces&#39;
set zone-policy zone internal interface &#39;eth2.205&#39;
set zone-policy zone local default-action &#39;drop&#39;
set zone-policy zone local description &#39;Local router zone&#39;
set zone-policy zone local local-zone
set zone-policy zone mgmt default-action &#39;drop&#39;
set zone-policy zone mgmt description &#39;Management zone&#39;
set zone-policy zone mgmt interface &#39;eth1&#39;
</code></pre></div><p>The <code>default-action</code> for each zone can either be <code>reject</code> or <code>drop</code> (i.e. we cannot accept by default). When configuring a <code>local-zone</code>, this matches all traffic destined to/from this router. This means that the moment this zone is applied, all traffic (including SSH traffic) is dropped. This presents a problem when configuring with Ansible, as it uses SSH to configure the router.</p>
<p>To avoid this, we also apply our management policy at the same time. This uses the following <code>host_vars</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">fw_policies</span>:
  <span style="color:#66d9ef">mgmt</span>:
    <span style="color:#66d9ef">ipv4</span>:
      - <span style="color:#66d9ef">name</span>: mgmt_to_local_ipv4
        <span style="color:#66d9ef">zones</span>:
          <span style="color:#66d9ef">from</span>: mgmt
          <span style="color:#66d9ef">to</span>: local
        <span style="color:#66d9ef">rules</span>:
          - <span style="color:#66d9ef">protocol</span>: tcp
            <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">22</span>
            <span style="color:#66d9ef">action</span>: accept
          - <span style="color:#66d9ef">protocol</span>: udp
            <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">161</span>
            <span style="color:#66d9ef">action</span>: accept
</code></pre></div><p>This will then generate the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set firewall name mgmt_to_local_ipv4 default-action &#39;drop&#39;
set firewall name mgmt_to_local_ipv4 rule 1 action &#39;accept&#39;
set firewall name mgmt_to_local_ipv4 rule 1 destination port &#39;22&#39;
set firewall name mgmt_to_local_ipv4 rule 1 protocol &#39;tcp&#39;
set firewall name mgmt_to_local_ipv4 rule 2 action &#39;accept&#39;
set firewall name mgmt_to_local_ipv4 rule 2 destination port &#39;161&#39;
set firewall name mgmt_to_local_ipv4 rule 2 protocol &#39;udp&#39;
</code></pre></div><p>This is enough to allow Ansible to continue configuring the routers. It also allows us to use the management address for SNMP later in this post.</p>
<h5 id="define-zone-policies---ipv4">Define Zone Policies - IPv4</h5>
<p><strong>Ansible module</strong>: <code>vyos_config</code></p>
<p>This task configures the IPv4 firewall policies in VyOS, and applies them to zones.</p>
<p>The template used is below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% for policy in fw_policies['ipv4'] %}
{% for rule in policy['rules'] %}
{% if rule['source_groups'] is defined %}
set firewall name {{ policy['name'] }} rule {{ loop.index }} source group address-group {{ rule['source_groups'] }}
{% endif %}
{% if rule['dest_groups'] is defined %}
set firewall name {{ policy['name'] }} rule {{ loop.index }} destination group address-group {{ rule['dest_groups'] }}
{% endif %}
{% if rule['protocol'] is defined %}
set firewall name {{ policy['name'] }} rule {{ loop.index }} protocol {{ rule['protocol'] }}
{% endif %}
{% if rule['port'] is defined %}
set firewall name {{ policy['name'] }} rule {{ loop.index }} destination port {{ rule['port'] }}
{% endif %}
{% if rule['state'] is defined %}
set firewall name {{ policy['name'] }} rule {{ loop.index }} state {{ rule['state'] }} {{ rule['action'] }}
{% endif %}
{% if rule['action'] is defined %}
set firewall name {{ policy['name'] }} rule {{ loop.index }} action {{ rule['action'] }}
{% endif %}
{% endfor %}
set zone-policy zone {{ policy['zones']['to'] }} from {{ policy['zones']['from'] }} firewall name {{ policy['name'] }}
{% endfor %}
</code></pre><p>This template does the following: -</p>
<ul>
<li>It goes through the firewall policies for IPv4 and looks to see if there are
<ul>
<li>Source Groups for the policy</li>
<li>Destination Groups for the policy</li>
<li>Checks if a protocol is defined</li>
<li>Checks if a port is defined</li>
<li>Checks if we expect the policy to apply based upon a certain TCP state</li>
<li>Defines an action</li>
</ul>
</li>
<li>It also applies this zone between the zones referenced
<ul>
<li>The <code>to</code> zone is the destination of traffic</li>
<li>The <code>from</code> zone is the source of traffic</li>
</ul>
</li>
</ul>
<p>We also use <code>{{ loop.index }}</code>. This starts at <code>1</code> and increments for every time the loop is iterated.</p>
<p>The following <code>host_vars</code> are relevant to this: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">fw_policies</span>:
  <span style="color:#66d9ef">ipv4</span>:
    - <span style="color:#66d9ef">name</span>: external_to_local_ipv4
      <span style="color:#66d9ef">zones</span>:
        <span style="color:#66d9ef">from</span>: external
        <span style="color:#66d9ef">to</span>: local
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">source_groups</span>: external-bgp-peers-v4
          <span style="color:#66d9ef">protocol</span>: tcp
          <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">179</span>
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: icmp
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: all
          <span style="color:#66d9ef">action</span>: reject
    - <span style="color:#66d9ef">name</span>: internal_to_local_ipv4
      <span style="color:#66d9ef">zones</span>:
        <span style="color:#66d9ef">from</span>: internal
        <span style="color:#66d9ef">to</span>: local
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">source_groups</span>: internal-bgp-peers-v4
          <span style="color:#66d9ef">protocol</span>: tcp
          <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">179</span>
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: ospf
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: icmp
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: all
          <span style="color:#66d9ef">action</span>: reject
    - <span style="color:#66d9ef">name</span>: external_to_internal_ipv4
      <span style="color:#66d9ef">zones</span>:
        <span style="color:#66d9ef">from</span>: external
        <span style="color:#66d9ef">to</span>: internal
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">protocol</span>: icmp
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: all
          <span style="color:#66d9ef">action</span>: reject
    - <span style="color:#66d9ef">name</span>: local_to_external_ipv4
      <span style="color:#66d9ef">zones</span>:
        <span style="color:#66d9ef">from</span>: local
        <span style="color:#66d9ef">to</span>: external
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">dest_groups</span>: external-bgp-peers-v4
          <span style="color:#66d9ef">protocol</span>: tcp
          <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">179</span>
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">dest_groups</span>: netsvr-direct-v4
          <span style="color:#66d9ef">protocol</span>: udp
          <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">514</span>
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">dest_groups</span>: netsvr-loop-v4
          <span style="color:#66d9ef">protocol</span>: tcp_udp
          <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">1812-1813</span>
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: icmp
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: all
          <span style="color:#66d9ef">action</span>: reject
    - <span style="color:#66d9ef">name</span>: local_to_internal_ipv4
      <span style="color:#66d9ef">zones</span>:
        <span style="color:#66d9ef">from</span>: local
        <span style="color:#66d9ef">to</span>: internal
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">dest_groups</span>: internal-bgp-peers-v4
          <span style="color:#66d9ef">protocol</span>: tcp
          <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">179</span>
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: ospf
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: icmp
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: all
          <span style="color:#66d9ef">action</span>: reject
    - <span style="color:#66d9ef">name</span>: internal_to_external_ipv4
      <span style="color:#66d9ef">zones</span>:
        <span style="color:#66d9ef">from</span>: internal
        <span style="color:#66d9ef">to</span>: external
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">dest_groups</span>: netsvr-direct-v4
          <span style="color:#66d9ef">protocol</span>: udp
          <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">514</span>
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">dest_groups</span>: netsvr-loop-v4
          <span style="color:#66d9ef">protocol</span>: tcp_udp
          <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">1812-1813</span>
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: icmp
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: all
          <span style="color:#66d9ef">action</span>: reject
</code></pre></div><p>This generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">set firewall name external_to_internal_ipv4 rule <span style="color:#ae81ff">1</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall name external_to_internal_ipv4 rule <span style="color:#ae81ff">1</span> protocol <span style="color:#e6db74">&#39;icmp&#39;</span>
set firewall name external_to_internal_ipv4 rule <span style="color:#ae81ff">2</span> action <span style="color:#e6db74">&#39;reject&#39;</span>
set firewall name external_to_internal_ipv4 rule <span style="color:#ae81ff">2</span> protocol <span style="color:#e6db74">&#39;all&#39;</span>
set firewall name external_to_local_ipv4 rule <span style="color:#ae81ff">1</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall name external_to_local_ipv4 rule <span style="color:#ae81ff">1</span> destination port <span style="color:#e6db74">&#39;179&#39;</span>
set firewall name external_to_local_ipv4 rule <span style="color:#ae81ff">1</span> protocol <span style="color:#e6db74">&#39;tcp&#39;</span>
set firewall name external_to_local_ipv4 rule <span style="color:#ae81ff">1</span> source group address-group <span style="color:#e6db74">&#39;external-bgp-peers-v4&#39;</span>
set firewall name external_to_local_ipv4 rule <span style="color:#ae81ff">2</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall name external_to_local_ipv4 rule <span style="color:#ae81ff">2</span> protocol <span style="color:#e6db74">&#39;icmp&#39;</span>
set firewall name external_to_local_ipv4 rule <span style="color:#ae81ff">3</span> action <span style="color:#e6db74">&#39;reject&#39;</span>
set firewall name external_to_local_ipv4 rule <span style="color:#ae81ff">3</span> protocol <span style="color:#e6db74">&#39;all&#39;</span>
set firewall name internal_to_external_ipv4 rule <span style="color:#ae81ff">1</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall name internal_to_external_ipv4 rule <span style="color:#ae81ff">1</span> destination group address-group <span style="color:#e6db74">&#39;netsvr-direct-v4&#39;</span>
set firewall name internal_to_external_ipv4 rule <span style="color:#ae81ff">1</span> destination port <span style="color:#e6db74">&#39;514&#39;</span>
set firewall name internal_to_external_ipv4 rule <span style="color:#ae81ff">1</span> protocol <span style="color:#e6db74">&#39;udp&#39;</span>
set firewall name internal_to_external_ipv4 rule <span style="color:#ae81ff">2</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall name internal_to_external_ipv4 rule <span style="color:#ae81ff">2</span> destination group address-group <span style="color:#e6db74">&#39;netsvr-direct-v4&#39;</span>
set firewall name internal_to_external_ipv4 rule <span style="color:#ae81ff">2</span> destination port <span style="color:#e6db74">&#39;49&#39;</span>
set firewall name internal_to_external_ipv4 rule <span style="color:#ae81ff">2</span> protocol <span style="color:#e6db74">&#39;tcp_udp&#39;</span>
set firewall name internal_to_external_ipv4 rule <span style="color:#ae81ff">3</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall name internal_to_external_ipv4 rule <span style="color:#ae81ff">3</span> protocol <span style="color:#e6db74">&#39;icmp&#39;</span>
set firewall name internal_to_external_ipv4 rule <span style="color:#ae81ff">4</span> action <span style="color:#e6db74">&#39;reject&#39;</span>
set firewall name internal_to_external_ipv4 rule <span style="color:#ae81ff">4</span> protocol <span style="color:#e6db74">&#39;all&#39;</span>
set firewall name internal_to_local_ipv4 rule <span style="color:#ae81ff">1</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall name internal_to_local_ipv4 rule <span style="color:#ae81ff">1</span> destination port <span style="color:#e6db74">&#39;179&#39;</span>
set firewall name internal_to_local_ipv4 rule <span style="color:#ae81ff">1</span> protocol <span style="color:#e6db74">&#39;tcp&#39;</span>
set firewall name internal_to_local_ipv4 rule <span style="color:#ae81ff">1</span> source group address-group <span style="color:#e6db74">&#39;internal-bgp-peers-v4&#39;</span>
set firewall name internal_to_local_ipv4 rule <span style="color:#ae81ff">2</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall name internal_to_local_ipv4 rule <span style="color:#ae81ff">2</span> protocol <span style="color:#e6db74">&#39;ospf&#39;</span>
set firewall name internal_to_local_ipv4 rule <span style="color:#ae81ff">3</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall name internal_to_local_ipv4 rule <span style="color:#ae81ff">3</span> protocol <span style="color:#e6db74">&#39;icmp&#39;</span>
set firewall name internal_to_local_ipv4 rule <span style="color:#ae81ff">4</span> action <span style="color:#e6db74">&#39;reject&#39;</span>
set firewall name internal_to_local_ipv4 rule <span style="color:#ae81ff">4</span> protocol <span style="color:#e6db74">&#39;all&#39;</span>
set firewall name local_to_external_ipv4 rule <span style="color:#ae81ff">1</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall name local_to_external_ipv4 rule <span style="color:#ae81ff">1</span> destination group address-group <span style="color:#e6db74">&#39;external-bgp-peers-v4&#39;</span>
set firewall name local_to_external_ipv4 rule <span style="color:#ae81ff">1</span> destination port <span style="color:#e6db74">&#39;179&#39;</span>
set firewall name local_to_external_ipv4 rule <span style="color:#ae81ff">1</span> protocol <span style="color:#e6db74">&#39;tcp&#39;</span>
set firewall name local_to_external_ipv4 rule <span style="color:#ae81ff">2</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall name local_to_external_ipv4 rule <span style="color:#ae81ff">2</span> destination group address-group <span style="color:#e6db74">&#39;netsvr-direct-v4&#39;</span>
set firewall name local_to_external_ipv4 rule <span style="color:#ae81ff">2</span> destination port <span style="color:#e6db74">&#39;514&#39;</span>
set firewall name local_to_external_ipv4 rule <span style="color:#ae81ff">2</span> protocol <span style="color:#e6db74">&#39;udp&#39;</span>
set firewall name local_to_external_ipv4 rule <span style="color:#ae81ff">3</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall name local_to_external_ipv4 rule <span style="color:#ae81ff">3</span> destination group address-group <span style="color:#e6db74">&#39;netsvr-direct-v4&#39;</span>
set firewall name local_to_external_ipv4 rule <span style="color:#ae81ff">3</span> destination port <span style="color:#e6db74">&#39;49&#39;</span>
set firewall name local_to_external_ipv4 rule <span style="color:#ae81ff">3</span> protocol <span style="color:#e6db74">&#39;tcp_udp&#39;</span>
set firewall name local_to_external_ipv4 rule <span style="color:#ae81ff">4</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall name local_to_external_ipv4 rule <span style="color:#ae81ff">4</span> protocol <span style="color:#e6db74">&#39;icmp&#39;</span>
set firewall name local_to_external_ipv4 rule <span style="color:#ae81ff">5</span> action <span style="color:#e6db74">&#39;reject&#39;</span>
set firewall name local_to_external_ipv4 rule <span style="color:#ae81ff">5</span> protocol <span style="color:#e6db74">&#39;all&#39;</span>
set firewall name local_to_internal_ipv4 rule <span style="color:#ae81ff">1</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall name local_to_internal_ipv4 rule <span style="color:#ae81ff">1</span> destination group address-group <span style="color:#e6db74">&#39;internal-bgp-peers-v4&#39;</span>
set firewall name local_to_internal_ipv4 rule <span style="color:#ae81ff">1</span> destination port <span style="color:#e6db74">&#39;179&#39;</span>
set firewall name local_to_internal_ipv4 rule <span style="color:#ae81ff">1</span> protocol <span style="color:#e6db74">&#39;tcp&#39;</span>
set firewall name local_to_internal_ipv4 rule <span style="color:#ae81ff">2</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall name local_to_internal_ipv4 rule <span style="color:#ae81ff">2</span> protocol <span style="color:#e6db74">&#39;ospf&#39;</span>
set firewall name local_to_internal_ipv4 rule <span style="color:#ae81ff">3</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall name local_to_internal_ipv4 rule <span style="color:#ae81ff">3</span> protocol <span style="color:#e6db74">&#39;icmp&#39;</span>
set firewall name local_to_internal_ipv4 rule <span style="color:#ae81ff">4</span> action <span style="color:#e6db74">&#39;reject&#39;</span>
set firewall name local_to_internal_ipv4 rule <span style="color:#ae81ff">4</span> protocol <span style="color:#e6db74">&#39;all&#39;</span>
set zone-policy zone external from internal firewall name <span style="color:#e6db74">&#39;internal_to_external_ipv4&#39;</span>
set zone-policy zone external from local firewall name <span style="color:#e6db74">&#39;local_to_external_ipv4&#39;</span>
set zone-policy zone internal from external firewall name <span style="color:#e6db74">&#39;external_to_internal_ipv4&#39;</span>
set zone-policy zone internal from local firewall name <span style="color:#e6db74">&#39;local_to_internal_ipv4&#39;</span>
set zone-policy zone local from external firewall name <span style="color:#e6db74">&#39;external_to_local_ipv4&#39;</span>
set zone-policy zone local from internal firewall name <span style="color:#e6db74">&#39;internal_to_local_ipv4&#39;</span>
</code></pre></div><h5 id="define-zone-policies---ipv6">Define Zone Policies - IPv6</h5>
<p><strong>Ansible module</strong>: <code>vyos_config</code></p>
<p>This task is identical to the previous, except that it applies to IPv6.</p>
<p>The template used is below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% for policy in fw_policies['ipv6'] %}
{% for rule in policy['rules'] %}
{% if rule['source_groups'] is defined %}
set firewall ipv6-name {{ policy['name'] }} rule {{ loop.index }} source group address-group {{ rule['source_groups'] }}
{% endif %}
{% if rule['dest_groups'] is defined %}
set firewall ipv6-name {{ policy['name'] }} rule {{ loop.index }} destination group address-group {{ rule['dest_groups'] }}
{% endif %}
{% if rule['protocol'] is defined %}
set firewall ipv6-name {{ policy['name'] }} rule {{ loop.index }} protocol {{ rule['protocol'] }}
{% endif %}
{% if rule['port'] is defined %}
set firewall ipv6-name {{ policy['name'] }} rule {{ loop.index }} destination port {{ rule['port'] }}
{% endif %}
{% if rule['state'] is defined %}
set firewall ipv6-name {{ policy['name'] }} rule {{ loop.index }} state {{ rule['state'] }} {{ rule['action'] }}
{% endif %}
{% if rule['action'] is defined %}
set firewall ipv6-name {{ policy['name'] }} rule {{ loop.index }} action {{ rule['action'] }}
{% endif %}
{% endfor %}
set zone-policy zone {{ policy['zones']['to'] }} from {{ policy['zones']['from'] }} firewall ipv6-name {{ policy['name'] }}
{% endfor %}
</code></pre><p>This is the same as the previous template, except it uses <code>ipv6-name</code> in place of <code>name</code>.</p>
<p>The following <code>host_vars</code> are relevant to this: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">fw_policies</span>:
  <span style="color:#66d9ef">ipv6</span>:
    - <span style="color:#66d9ef">name</span>: external_to_local_ipv6
      <span style="color:#66d9ef">zones</span>:
        <span style="color:#66d9ef">from</span>: external
        <span style="color:#66d9ef">to</span>: local
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">source_groups</span>: external-bgp-peers-v6
          <span style="color:#66d9ef">protocol</span>: tcp
          <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">179</span>
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: icmpv6
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: all
          <span style="color:#66d9ef">action</span>: reject
    - <span style="color:#66d9ef">name</span>: internal_to_local_ipv6
      <span style="color:#66d9ef">zones</span>:
        <span style="color:#66d9ef">from</span>: internal
        <span style="color:#66d9ef">to</span>: local
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">source_groups</span>: internal-bgp-peers-v6
          <span style="color:#66d9ef">protocol</span>: tcp
          <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">179</span>
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: ospf
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: icmpv6
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: all
          <span style="color:#66d9ef">action</span>: reject
    - <span style="color:#66d9ef">name</span>: external_to_internal_ipv6
      <span style="color:#66d9ef">zones</span>:
        <span style="color:#66d9ef">from</span>: external
        <span style="color:#66d9ef">to</span>: internal
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">protocol</span>: icmpv6
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: all
          <span style="color:#66d9ef">action</span>: reject
    - <span style="color:#66d9ef">name</span>: local_to_external_ipv6
      <span style="color:#66d9ef">zones</span>:
        <span style="color:#66d9ef">from</span>: local
        <span style="color:#66d9ef">to</span>: external
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">dest_groups</span>: external-bgp-peers-v6
          <span style="color:#66d9ef">protocol</span>: tcp
          <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">179</span>
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: ospf
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: icmpv6
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: all
          <span style="color:#66d9ef">action</span>: reject
    - <span style="color:#66d9ef">name</span>: local_to_internal_ipv6
      <span style="color:#66d9ef">zones</span>:
        <span style="color:#66d9ef">from</span>: local
        <span style="color:#66d9ef">to</span>: internal
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">dest_groups</span>: internal-bgp-peers-v6
          <span style="color:#66d9ef">protocol</span>: tcp
          <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">179</span>
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: ospf
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: icmpv6
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: all
          <span style="color:#66d9ef">action</span>: reject
    - <span style="color:#66d9ef">name</span>: internal_to_external_ipv6
      <span style="color:#66d9ef">zones</span>:
        <span style="color:#66d9ef">from</span>: internal
        <span style="color:#66d9ef">to</span>: external
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">protocol</span>: icmpv6
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: all
          <span style="color:#66d9ef">action</span>: reject
</code></pre></div><p>This generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">set firewall ipv6-name external_to_internal_ipv6 default-action <span style="color:#e6db74">&#39;drop&#39;</span>
set firewall ipv6-name external_to_internal_ipv6 rule <span style="color:#ae81ff">1</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall ipv6-name external_to_internal_ipv6 rule <span style="color:#ae81ff">1</span> protocol <span style="color:#e6db74">&#39;icmpv6&#39;</span>
set firewall ipv6-name external_to_internal_ipv6 rule <span style="color:#ae81ff">2</span> action <span style="color:#e6db74">&#39;reject&#39;</span>
set firewall ipv6-name external_to_internal_ipv6 rule <span style="color:#ae81ff">2</span> protocol <span style="color:#e6db74">&#39;all&#39;</span>
set firewall ipv6-name external_to_local_ipv6 rule <span style="color:#ae81ff">1</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall ipv6-name external_to_local_ipv6 rule <span style="color:#ae81ff">1</span> destination port <span style="color:#e6db74">&#39;179&#39;</span>
set firewall ipv6-name external_to_local_ipv6 rule <span style="color:#ae81ff">1</span> protocol <span style="color:#e6db74">&#39;tcp&#39;</span>
set firewall ipv6-name external_to_local_ipv6 rule <span style="color:#ae81ff">1</span> source group address-group <span style="color:#e6db74">&#39;external-bgp-peers-v6&#39;</span>
set firewall ipv6-name external_to_local_ipv6 rule <span style="color:#ae81ff">2</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall ipv6-name external_to_local_ipv6 rule <span style="color:#ae81ff">2</span> protocol <span style="color:#e6db74">&#39;icmpv6&#39;</span>
set firewall ipv6-name external_to_local_ipv6 rule <span style="color:#ae81ff">3</span> action <span style="color:#e6db74">&#39;reject&#39;</span>
set firewall ipv6-name external_to_local_ipv6 rule <span style="color:#ae81ff">3</span> protocol <span style="color:#e6db74">&#39;all&#39;</span>
set firewall ipv6-name internal_to_external_ipv6 rule <span style="color:#ae81ff">1</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall ipv6-name internal_to_external_ipv6 rule <span style="color:#ae81ff">1</span> protocol <span style="color:#e6db74">&#39;icmpv6&#39;</span>
set firewall ipv6-name internal_to_external_ipv6 rule <span style="color:#ae81ff">2</span> action <span style="color:#e6db74">&#39;reject&#39;</span>
set firewall ipv6-name internal_to_external_ipv6 rule <span style="color:#ae81ff">2</span> protocol <span style="color:#e6db74">&#39;all&#39;</span>
set firewall ipv6-name internal_to_local_ipv6 rule <span style="color:#ae81ff">1</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall ipv6-name internal_to_local_ipv6 rule <span style="color:#ae81ff">1</span> destination port <span style="color:#e6db74">&#39;179&#39;</span>
set firewall ipv6-name internal_to_local_ipv6 rule <span style="color:#ae81ff">1</span> protocol <span style="color:#e6db74">&#39;tcp&#39;</span>
set firewall ipv6-name internal_to_local_ipv6 rule <span style="color:#ae81ff">1</span> source group address-group <span style="color:#e6db74">&#39;internal-bgp-peers-v6&#39;</span>
set firewall ipv6-name internal_to_local_ipv6 rule <span style="color:#ae81ff">2</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall ipv6-name internal_to_local_ipv6 rule <span style="color:#ae81ff">2</span> protocol <span style="color:#e6db74">&#39;ospf&#39;</span>
set firewall ipv6-name internal_to_local_ipv6 rule <span style="color:#ae81ff">3</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall ipv6-name internal_to_local_ipv6 rule <span style="color:#ae81ff">3</span> protocol <span style="color:#e6db74">&#39;icmpv6&#39;</span>
set firewall ipv6-name internal_to_local_ipv6 rule <span style="color:#ae81ff">4</span> action <span style="color:#e6db74">&#39;reject&#39;</span>
set firewall ipv6-name internal_to_local_ipv6 rule <span style="color:#ae81ff">4</span> protocol <span style="color:#e6db74">&#39;all&#39;</span>
set firewall ipv6-name local_to_external_ipv6 rule <span style="color:#ae81ff">1</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall ipv6-name local_to_external_ipv6 rule <span style="color:#ae81ff">1</span> destination group address-group <span style="color:#e6db74">&#39;external-bgp-peers-v6&#39;</span>
set firewall ipv6-name local_to_external_ipv6 rule <span style="color:#ae81ff">1</span> destination port <span style="color:#e6db74">&#39;179&#39;</span>
set firewall ipv6-name local_to_external_ipv6 rule <span style="color:#ae81ff">1</span> protocol <span style="color:#e6db74">&#39;tcp&#39;</span>
set firewall ipv6-name local_to_external_ipv6 rule <span style="color:#ae81ff">2</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall ipv6-name local_to_external_ipv6 rule <span style="color:#ae81ff">2</span> protocol <span style="color:#e6db74">&#39;ospf&#39;</span>
set firewall ipv6-name local_to_external_ipv6 rule <span style="color:#ae81ff">3</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall ipv6-name local_to_external_ipv6 rule <span style="color:#ae81ff">3</span> protocol <span style="color:#e6db74">&#39;icmpv6&#39;</span>
set firewall ipv6-name local_to_external_ipv6 rule <span style="color:#ae81ff">4</span> action <span style="color:#e6db74">&#39;reject&#39;</span>
set firewall ipv6-name local_to_external_ipv6 rule <span style="color:#ae81ff">4</span> protocol <span style="color:#e6db74">&#39;all&#39;</span>
set firewall ipv6-name local_to_internal_ipv6 rule <span style="color:#ae81ff">1</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall ipv6-name local_to_internal_ipv6 rule <span style="color:#ae81ff">1</span> destination group address-group <span style="color:#e6db74">&#39;internal-bgp-peers-v6&#39;</span>
set firewall ipv6-name local_to_internal_ipv6 rule <span style="color:#ae81ff">1</span> destination port <span style="color:#e6db74">&#39;179&#39;</span>
set firewall ipv6-name local_to_internal_ipv6 rule <span style="color:#ae81ff">1</span> protocol <span style="color:#e6db74">&#39;tcp&#39;</span>
set firewall ipv6-name local_to_internal_ipv6 rule <span style="color:#ae81ff">2</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall ipv6-name local_to_internal_ipv6 rule <span style="color:#ae81ff">2</span> protocol <span style="color:#e6db74">&#39;ospf&#39;</span>
set firewall ipv6-name local_to_internal_ipv6 rule <span style="color:#ae81ff">3</span> action <span style="color:#e6db74">&#39;accept&#39;</span>
set firewall ipv6-name local_to_internal_ipv6 rule <span style="color:#ae81ff">3</span> protocol <span style="color:#e6db74">&#39;icmpv6&#39;</span>
set firewall ipv6-name local_to_internal_ipv6 rule <span style="color:#ae81ff">4</span> action <span style="color:#e6db74">&#39;reject&#39;</span>
set firewall ipv6-name local_to_internal_ipv6 rule <span style="color:#ae81ff">4</span> protocol <span style="color:#e6db74">&#39;all&#39;</span>
set zone-policy zone external from internal firewall ipv6-name <span style="color:#e6db74">&#39;internal_to_external_ipv6&#39;</span>
set zone-policy zone external from local firewall ipv6-name <span style="color:#e6db74">&#39;local_to_external_ipv6&#39;</span>
set zone-policy zone internal from external firewall ipv6-name <span style="color:#e6db74">&#39;external_to_internal_ipv6&#39;</span>
set zone-policy zone internal from local firewall ipv6-name <span style="color:#e6db74">&#39;local_to_internal_ipv6&#39;</span>
set zone-policy zone local from external firewall ipv6-name <span style="color:#e6db74">&#39;external_to_local_ipv6&#39;</span>
set zone-policy zone local from internal firewall ipv6-name <span style="color:#e6db74">&#39;internal_to_local_ipv6&#39;</span>
</code></pre></div><h4 id="verification-2">Verification</h4>
<p>Now we can verify whether the firewall rules work: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">vyos@vyos-01:~$ show firewall statistics

------------------------
Firewall Global Settings
------------------------

Firewall state-policy for all IPv4 and Ipv6 traffic

state           action   log
-----           ------   ---
established     accept   disabled
related         accept   disabled

-----------------------------
Rulesets Information
-----------------------------
--------------------------------------------------------------------------------
IPv4 Firewall &#34;external_to_internal_ipv4&#34;:

 Active on traffic to -
  zone [internal] from zone [external]

rule  packets   bytes     action  source              destination
----  -------   -----     ------  ------              -----------
1     0         0         ACCEPT  0.0.0.0/0           0.0.0.0/0
2     0         0         REJECT  0.0.0.0/0           0.0.0.0/0
10000 0         0         DROP    0.0.0.0/0           0.0.0.0/0

--------------------------------------------------------------------------------
IPv4 Firewall &#34;external_to_local_ipv4&#34;:

 Active on traffic to -
  zone [local] from zone [external]

rule  packets   bytes     action  source              destination
----  -------   -----     ------  ------              -----------
1     0         0         ACCEPT  0.0.0.0/0           0.0.0.0/0
2     0         0         ACCEPT  0.0.0.0/0           0.0.0.0/0
3     6         1.90K     REJECT  0.0.0.0/0           0.0.0.0/0
10000 0         0         DROP    0.0.0.0/0           0.0.0.0/0

--------------------------------------------------------------------------------
IPv4 Firewall &#34;internal_to_external_ipv4&#34;:

 Active on traffic to -
  zone [external] from zone [internal]

rule  packets   bytes     action  source              destination
----  -------   -----     ------  ------              -----------
1     0         0         ACCEPT  0.0.0.0/0           0.0.0.0/0
2     0         0         ACCEPT  0.0.0.0/0           0.0.0.0/0
3     0         0         ACCEPT  0.0.0.0/0           0.0.0.0/0
4     0         0         REJECT  0.0.0.0/0           0.0.0.0/0
10000 0         0         DROP    0.0.0.0/0           0.0.0.0/0

--------------------------------------------------------------------------------
IPv4 Firewall &#34;internal_to_local_ipv4&#34;:

 Active on traffic to -
  zone [local] from zone [internal]

rule  packets   bytes     action  source              destination
----  -------   -----     ------  ------              -----------
1     1         60        ACCEPT  0.0.0.0/0           0.0.0.0/0
2     16        1.12K     ACCEPT  0.0.0.0/0           0.0.0.0/0
3     8         672       ACCEPT  0.0.0.0/0           0.0.0.0/0
4     0         0         REJECT  0.0.0.0/0           0.0.0.0/0
10000 0         0         DROP    0.0.0.0/0           0.0.0.0/0

--------------------------------------------------------------------------------
IPv4 Firewall &#34;local_to_external_ipv4&#34;:

 Active on traffic to -
  zone [external] from zone [local]

rule  packets   bytes     action  source              destination
----  -------   -----     ------  ------              -----------
1     1         60        ACCEPT  0.0.0.0/0           0.0.0.0/0
2     166       20.52K    ACCEPT  0.0.0.0/0           0.0.0.0/0
3     0         0         ACCEPT  0.0.0.0/0           0.0.0.0/0
4     0         0         ACCEPT  0.0.0.0/0           0.0.0.0/0
5     23        2.09K     REJECT  0.0.0.0/0           0.0.0.0/0
10000 0         0         DROP    0.0.0.0/0           0.0.0.0/0

--------------------------------------------------------------------------------
IPv4 Firewall &#34;local_to_internal_ipv4&#34;:

 Active on traffic to -
  zone [internal] from zone [local]

rule  packets   bytes     action  source              destination
----  -------   -----     ------  ------              -----------
1     1         60        ACCEPT  0.0.0.0/0           0.0.0.0/0
2     14        1.04K     ACCEPT  0.0.0.0/0           0.0.0.0/0
3     9         756       ACCEPT  0.0.0.0/0           0.0.0.0/0
4     4         160       REJECT  0.0.0.0/0           0.0.0.0/0
10000 0         0         DROP    0.0.0.0/0           0.0.0.0/0

--------------------------------------------------------------------------------
IPv4 Firewall &#34;mgmt_to_local_ipv4&#34;:

 Active on traffic to -
  zone [local] from zone [mgmt]

rule  packets   bytes     action  source              destination
----  -------   -----     ------  ------              -----------
1     1         64        ACCEPT  0.0.0.0/0           0.0.0.0/0
10000 0         0         DROP    0.0.0.0/0           0.0.0.0/0

--------------------------------------------------------------------------------
IPv6 Firewall &#34;external_to_internal_ipv6&#34;:

 Active on traffic to -
  zone [internal] from zone [external]

rule  packets   bytes     action  source              destination
----  -------   -----     ------  ------              -----------
1     0         0         ACCEPT  ::/0                ::/0
2     0         0         REJECT  ::/0                ::/0
10000 0         0         DROP    ::/0                ::/0

--------------------------------------------------------------------------------
IPv6 Firewall &#34;external_to_local_ipv6&#34;:

 Active on traffic to -
  zone [local] from zone [external]

rule  packets   bytes     action  source              destination
----  -------   -----     ------  ------              -----------
1     0         0         ACCEPT  ::/0                ::/0
2     8         488       ACCEPT  ::/0                ::/0
3     0         0         REJECT  ::/0                ::/0
10000 0         0         DROP    ::/0                ::/0

--------------------------------------------------------------------------------
IPv6 Firewall &#34;internal_to_external_ipv6&#34;:

 Active on traffic to -
  zone [external] from zone [internal]

rule  packets   bytes     action  source              destination
----  -------   -----     ------  ------              -----------
1     0         0         ACCEPT  ::/0                ::/0
2     0         0         REJECT  ::/0                ::/0
10000 0         0         DROP    ::/0                ::/0

--------------------------------------------------------------------------------
IPv6 Firewall &#34;internal_to_local_ipv6&#34;:

 Active on traffic to -
  zone [local] from zone [internal]

rule  packets   bytes     action  source              destination
----  -------   -----     ------  ------              -----------
1     1         80        ACCEPT  ::/0                ::/0
2     16        1.57K     ACCEPT  ::/0                ::/0
3     2         136       ACCEPT  ::/0                ::/0
4     0         0         REJECT  ::/0                ::/0
10000 0         0         DROP    ::/0                ::/0

--------------------------------------------------------------------------------
IPv6 Firewall &#34;local_to_external_ipv6&#34;:

 Active on traffic to -
  zone [external] from zone [local]

rule  packets   bytes     action  source              destination
----  -------   -----     ------  ------              -----------
1     1         80        ACCEPT  ::/0                ::/0
2     0         0         ACCEPT  ::/0                ::/0
3     17        1.45K     ACCEPT  ::/0                ::/0
4     0         0         REJECT  ::/0                ::/0
10000 0         0         DROP    ::/0                ::/0

--------------------------------------------------------------------------------
IPv6 Firewall &#34;local_to_internal_ipv6&#34;:

 Active on traffic to -
  zone [internal] from zone [local]

rule  packets   bytes     action  source              destination
----  -------   -----     ------  ------              -----------
1     1         80        ACCEPT  ::/0                ::/0
2     14        1.43K     ACCEPT  ::/0                ::/0
3     14        1.25K     ACCEPT  ::/0                ::/0
4     0         0         REJECT  ::/0                ::/0
10000 0         0         DROP    ::/0                ::/0

! What happens if we try to SSH from the netsvr?

[stuh84@netsvr-01 ~] $ ssh vyos@10.100.105.253
ssh: connect to host 10.100.105.253 port 22: Connection refused

! Can we still ping it?
[stuh84@netsvr-01 ~] $ ping 10.100.105.253
PING 10.100.105.253 (10.100.105.253) 56(84) bytes of data.
64 bytes from 10.100.105.253: icmp_seq=1 ttl=64 time=0.837 ms
64 bytes from 10.100.105.253: icmp_seq=2 ttl=64 time=0.854 ms
^C
--- 10.100.105.253 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 36ms
rtt min/avg/max/mdev = 0.837/0.845/0.854/0.030 ms
</code></pre></div><h3 id="routing">Routing</h3>
<p>For routing, we use BGP for external network connectivity, OSPF for internal IPv4 routing and OSPFv3 for internal IPv6 routing. Unlike other vendors that have IPv4 support for OSPFv3, VyOS currently only support IPv6 routes and addressing in OSPFv3. This is why I have chosen to configure both in each article in this series, so that those using mixed-vendor networks can integrate them together without needing to reconfigure their core routing protocols.</p>
<h4 id="main-playbook">Main Playbook</h4>
<p>As with the others in this series, the main playbook is used to include other playbooks: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for routing</span>
<span style="color:#75715e">##</span>
- <span style="color:#66d9ef">name</span>: Include OSPF routing
  <span style="color:#66d9ef">include</span>: ospf.yml

- <span style="color:#66d9ef">name</span>: Include OSPFv3 routing
  <span style="color:#66d9ef">include</span>: ospfv3.yml

- <span style="color:#66d9ef">name</span>: Include BGP routing
  <span style="color:#66d9ef">include</span>: bgp.yml
</code></pre></div><h4 id="ospf-playbook">OSPF Playbook</h4>
<p>The contents of the OSPF Playbook are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e"># tasks file for routing</span>
<span style="color:#75715e">#</span>
- <span style="color:#66d9ef">name</span>: OSPF Process - Router ID
  <span style="color:#66d9ef">vyos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#e6db74">&#34;set protocols ospf parameters router-id  {{ router_id }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf
    - ospf_v4

- <span style="color:#66d9ef">name</span>: OSPF Interfaces
  <span style="color:#66d9ef">vyos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - set protocols ospf area {{ item.ospf.area }} network {{ item.ipv4_addr | ipaddr(<span style="color:#e6db74">&#39;network/prefix&#39;</span>) }}
  <span style="color:#66d9ef">when</span>:
    - item.ospf is defined
    - item.ipv4_addr is not search(<span style="color:#e6db74">&#34;dhcp&#34;</span>)
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf
    - ospf_v4

- <span style="color:#66d9ef">name</span>: OSPF Interfaces - Passive
  <span style="color:#66d9ef">vyos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - set protocols ospf passive-interface {{ item.vyos_if }}
  <span style="color:#66d9ef">when</span>:
    - item.ospf is defined
    - item.ospf.passive is defined
    - item.vif is not defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf
    - ospf_v4

- <span style="color:#66d9ef">name</span>: OSPF Interfaces - Passive - vif
  <span style="color:#66d9ef">vyos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - set protocols ospf passive-interface {{ item.vyos_if }}.{{ item.vif }}
  <span style="color:#66d9ef">when</span>:
    - item.ospf is defined
    - item.ospf.passive is defined
    - item.vif is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf
    - ospf_v4
</code></pre></div><p>This has some similarities to the <a href="/posts/ansible-for-networking-part-4-juniper-junos/#routing">JunOS</a> OSPF playbook, except that we must enable passive interfaces for standard interfaces and Virtual Interfaces separately.</p>
<h5 id="router-id">Router ID</h5>
<p><strong>Ansible module</strong>: <code>vyos_config</code></p>
<p>The Router ID task picks up the router_id variable from host_vars, and applies it as part of the OSPF configuration.</p>
<p>The following <code>host_vars</code> are relevant: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">router_id</span>: <span style="color:#ae81ff">192.0.2.105</span>
</code></pre></div><p>This generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">set protocols ospf parameters router-id 192.0.2.105
</code></pre></div><h5 id="ospf-interfaces">OSPF Interfaces</h5>
<p><strong>Ansible module</strong>: <code>vyos_config</code></p>
<p>This task goes through our list of interfaces, discovers the OSPF area they belong to, and uses the <code>ipaddr</code> feature within Jinja2/Ansible to discover the network address for the subnet the interface is in. For example, an interface with the IP <code>192.168.1.222/24</code>, the network address would be <code>192.168.1.0/24</code>.</p>
<p>The relevant <code>host_vars</code> for this are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth2&#34;</span>
    <span style="color:#66d9ef">vif</span>: <span style="color:#ae81ff">105</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To netsvr&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.100.105.253/24&#34;</span>
    <span style="color:#66d9ef">zone</span>: <span style="color:#e6db74">&#34;external&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth2&#34;</span>
    <span style="color:#66d9ef">vif</span>: <span style="color:#ae81ff">205</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To vyos-02&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">zone</span>: <span style="color:#e6db74">&#34;internal&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.100.205.254/24&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth0&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To the Internet&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;dhcp&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;lo&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;192.0.2.105/32&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
</code></pre></div><p>This generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">set protocols ospf area 0.0.0.0 network <span style="color:#e6db74">&#39;10.100.105.0/24&#39;</span>
set protocols ospf area 0.0.0.0 network <span style="color:#e6db74">&#39;10.100.205.0/24&#39;</span>
set protocols ospf area 0.0.0.0 network <span style="color:#e6db74">&#39;192.0.2.105/32&#39;</span>
</code></pre></div><h5 id="ospf-interfaces---passive">OSPF Interfaces - Passive</h5>
<p><strong>Ansible module</strong>: <code>vyos_config</code></p>
<p>This is similar to the above, except for any interface with the <code>passive</code> field under OSPF, we also generate the passive configuration.</p>
<p>The <code>host_vars</code> relevant to this are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth2&#34;</span>
    <span style="color:#66d9ef">vif</span>: <span style="color:#ae81ff">105</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To netsvr&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.100.105.253/24&#34;</span>
    <span style="color:#66d9ef">ipv6_addr</span>: <span style="color:#e6db74">&#34;2001:db8:105::f/64&#34;</span>
    <span style="color:#66d9ef">zone</span>: <span style="color:#e6db74">&#34;external&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth2&#34;</span>
    <span style="color:#66d9ef">vif</span>: <span style="color:#ae81ff">205</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To vyos-02&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">zone</span>: <span style="color:#e6db74">&#34;internal&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.100.205.254/24&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth0&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To the Internet&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;dhcp&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;lo&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;192.0.2.105/32&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
</code></pre></div><p>This would then generate the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">set protocols ospf passive-interface <span style="color:#e6db74">&#39;eth0&#39;</span>
set protocols ospf passive-interface <span style="color:#e6db74">&#39;lo&#39;</span>
</code></pre></div><p>This task is only for non-VIF interfaces, meaning that it only applies to <code>eth0</code> and <code>lo</code>.</p>
<h5 id="ospf-interfaces---passive-vifs">OSPF Interfaces - Passive (VIFs)</h5>
<p><strong>Ansible module</strong>: <code>vyos_config</code></p>
<p>This is almost identical to the previous task, except that it applies to VIFs.</p>
<p>The previous <code>host_vars</code> apply, and generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">set protocols ospf passive-interface <span style="color:#e6db74">&#39;eth2.105&#39;</span>
</code></pre></div><h5 id="verification-3">Verification</h5>
<p>After this, we should be able to see OSPF routes on both the <strong>edge</strong> router and the <strong>internal</strong> router: -</p>
<p><strong>vyos-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show OSPF interfaces
vyos@vyos-01:~$ show ip ospf interface
eth2.105 is up
  ifindex 6, MTU 1500 bytes, BW 1000 Mbit &lt;UP,BROADCAST,RUNNING,MULTICAST&gt;
  Internet Address 10.100.105.253/24, Broadcast 10.100.105.255, Area 0.0.0.0
  MTU mismatch detection: enabled
  Router ID 192.0.2.105, Network Type BROADCAST, Cost: 100
  Transmit Delay is 1 sec, State DR, Priority 1
  No backup designated router on this network
  Multicast group memberships: &lt;None&gt;
  Timer intervals configured, Hello 10s, Dead 40s, Wait 40s, Retransmit 5
    No Hellos (Passive interface)
  Neighbor Count is 0, Adjacent neighbor count is 0
eth2.205 is up
  ifindex 7, MTU 1500 bytes, BW 1000 Mbit &lt;UP,BROADCAST,RUNNING,MULTICAST&gt;
  Internet Address 10.100.205.254/24, Broadcast 10.100.205.255, Area 0.0.0.0
  MTU mismatch detection: enabled
  Router ID 192.0.2.105, Network Type BROADCAST, Cost: 100
  Transmit Delay is 1 sec, State Backup, Priority 1
  Backup Designated Router (ID) 192.0.2.105, Interface Address 10.100.205.254
  Multicast group memberships: OSPFAllRouters OSPFDesignatedRouters
  Timer intervals configured, Hello 10s, Dead 40s, Wait 40s, Retransmit 5
    Hello due in 3.154s
  Neighbor Count is 1, Adjacent neighbor count is 1
lo is up
  ifindex 1, MTU 65536 bytes, BW 0 Mbit &lt;UP,LOOPBACK,RUNNING&gt;
  Internet Address 192.0.2.105/32, Broadcast 192.0.2.105, Area 0.0.0.0
  MTU mismatch detection: enabled
  Router ID 192.0.2.105, Network Type LOOPBACK, Cost: 10
  Transmit Delay is 1 sec, State Loopback, Priority 1
  No backup designated router on this network
  Multicast group memberships: &lt;None&gt;
  Timer intervals configured, Hello 10s, Dead 40s, Wait 40s, Retransmit 5
    No Hellos (Passive interface)
  Neighbor Count is 0, Adjacent neighbor count is 0

! Show OSPF neighbours
vyos@vyos-01:~$ show ip ospf neighbor

Neighbor ID     Pri State           Dead Time Address         Interface                        RXmtL RqstL DBsmL
192.0.2.205       1 Full/DR           38.604s 10.100.205.253  eth2.205:10.100.205.254              0     0     0

! Show routing table
vyos@vyos-01:~$ show ip route ospf
Codes: K - kernel route, C - connected, S - static, R - RIP,
       O - OSPF, I - IS-IS, B - BGP, E - EIGRP, N - NHRP,
       T - Table, v - VNC, V - VNC-Direct, A - Babel, D - SHARP,
       F - PBR, f - OpenFabric,
       &gt; - selected route, * - FIB route, q - queued route, r - rejected route

O   10.100.105.0/24 [110/100] is directly connected, eth2.105, 00:07:39
O   10.100.205.0/24 [110/100] is directly connected, eth2.205, 00:07:38
O   192.0.2.105/32 [110/0] is directly connected, lo, 00:07:44
O&gt;* 192.0.2.205/32 [110/100] via 10.100.205.253, eth2.205, 00:06:52

! Can we ping?
vyos@vyos-01:~$ ping 192.0.2.205
PING 192.0.2.205 (192.0.2.205) 56(84) bytes of data.
64 bytes from 192.0.2.205: icmp_seq=1 ttl=64 time=0.359 ms
^C
--- 192.0.2.205 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.359/0.359/0.359/0.000 ms
</code></pre></div><p><strong>vyos-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show OSPF interfaces
vyos@vyos-02:~$ show ip ospf interface
eth2.205 is up
  ifindex 5, MTU 1500 bytes, BW 4294967295 Mbit &lt;UP,BROADCAST,RUNNING,MULTICAST&gt;
  Internet Address 10.100.205.253/24, Broadcast 10.100.205.255, Area 0.0.0.0
  MTU mismatch detection: enabled
  Router ID 192.0.2.205, Network Type BROADCAST, Cost: 1
  Transmit Delay is 1 sec, State DR, Priority 1
  Backup Designated Router (ID) 192.0.2.105, Interface Address 10.100.205.254
  Multicast group memberships: OSPFAllRouters OSPFDesignatedRouters
  Timer intervals configured, Hello 10s, Dead 40s, Wait 40s, Retransmit 5
    Hello due in 2.643s
  Neighbor Count is 1, Adjacent neighbor count is 1
lo is up
  ifindex 1, MTU 65536 bytes, BW 0 Mbit &lt;UP,LOOPBACK,RUNNING&gt;
  Internet Address 192.0.2.205/32, Broadcast 192.0.2.205, Area 0.0.0.0
  MTU mismatch detection: enabled
  Router ID 192.0.2.205, Network Type LOOPBACK, Cost: 10
  Transmit Delay is 1 sec, State Loopback, Priority 1
  No backup designated router on this network
  Multicast group memberships: &lt;None&gt;
  Timer intervals configured, Hello 10s, Dead 40s, Wait 40s, Retransmit 5
    No Hellos (Passive interface)
  Neighbor Count is 0, Adjacent neighbor count is 0

! Show OSPF neighbours
vyos@vyos-02:~$ show ip ospf neighbor

Neighbor ID     Pri State           Dead Time Address         Interface                        RXmtL RqstL DBsmL
192.0.2.105       1 Full/Backup       38.921s 10.100.205.254  eth2.205:10.100.205.253              0     0     0

! Show routing table
vyos@vyos-02:~$ show ip route ospf
Codes: K - kernel route, C - connected, S - static, R - RIP,
       O - OSPF, I - IS-IS, B - BGP, E - EIGRP, N - NHRP,
       T - Table, v - VNC, V - VNC-Direct, A - Babel, D - SHARP,
       F - PBR, f - OpenFabric,
       &gt; - selected route, * - FIB route, q - queued route, r - rejected route

O&gt;* 10.100.105.0/24 [110/101] via 10.100.205.254, eth2.205, 00:09:21
O   10.100.205.0/24 [110/1] is directly connected, eth2.205, 00:10:18
O&gt;* 192.0.2.105/32 [110/1] via 10.100.205.254, eth2.205, 00:09:21
O   192.0.2.205/32 [110/0] is directly connected, lo, 00:10:26

! Can we ping?
vyos@vyos-02:~$ ping 192.0.2.105
PING 192.0.2.105 (192.0.2.105) 56(84) bytes of data.
64 bytes from 192.0.2.105: icmp_seq=1 ttl=64 time=0.454 ms
64 bytes from 192.0.2.105: icmp_seq=2 ttl=64 time=0.798 ms
^C
--- 192.0.2.105 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 18ms
rtt min/avg/max/mdev = 0.454/0.626/0.798/0.172 ms

vyos@vyos-02:~$ ping 10.100.105.253
PING 10.100.105.253 (10.100.105.253) 56(84) bytes of data.
64 bytes from 10.100.105.253: icmp_seq=1 ttl=64 time=0.452 ms
64 bytes from 10.100.105.253: icmp_seq=2 ttl=64 time=0.831 ms
64 bytes from 10.100.105.253: icmp_seq=3 ttl=64 time=0.861 ms
^C
--- 10.100.105.253 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 14ms
rtt min/avg/max/mdev = 0.452/0.714/0.861/0.188 ms
</code></pre></div><p>All looks good!</p>
<h4 id="ospfv3-playbook">OSPFv3 Playbook</h4>
<p>As noted, we are using OSPFv3 for IPv6 internal routing.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e"># tasks file for routing</span>
<span style="color:#75715e">#</span>

- <span style="color:#66d9ef">name</span>: OSPFv3 Interfaces
  <span style="color:#66d9ef">vyos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - set protocols ospfv3 area {{ item.ospf.area }} interface {{ item.vyos_if }}
  <span style="color:#66d9ef">when</span>:
    - item.ospfv3 is defined
    - item.vif is not defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf
    - ospf_v6

- <span style="color:#66d9ef">name</span>: OSPFv3 Interfaces - vif
  <span style="color:#66d9ef">vyos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - set protocols ospfv3 area {{ item.ospf.area }} interface {{ item.vyos_if }}.{{ item.vif }}
  <span style="color:#66d9ef">when</span>:
     - item.ospfv3 is defined
     - item.vif is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf
    - ospf_v6

- <span style="color:#66d9ef">name</span>: OSPFv3 Interfaces - Passive
  <span style="color:#66d9ef">vyos_config</span>:
    <span style="color:#66d9ef">src</span>: ospfv3_passive.j2
  <span style="color:#66d9ef">tags</span>:
    - ospf
    - ospf_v6
</code></pre></div><p>We allow OSPFv3 to dynamically discover the router ID in this case, although you may prefer to set it statically.</p>
<p>The first two tasks are fundamentally the same as for OSPF, except we use the <code>ospfv3</code> keyword rather than <code>ospf</code> and that we apply OSPFv3 to an interface rather than using a <code>network</code> to match interfaces.</p>
<p>However we use a different task for setting passive interfaces. This is because you need to define whether an interface is passive using the <code>set interface $MEDIA $INTERFACE ipv6 ospf passive</code> syntax, which differs for the three kinds of interfaces to apply this to (Ethernet, VIFs and Loopbacks). Rather than creating three separate tasks for this, we use a template instead.</p>
<p>This template looks like the below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% for interface in interfaces %}
  {% if interface.ospfv3 is defined %}
    {% if interface.ospfv3.passive is defined %}
      {% if &quot;eth&quot; in interface.vyos_if %}
        {% if interface.vif is defined %}
interfaces ethernet {{ interface.vyos_if }} vif {{ interface.vif }} ipv6 ospfv3 passive
        {% else %}
interfaces ethernet {{ interface.vyos_if }} ipv6 ospfv3 passive
        {% endif %}
      {% elif &quot;lo&quot; in interface.vyos_if %}
interfaces loopback {{ interface.vyos_if }} ipv6 ospfv3 passive
      {% endif %}
   {% endif %}
  {% endif %}
{% endfor %}
</code></pre><p>As you can see, we are doing the following: -</p>
<ul>
<li>Looping through our interfaces&hellip;</li>
<li>If the interface has an OSPFv3 section then&hellip;</li>
<li>If the interface has a VIF then apply the correct <code>passive</code> command to it else&hellip;</li>
<li>If the interface does not have a VIF, apply the passive command directly to the interface, else&hellip;</li>
<li>If the interface is a loopback, use the correct command for the loopback</li>
</ul>
<p>The use of the media type of the interface, as well as VIFs requiring slightly different syntax makes applying passive OSPFv3 commands slightly more complex.</p>
<p>The <code>host_vars</code> that are relevant to this playbook are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth1&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Management&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.15.30.63/24&#34;</span>
    <span style="color:#66d9ef">zone</span>: <span style="color:#e6db74">&#34;mgmt&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth2&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;VLAN Bridge&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth2&#34;</span>
    <span style="color:#66d9ef">vif</span>: <span style="color:#ae81ff">105</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To netsvr&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.100.105.253/24&#34;</span>
    <span style="color:#66d9ef">ipv6_addr</span>: <span style="color:#e6db74">&#34;2001:db8:105::f/64&#34;</span>
    <span style="color:#66d9ef">zone</span>: <span style="color:#e6db74">&#34;external&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth2&#34;</span>
    <span style="color:#66d9ef">vif</span>: <span style="color:#ae81ff">205</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To vyos-02&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">zone</span>: <span style="color:#e6db74">&#34;internal&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.100.205.254/24&#34;</span>
    <span style="color:#66d9ef">ipv6_addr</span>: <span style="color:#e6db74">&#34;2001:db8:205::a/64&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth0&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To the Internet&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;dhcp&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">nat</span>:
      <span style="color:#66d9ef">role</span>: <span style="color:#e6db74">&#34;outside&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;lo&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;192.0.2.105/32&#34;</span>
    <span style="color:#66d9ef">ipv6_addr</span>: <span style="color:#e6db74">&#34;2001:db8:905:beef::1/128&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
</code></pre></div><p>This then generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set interfaces ethernet eth2 vif 105 ipv6 ospfv3 passive
set interfaces loopback lo ipv6 ospfv3 passive
set protocols ospfv3 area 0.0.0.0 interface &#39;lo&#39;
set protocols ospfv3 area 0.0.0.0 interface &#39;eth2.105&#39;
set protocols ospfv3 area 0.0.0.0 interface &#39;eth2.205&#39;
</code></pre></div><h5 id="verification-4">Verification</h5>
<p>We&rsquo;ll follow the same steps as we did for OSPF: -</p>
<p><strong>vyos-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show OSPFv3 interfaces
vyos@vyos-01:~$ show ipv6 ospfv3 interface
eth0 is up, type BROADCAST
  Interface ID: 2
   OSPF not enabled on this interface
eth1 is up, type BROADCAST
  Interface ID: 3
   OSPF not enabled on this interface
eth2 is up, type BROADCAST
  Interface ID: 5
   OSPF not enabled on this interface
eth2.105 is up, type BROADCAST
  Interface ID: 6
  Internet Address:
    inet : 10.100.105.253/24
    inet6: 2001:db8:105::f/64
    inet6: fe80::5054:ff:fe23:2eac/64
  Instance ID 0, Interface MTU 1500 (autodetect: 1500)
  MTU mismatch detection: enabled
  Area ID 0.0.0.0, Cost 1
  State DR, Transmit Delay 1 sec, Priority 1
  Timer intervals configured:
   Hello 10, Dead 40, Retransmit 5
  DR: 192.0.2.105 BDR: 0.0.0.0
  Number of I/F scoped LSAs is 1
    0 Pending LSAs for LSUpdate in Time 00:00:00 [thread off]
    0 Pending LSAs for LSAck in Time 00:00:00 [thread off]
eth2.205 is up, type BROADCAST
  Interface ID: 7
  Internet Address:
    inet : 10.100.205.254/24
    inet6: fe80::5054:ff:fe23:2eac/64
    inet6: 2001:db8:205::a/64
  Instance ID 0, Interface MTU 1500 (autodetect: 1500)
  MTU mismatch detection: enabled
  Area ID 0.0.0.0, Cost 100
  State BDR, Transmit Delay 1 sec, Priority 1
  Timer intervals configured:
   Hello 10, Dead 40, Retransmit 5
  DR: 192.0.2.205 BDR: 192.0.2.105
  Number of I/F scoped LSAs is 2
    0 Pending LSAs for LSUpdate in Time 00:00:00 [thread off]
    0 Pending LSAs for LSAck in Time 00:00:00 [thread off]
eth3 is up, type BROADCAST
  Interface ID: 4
   OSPF not enabled on this interface
lo is up, type LOOPBACK
  Interface ID: 1
  Internet Address:
    inet : 192.0.2.105/32
    inet6: 2001:db8:905:beef::1/128
    inet6: fe80::200:ff:fe00:0/64
  Instance ID 0, Interface MTU 65536 (autodetect: 65536)
  MTU mismatch detection: enabled
  Area ID 0.0.0.0, Cost 1
  State DR, Transmit Delay 1 sec, Priority 1
  Timer intervals configured:
   Hello 10, Dead 40, Retransmit 5
  DR: 192.0.2.105 BDR: 0.0.0.0
  Number of I/F scoped LSAs is 1
    0 Pending LSAs for LSUpdate in Time 00:00:00 [thread off]
    0 Pending LSAs for LSAck in Time 00:00:00 [thread off]

! Show OSPFv3 neighbours
Neighbor ID     Pri    DeadTime    State/IfState         Duration I/F[State]
192.0.2.205       1    00:00:38     Full/DR              00:24:06 eth2.205[BDR]

! Show routing table
vyos@vyos-01:~$ show ipv6 ospfv3 neighbor
Neighbor ID     Pri    DeadTime    State/IfState         Duration I/F[State]
192.0.2.205       1    00:00:38     Full/DR              00:24:06 eth2.205[BDR]
vyos@vyos-01:~$ show ipv6 route ospfv3
Codes: K - kernel route, C - connected, S - static, R - RIPng,
       O - OSPFv3, I - IS-IS, B - BGP, N - NHRP, T - Table,
       v - VNC, V - VNC-Direct, A - Babel, D - SHARP, F - PBR,
       f - OpenFabric,
       &gt; - selected route, * - FIB route, q - queued route, r - rejected route

O   2001:db8:105::/64 [110/1] is directly connected, eth2.105, 00:24:59
O   2001:db8:205::/64 [110/100] is directly connected, eth2.205, 00:24:23
O   2001:db8:905:beef::1/128 [110/1] is directly connected, lo, 00:24:59
O&gt;* 2001:db8:905:beef::2/128 [110/101] via fe80::5054:ff:fe50:4198, eth2.205, 00:24:18

! Ping!
vyos@vyos-01:~$ ping 2001:db8:905:beef::2
PING 2001:db8:905:beef::2(2001:db8:905:beef::2) 56 data bytes
64 bytes from 2001:db8:905:beef::2: icmp_seq=1 ttl=64 time=0.836 ms
64 bytes from 2001:db8:905:beef::2: icmp_seq=2 ttl=64 time=0.615 ms
^C
--- 2001:db8:905:beef::2 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 29ms
rtt min/avg/max/mdev = 0.615/0.725/0.836/0.113 ms
</code></pre></div><p><strong>vyos-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show OSPFv3 interfaces
vyos@vyos-02:~$ show ipv6 ospfv3 interface
eth0 is up, type BROADCAST
  Interface ID: 2
   OSPF not enabled on this interface
eth1 is up, type BROADCAST
  Interface ID: 3
   OSPF not enabled on this interface
eth2 is up, type BROADCAST
  Interface ID: 4
   OSPF not enabled on this interface
eth2.205 is up, type BROADCAST
  Interface ID: 5
  Internet Address:
    inet : 10.100.205.253/24
    inet6: fe80::5054:ff:fe50:4198/64
    inet6: 2001:db8:205::f/64
  Instance ID 0, Interface MTU 1500 (autodetect: 1500)
  MTU mismatch detection: enabled
  Area ID 0.0.0.0, Cost 1
  State DR, Transmit Delay 1 sec, Priority 1
  Timer intervals configured:
   Hello 10, Dead 40, Retransmit 5
  DR: 192.0.2.205 BDR: 192.0.2.105
  Number of I/F scoped LSAs is 2
    0 Pending LSAs for LSUpdate in Time 00:00:00 [thread off]
    0 Pending LSAs for LSAck in Time 00:00:00 [thread off]
lo is up, type LOOPBACK
  Interface ID: 1
  Internet Address:
    inet : 192.0.2.205/32
    inet6: 2001:db8:905:beef::2/128
    inet6: fe80::200:ff:fe00:0/64
  Instance ID 0, Interface MTU 65536 (autodetect: 65536)
  MTU mismatch detection: enabled
  Area ID 0.0.0.0, Cost 1
  State DR, Transmit Delay 1 sec, Priority 1
  Timer intervals configured:
   Hello 10, Dead 40, Retransmit 5
  DR: 192.0.2.205 BDR: 0.0.0.0
  Number of I/F scoped LSAs is 1
    0 Pending LSAs for LSUpdate in Time 00:00:00 [thread off]
    0 Pending LSAs for LSAck in Time 00:00:00 [thread off]

! Show OSPFv3 neighbours
vyos@vyos-02:~$ show ipv6 ospfv3 neighbor
Neighbor ID     Pri    DeadTime    State/IfState         Duration I/F[State]
192.0.2.105       1    00:00:39     Full/BDR             00:26:09 eth2.205[DR]

! Show routing table
vyos@vyos-02:~$ show ipv6 ospfv3 neighbor
Neighbor ID     Pri    DeadTime    State/IfState         Duration I/F[State]
192.0.2.105       1    00:00:39     Full/BDR             00:26:09 eth2.205[DR]
vyos@vyos-02:~$ show ipv6 route ospfv3
Codes: K - kernel route, C - connected, S - static, R - RIPng,
       O - OSPFv3, I - IS-IS, B - BGP, N - NHRP, T - Table,
       v - VNC, V - VNC-Direct, A - Babel, D - SHARP, F - PBR,
       f - OpenFabric,
       &gt; - selected route, * - FIB route, q - queued route, r - rejected route

O&gt;* 2001:db8:105::/64 [110/2] via fe80::5054:ff:fe23:2eac, eth2.205, 00:26:18
O   2001:db8:205::/64 [110/1] is directly connected, eth2.205, 00:26:23
O&gt;* 2001:db8:905:beef::1/128 [110/2] via fe80::5054:ff:fe23:2eac, eth2.205, 00:26:18
O   2001:db8:905:beef::2/128 [110/1] is directly connected, lo, 00:27:10

! Ping!
vyos@vyos-02:~$ ping 2001:db8:905:beef::1
PING 2001:db8:905:beef::1(2001:db8:905:beef::1) 56 data bytes
64 bytes from 2001:db8:905:beef::1: icmp_seq=1 ttl=64 time=0.803 ms
64 bytes from 2001:db8:905:beef::1: icmp_seq=2 ttl=64 time=0.757 ms
^C
--- 2001:db8:905:beef::1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 6ms
rtt min/avg/max/mdev = 0.757/0.780/0.803/0.023 ms

vyos@vyos-02:~$ ping 2001:db8:105::f
PING 2001:db8:105::f(2001:db8:105::f) 56 data bytes
64 bytes from 2001:db8:105::f: icmp_seq=1 ttl=64 time=0.517 ms
64 bytes from 2001:db8:105::f: icmp_seq=2 ttl=64 time=0.712 ms
64 bytes from 2001:db8:105::f: icmp_seq=3 ttl=64 time=0.563 ms
^C
--- 2001:db8:105::f ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 35ms
rtt min/avg/max/mdev = 0.517/0.597/0.712/0.085 ms
</code></pre></div><p>Looks good to me!</p>
<h4 id="bgp-playbook">BGP Playbook</h4>
<p>The BGP playbook is where we configure our internal and external BGP peers. There are no BGP Ansible modules for VyOS, so we use <code>vyos_config</code> instead.</p>
<p>We are also applying prefix lists and route maps to only allow certain routes to be advertised and received. In day to day BGP configuration, you would configure these all the time, so it makes sense to include tasks to configure them.</p>
<p>The playbook itself looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
- <span style="color:#66d9ef">name</span>: Configure Prefix Lists - IPv4
  <span style="color:#66d9ef">vyos_config</span>:
    <span style="color:#66d9ef">src</span>: prefixlists_v4.j2
  <span style="color:#66d9ef">when</span>:
    - route_maps is defined
    - route_maps.prefix_lists is defined
    - route_maps.prefix_lists.ipv4 is defined
  <span style="color:#66d9ef">tags</span>:
    - bgp
    - bgp_v4

- <span style="color:#66d9ef">name</span>: Configure Prefix Lists - IPv6
  <span style="color:#66d9ef">vyos_config</span>:
    <span style="color:#66d9ef">src</span>: prefixlists_v6.j2
  <span style="color:#66d9ef">when</span>:
    - route_maps is defined
    - route_maps.prefix_lists is defined
    - route_maps.prefix_lists.ipv6 is defined
  <span style="color:#66d9ef">tags</span>:
    - bgp
    - bgp_v6

- <span style="color:#66d9ef">name</span>: Configure Route Maps
  <span style="color:#66d9ef">vyos_config</span>:
    <span style="color:#66d9ef">src</span>: routemap.j2
  <span style="color:#66d9ef">when</span>:
    - route_maps is defined
    - route_maps.rm is defined
  <span style="color:#66d9ef">tags</span>:
    - bgp
    - bgp_v4
    - bgp_v6

- <span style="color:#66d9ef">name</span>: Configure BGP Peers
  <span style="color:#66d9ef">vyos_config</span>:
    <span style="color:#66d9ef">src</span>: bgp.j2
  <span style="color:#66d9ef">when</span>:
    - bgp is defined
  <span style="color:#66d9ef">tags</span>:
    - bgp
    - bgp_v4
    - bgp_v6
</code></pre></div><h5 id="prefix-lists---ipv4">Prefix Lists - IPv4</h5>
<p><strong>Ansible module</strong>: <code>vyos_config</code></p>
<p>Prefix lists are used to match a range of IP addresses. They can be exact matches (i.e. <code>192.168.0.0/24</code>), or you can match on a longer or shorter prefix length.</p>
<p>The configuration template looks like the below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% for pfx_list in route_maps['prefix_lists']['ipv4'] %}
{% for address in pfx_list['addresses'] %}
set policy prefix-list {{ pfx_list['name'] }} rule {{ loop.index }} prefix {{ address }}
set policy prefix-list {{ pfx_list['name'] }} rule {{ loop.index }} action {{ pfx_list['action'] }}
{% endfor %}
{% endfor %}
</code></pre><p>As you can see, we use the <code>{{ loop.index }}</code> value again, which increments on every iteration. This means that each rule has a number to say what order it will be evaluated in.</p>
<p>The relevant <code>host_vars</code> for this are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">route_maps</span>:
  <span style="color:#66d9ef">prefix_lists</span>:
    <span style="color:#66d9ef">ipv4</span>:
      - <span style="color:#66d9ef">name</span>: internal-nets-v4
        <span style="color:#66d9ef">addresses</span>:
           - <span style="color:#ae81ff">192.0.2.105</span>/<span style="color:#ae81ff">32</span>
           - <span style="color:#ae81ff">192.0.2.205</span>/<span style="color:#ae81ff">32</span>
           - <span style="color:#ae81ff">10.100.205.0</span>/<span style="color:#ae81ff">24</span>
        <span style="color:#66d9ef">action</span>: permit
      - <span style="color:#66d9ef">name</span>: external-nets-v4
        <span style="color:#66d9ef">addresses</span>:
           - <span style="color:#ae81ff">192.0.2.1</span>/<span style="color:#ae81ff">32</span>
           - <span style="color:#ae81ff">10.100.105.0</span>/<span style="color:#ae81ff">24</span>
        <span style="color:#66d9ef">action</span>: permit
</code></pre></div><p>This generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set policy prefix-list external-nets-v4 rule 1 action &#39;permit&#39;
set policy prefix-list external-nets-v4 rule 1 prefix &#39;192.0.2.1/32&#39;
set policy prefix-list external-nets-v4 rule 2 action &#39;permit&#39;
set policy prefix-list external-nets-v4 rule 2 prefix &#39;10.100.105.0/24&#39;
set policy prefix-list internal-nets-v4 rule 1 action &#39;permit&#39;
set policy prefix-list internal-nets-v4 rule 1 prefix &#39;192.0.2.105/32&#39;
set policy prefix-list internal-nets-v4 rule 2 action &#39;permit&#39;
set policy prefix-list internal-nets-v4 rule 2 prefix &#39;192.0.2.205/32&#39;
set policy prefix-list internal-nets-v4 rule 3 action &#39;permit&#39;
set policy prefix-list internal-nets-v4 rule 3 prefix &#39;10.100.205.0/24&#39;
</code></pre></div><h5 id="prefix-lists---ipv6">Prefix Lists - IPv6</h5>
<p><strong>Ansible module</strong>: <code>vyos_config</code></p>
<p>This task performs the same function as the previous one, except for IPv6 prefixes.</p>
<p>The configuration template looks like the below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% for pfx_list in route_maps['prefix_lists']['ipv6'] %}
{% for address in pfx_list['addresses'] %}
set policy prefix-list6 {{ pfx_list['name'] }} rule {{ loop.index }} prefix {{ address }}
set policy prefix-list6 {{ pfx_list['name'] }} rule {{ loop.index }} action {{ pfx_list['action'] }}
{% endfor %}
{% endfor %}
</code></pre><p>The only difference here is that we use <code>prefix-list6</code> instead of <code>prefix-list</code>.</p>
<p>The relevant <code>host_vars</code> for this are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">route_maps</span>:
  <span style="color:#66d9ef">prefix_lists</span>:
    <span style="color:#66d9ef">ipv6</span>:
      - <span style="color:#66d9ef">name</span>: internal-nets-v6
        <span style="color:#66d9ef">addresses</span>:
           - <span style="color:#e6db74">&#34;2001:db8:905:beef::1/128&#34;</span>
           - <span style="color:#e6db74">&#34;2001:db8:905:beef::2/128&#34;</span>
           - <span style="color:#e6db74">&#34;2001:db8:905::/64&#34;</span>
        <span style="color:#66d9ef">action</span>: permit
      - <span style="color:#66d9ef">name</span>: external-nets-v6
        <span style="color:#66d9ef">addresses</span>:
           - <span style="color:#e6db74">&#34;2001:db8:999:beef::1/128&#34;</span>
        <span style="color:#66d9ef">action</span>: permit
</code></pre></div><p>This generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set policy prefix-list6 external-nets-v6 rule 1 action &#39;permit&#39;
set policy prefix-list6 external-nets-v6 rule 1 prefix &#39;2001:db8:999:beef::1/128&#39;
set policy prefix-list6 internal-nets-v6 rule 1 action &#39;permit&#39;
set policy prefix-list6 internal-nets-v6 rule 1 prefix &#39;2001:db8:905:beef::1/128&#39;
set policy prefix-list6 internal-nets-v6 rule 2 action &#39;permit&#39;
set policy prefix-list6 internal-nets-v6 rule 2 prefix &#39;2001:db8:905:beef::2/128&#39;
set policy prefix-list6 internal-nets-v6 rule 3 action &#39;permit&#39;
set policy prefix-list6 internal-nets-v6 rule 3 prefix &#39;2001:db8:905::/64&#39;
</code></pre></div><h5 id="route-maps">Route Maps</h5>
<p><strong>Ansible module</strong>: <code>vyos_config</code></p>
<p>Route maps are used to apply our chosen actions to the routes advertised to, or received from, our BGP neighbours.</p>
<p>Our template looks like the below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% for route_map in route_maps['rm'] %}
{% for rule in route_map['rules'] %}
{% if rule['pfx_list'] is defined %}
set policy route-map {{ route_map['name'] }} rule {{ loop.index }} match ip address prefix-list {{ rule['pfx_list'] }}
{% endif %}
{% if rule['pfx_list6'] is defined %}
set policy route-map {{ route_map['name'] }} rule {{ loop.index }} match ipv6 address prefix-list {{ rule['pfx_list6'] }}
{% endif %}
set policy route-map {{ route_map['name'] }} rule {{ loop.index }} action {{ rule['action'] }}
{% endfor %}
{% endfor %}
</code></pre><p>In this, we loop through the <code>route_maps</code> section of our <code>host_vars</code>. If the prefix list used to match is in the <code>pfx_list</code> section, we use <code>match ip address prefix-list $PREFIX_LIST_NAME</code> (i.e. for IPv4). If it is in the <code>pfx_list6</code> section, we use <code>match ipv6 address prefix-list $IPv6_PREFIX_LIST_NAME</code> (i.e. for IPv6).</p>
<p>The relevant <code>host_vars</code> for this are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">route_maps</span>:
  <span style="color:#66d9ef">rm</span>:
    - <span style="color:#66d9ef">name</span>: external-networks-v4
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">pfx_list</span>: external-nets-v4
          <span style="color:#66d9ef">action</span>: permit
        - <span style="color:#66d9ef">action</span>: deny
    - <span style="color:#66d9ef">name</span>: internal-networks-v4
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">pfx_list</span>: internal-nets-v4
          <span style="color:#66d9ef">action</span>: permit
        - <span style="color:#66d9ef">action</span>: deny
    - <span style="color:#66d9ef">name</span>: external-networks-v6
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">pfx_list6</span>: external-nets-v6
          <span style="color:#66d9ef">action</span>: permit
        - <span style="color:#66d9ef">action</span>: deny
    - <span style="color:#66d9ef">name</span>: internal-networks-v6
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">pfx_list6</span>: internal-nets-v6
          <span style="color:#66d9ef">action</span>: permit
        - <span style="color:#66d9ef">action</span>: deny
</code></pre></div><p>The generated configuration looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set policy route-map external-networks-v4 rule 1 action &#39;permit&#39;
set policy route-map external-networks-v4 rule 1 match ip address prefix-list &#39;external-nets-v4&#39;
set policy route-map external-networks-v4 rule 2 action &#39;deny&#39;
set policy route-map external-networks-v6 rule 1 action &#39;permit&#39;
set policy route-map external-networks-v6 rule 1 match ipv6 address prefix-list &#39;external-nets-v6&#39;
set policy route-map external-networks-v6 rule 2 action &#39;deny&#39;
set policy route-map internal-networks-v4 rule 1 action &#39;permit&#39;
set policy route-map internal-networks-v4 rule 1 match ip address prefix-list &#39;internal-nets-v4&#39;
set policy route-map internal-networks-v4 rule 2 action &#39;deny&#39;
set policy route-map internal-networks-v6 rule 1 action &#39;permit&#39;
set policy route-map internal-networks-v6 rule 1 match ipv6 address prefix-list &#39;internal-nets-v6&#39;
set policy route-map internal-networks-v6 rule 2 action &#39;deny&#39;
</code></pre></div><h5 id="configuring-bgp-peers">Configuring BGP peers</h5>
<p><strong>Ansible module</strong>: <code>vyos_config</code></p>
<p>This task creates the BGP peers. The template is quite complex, so I will explain each section in detail: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">set protocols bgp {{ bgp['local_as'] }} parameters router-id {{ router_id }}
set protocols bgp {{ bgp['local_as'] }} parameters default no-ipv4-unicast
{% if bgp['redistribute'] is defined %}
{% if bgp['redistribute']['ospf'] is defined %}
set protocols bgp {{ bgp['local_as'] }} address-family ipv4-unicast redistribute ospf
{% endif %}
{% endif %}
{% if bgp['redistribute'] is defined %}
{% if bgp['redistribute']['ospfv3'] is defined %}
set protocols bgp {{ bgp['local_as'] }} address-family ipv6-unicast redistribute ospfv3
{% endif %}
{% endif %}
{% if bgp['neighbours']['ipv4'] is defined %}
{% for neighbour in bgp['neighbours']['ipv4'] %}
set protocols bgp {{ bgp['local_as'] }} neighbor {{ neighbour['peer'] }} remote-as {{ neighbour['remote_as'] }}
set protocols bgp {{ bgp['local_as'] }} neighbor {{ neighbour['peer'] }} address-family ipv4-unicast
set protocols bgp {{ bgp['local_as'] }} neighbor {{ neighbour['peer'] }} description &quot;{{ neighbour['desc'] }}&quot;
{% if neighbour['loc_ip'] is defined %}
set protocols bgp {{ bgp['local_as'] }} neighbor {{ neighbour['peer'] }} update-source {{ neighbour['loc_ip'] }}
{% endif %}
{% if neighbour['default_originate'] is defined %}
set protocols bgp {{ bgp['local_as'] }} neighbor {{ neighbour['peer'] }} address-family ipv4-unicast default-originate
{% endif %}
{% if neighbour['route_map'] is defined %}
{% if neighbour['route_map']['in'] is defined %}
set protocols bgp {{ bgp['local_as'] }} neighbor {{ neighbour['peer'] }} address-family ipv4-unicast route-map import {{ neighbour['route_map']['in'] }}
{% endif %}
{% if neighbour['route_map']['out'] is defined %}
set protocols bgp {{ bgp['local_as'] }} neighbor {{ neighbour['peer'] }} address-family ipv4-unicast route-map export {{ neighbour['route_map']['out'] }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% if bgp['neighbours']['ipv6'] is defined %}
{% for neighbour in bgp['neighbours']['ipv6'] %}
set protocols bgp {{ bgp['local_as'] }} neighbor {{ neighbour['peer'] }} remote-as {{ neighbour['remote_as'] }}
set protocols bgp {{ bgp['local_as'] }} neighbor {{ neighbour['peer'] }} address-family ipv6-unicast
set protocols bgp {{ bgp['local_as'] }} neighbor {{ neighbour['peer'] }} description &quot;{{ neighbour['desc'] }}&quot;
{% if neighbour['loc_ip'] is defined %}
set protocols bgp {{ bgp['local_as'] }} neighbor {{ neighbour['peer'] }} update-source {{ neighbour['loc_ip'] }}
{% endif %}
{% if neighbour['route_map'] is defined %}
{% if neighbour['route_map']['in'] is defined %}
set protocols bgp {{ bgp['local_as'] }} neighbor {{ neighbour['peer'] }} address-family ipv6-unicast route-map import {{ neighbour['route_map']['in'] }}
{% endif %}
{% if neighbour['route_map']['out'] is defined %}
set protocols bgp {{ bgp['local_as'] }} neighbor {{ neighbour['peer'] }} address-family ipv6-unicast route-map export {{ neighbour['route_map']['out'] }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
</code></pre><p>There are multiple different sections here, and we also combine redistribution and IPv4 and IPv6 in the same template.</p>
<pre><code class="language-jinja2" data-lang="jinja2">set protocols bgp {{ bgp['local_as'] }} parameters router-id {{ router_id }}
set protocols bgp {{ bgp['local_as'] }} parameters default no-ipv4-unicast
</code></pre><p>These two lines set the router ID for BGP, and also disable IPv4 unicast by default. The reason for the latter is that IPv4 unicast would be enabled for every peer, including IPv6 peers. This means that we would advertise IPv4 routes over an IPv6 session, making our route-maps applied to the IPv4 peers superfluous.</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% if bgp['redistribute'] is defined %}
{% if bgp['redistribute']['ospf'] is defined %}
set protocols bgp {{ bgp['local_as'] }} address-family ipv4-unicast redistribute ospf
{% endif %}
{% endif %}
</code></pre><p>This section enables the redistribution of OSPF routes into BGP if it has been defined in our <code>host_vars</code>.</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% if bgp['redistribute'] is defined %}
{% if bgp['redistribute']['ospfv3'] is defined %}
set protocols bgp {{ bgp['local_as'] }} address-family ipv6-unicast redistribute ospfv3
{% endif %}
{% endif %}
</code></pre><p>This is the same as above, except for IPv6.</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% if bgp['neighbours']['ipv4'] is defined %}
{% for neighbour in bgp['neighbours']['ipv4'] %}
set protocols bgp {{ bgp['local_as'] }} neighbor {{ neighbour['peer'] }} remote-as {{ neighbour['remote_as'] }}
set protocols bgp {{ bgp['local_as'] }} neighbor {{ neighbour['peer'] }} address-family ipv4-unicast
set protocols bgp {{ bgp['local_as'] }} neighbor {{ neighbour['peer'] }} description &quot;{{ neighbour['desc'] }}&quot;
{% if neighbour['loc_ip'] is defined %}
set protocols bgp {{ bgp['local_as'] }} neighbor {{ neighbour['peer'] }} update-source {{ neighbour['loc_ip'] }}
{% endif %}
{% if neighbour['default_originate'] is defined %}
set protocols bgp {{ bgp['local_as'] }} neighbor {{ neighbour['peer'] }} address-family ipv4-unicast default-originate
{% endif %}
{% if neighbour['route_map'] is defined %}
{% if neighbour['route_map']['in'] is defined %}
set protocols bgp {{ bgp['local_as'] }} neighbor {{ neighbour['peer'] }} address-family ipv4-unicast route-map import {{ neighbour['route_map']['in'] }}
{% endif %}
{% if neighbour['route_map']['out'] is defined %}
set protocols bgp {{ bgp['local_as'] }} neighbor {{ neighbour['peer'] }} address-family ipv4-unicast route-map export {{ neighbour['route_map']['out'] }}
{% endif %}
{% endif %}
</code></pre><p>In this section, we loop through any IPv4 BGP peers we have defined in our <code>host_vars</code> and then: -</p>
<ul>
<li>We configure the remote autonomous system number, enable IPv4 unicast on a per peer basis, and apply a description to the peer</li>
<li>If we have defined a local IP to source BGP from for this peer (potentially the loopback IP), we set it as an update source</li>
<li>If we want to unconditionally advertise a default route to the peer, we enable it with <code>default-originate</code></li>
<li>If any route maps are defined, we apply them either in the inbound (<code>import</code>) or outbound (<code>export</code>) direction</li>
</ul>
<pre><code class="language-jinja2" data-lang="jinja2">{% if bgp['neighbours']['ipv6'] is defined %}
{% for neighbour in bgp['neighbours']['ipv6'] %}
set protocols bgp {{ bgp['local_as'] }} neighbor {{ neighbour['peer'] }} remote-as {{ neighbour['remote_as'] }}
set protocols bgp {{ bgp['local_as'] }} neighbor {{ neighbour['peer'] }} address-family ipv6-unicast
set protocols bgp {{ bgp['local_as'] }} neighbor {{ neighbour['peer'] }} description &quot;{{ neighbour['desc'] }}&quot;
{% if neighbour['loc_ip'] is defined %}
set protocols bgp {{ bgp['local_as'] }} neighbor {{ neighbour['peer'] }} update-source {{ neighbour['loc_ip'] }}
{% endif %}
{% if neighbour['route_map'] is defined %}
{% if neighbour['route_map']['in'] is defined %}
set protocols bgp {{ bgp['local_as'] }} neighbor {{ neighbour['peer'] }} address-family ipv6-unicast route-map import {{ neighbour['route_map']['in'] }}
{% endif %}
{% if neighbour['route_map']['out'] is defined %}
set protocols bgp {{ bgp['local_as'] }} neighbor {{ neighbour['peer'] }} address-family ipv6-unicast route-map export {{ neighbour['route_map']['out'] }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
</code></pre><p>This section does fundamentally the same as the above except: -</p>
<ul>
<li>It enables the IPv6 address family (for IPv6 peers)</li>
<li>We do not use the <code>default-originate</code> command, as we have no IPv6 internet to test it with</li>
</ul>
<p>The relevant <code>host_vars</code> for this are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">bgp</span>:
  <span style="color:#66d9ef">local_as</span>: <span style="color:#ae81ff">65105</span>
  <span style="color:#66d9ef">redistribute</span>:
    <span style="color:#66d9ef">ospf</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">neighbours</span>:
    <span style="color:#66d9ef">ipv4</span>:
      - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">10.100.105.254</span>
        <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65430</span>
        <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;netsvr-01 IPv4&#34;</span>
        <span style="color:#66d9ef">route_map</span>:
          <span style="color:#66d9ef">in</span>: external-networks-v4
          <span style="color:#66d9ef">out</span>: internal-networks-v4
      - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">192.0.2.205</span>
        <span style="color:#66d9ef">loc_ip</span>: <span style="color:#ae81ff">192.0.2.105</span>
        <span style="color:#66d9ef">default_originate</span>: <span style="color:#66d9ef">true</span>
        <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;vyos-02 IPv4&#34;</span>
        <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65105</span>
        <span style="color:#66d9ef">route_map</span>:
          <span style="color:#66d9ef">out</span>: external-networks-v4
          <span style="color:#66d9ef">in</span>: internal-networks-v4
    <span style="color:#66d9ef">ipv6</span>:
      - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:105::ffff&#34;</span>
        <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65430</span>
        <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;netsvr-01 IPv6&#34;</span>
        <span style="color:#66d9ef">route_map</span>:
          <span style="color:#66d9ef">in</span>: external-networks-v6
          <span style="color:#66d9ef">out</span>: internal-networks-v6
      - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:905:beef::2&#34;</span>
        <span style="color:#66d9ef">loc_ip</span>: <span style="color:#e6db74">&#34;2001:db8:905:beef::1&#34;</span>
        <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;vyos-02 IPv6&#34;</span>
        <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65105</span>
        <span style="color:#66d9ef">route_map</span>:
          <span style="color:#66d9ef">out</span>: external-networks-v6
          <span style="color:#66d9ef">in</span>: internal-networks-v6
</code></pre></div><p>This generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set protocols bgp 65105 address-family ipv4-unicast redistribute ospf
set protocols bgp 65105 address-family ipv6-unicast redistribute ospfv3
set protocols bgp 65105 neighbor 10.100.105.254 address-family ipv4-unicast route-map export &#39;internal-networks-v4&#39;
set protocols bgp 65105 neighbor 10.100.105.254 address-family ipv4-unicast route-map import &#39;external-networks-v4&#39;
set protocols bgp 65105 neighbor 10.100.105.254 description &#39;netsvr-01 IPv4&#39;
set protocols bgp 65105 neighbor 10.100.105.254 remote-as &#39;65430&#39;
set protocols bgp 65105 neighbor 192.0.2.205 address-family ipv4-unicast default-originate
set protocols bgp 65105 neighbor 192.0.2.205 address-family ipv4-unicast route-map export &#39;external-networks-v4&#39;
set protocols bgp 65105 neighbor 192.0.2.205 address-family ipv4-unicast route-map import &#39;internal-networks-v4&#39;
set protocols bgp 65105 neighbor 192.0.2.205 description &#39;vyos-02 IPv4&#39;
set protocols bgp 65105 neighbor 192.0.2.205 remote-as &#39;65105&#39;
set protocols bgp 65105 neighbor 192.0.2.205 update-source &#39;192.0.2.105&#39;
set protocols bgp 65105 neighbor 2001:db8:105::ffff address-family ipv6-unicast route-map export &#39;internal-networks-v6&#39;
set protocols bgp 65105 neighbor 2001:db8:105::ffff address-family ipv6-unicast route-map import &#39;external-networks-v6&#39;
set protocols bgp 65105 neighbor 2001:db8:105::ffff description &#39;netsvr-01 IPv6&#39;
set protocols bgp 65105 neighbor 2001:db8:105::ffff remote-as &#39;65430&#39;
set protocols bgp 65105 neighbor 2001:db8:905:beef::2 address-family ipv6-unicast route-map export &#39;external-networks-v6&#39;
set protocols bgp 65105 neighbor 2001:db8:905:beef::2 address-family ipv6-unicast route-map import &#39;internal-networks-v6&#39;
set protocols bgp 65105 neighbor 2001:db8:905:beef::2 description &#39;vyos-02 IPv6&#39;
set protocols bgp 65105 neighbor 2001:db8:905:beef::2 remote-as &#39;65105&#39;
set protocols bgp 65105 neighbor 2001:db8:905:beef::2 update-source &#39;2001:db8:905:beef::1&#39;
set protocols bgp 65105 parameters default no-ipv4-unicast
set protocols bgp 65105 parameters router-id &#39;192.0.2.105&#39;
</code></pre></div><h5 id="verification-5">Verification</h5>
<p>After all of the above has run, we should have BGP sessions up over IPv4 and IPv6, as well as routes received and sent to <strong>netsvr-01</strong> BGP route server.</p>
<p><strong>vyos-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show IPv4 BGP neighbours
vyos@vyos-01:~$ show ip bgp summary

IPv4 Unicast Summary:
BGP router identifier 192.0.2.105, local AS number 65105 vrf-id 0
BGP table version 8
RIB entries 15, using 2760 bytes of memory
Peers 2, using 41 KiB of memory

Neighbor        V         AS MsgRcvd MsgSent   TblVer  InQ OutQ  Up/Down State/PfxRcd
10.100.105.254  4      65430      62      62        0    0    0 00:56:42            1
192.0.2.205     4      65105      58      61        0    0    0 00:55:54            0

Total number of neighbors 2

! Show IPv4 BGP routes
vyos@vyos-01:~$ show ip bgp
BGP table version is 8, local router ID is 192.0.2.105, vrf id 0
Default local pref 100, local AS 65105
Status codes:  s suppressed, d damped, h history, * valid, &gt; best, = multipath,
               i internal, r RIB-failure, S Stale, R Removed
Nexthop codes: @NNN nexthop&#39;s vrf id, &lt; announce-nh-self
Origin codes:  i - IGP, e - EGP, ? - incomplete

   Network          Next Hop            Metric LocPrf Weight Path
*&gt; 10.15.30.0/24    0.0.0.0                  0         32768 ?
*&gt; 10.100.105.0/24  0.0.0.0                  0         32768 ?
*&gt; 10.100.205.0/24  0.0.0.0                  0         32768 ?
*&gt; 192.0.2.1/32     10.100.105.254           0             0 65430 i
*&gt; 192.0.2.105/32   0.0.0.0                  0         32768 ?
*&gt; 192.0.2.205/32   10.100.205.253         100         32768 ?
*&gt; 192.168.30.0/24  0.0.0.0                  0         32768 ?
*&gt; 192.168.122.0/24 0.0.0.0                  0         32768 ?

! Ping the IPv4 netsvr Loopback (192.0.2.1)
vyos@vyos-01:~$ ping 192.0.2.1
PING 192.0.2.1 (192.0.2.1) 56(84) bytes of data.
64 bytes from 192.0.2.1: icmp_seq=1 ttl=64 time=0.585 ms
64 bytes from 192.0.2.1: icmp_seq=2 ttl=64 time=0.473 ms
^C
--- 192.0.2.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 25ms
rtt min/avg/max/mdev = 0.473/0.529/0.585/0.056 ms

! Show IPv6 BGP neighbours
vyos@vyos-01:~$ show ipv6 bgp summary

IPv6 Unicast Summary:
BGP router identifier 192.0.2.105, local AS number 65105 vrf-id 0
BGP table version 2
RIB entries 3, using 552 bytes of memory
Peers 2, using 41 KiB of memory

Neighbor             V         AS MsgRcvd MsgSent   TblVer  InQ OutQ  Up/Down State/PfxRcd
2001:db8:105::ffff   4      65430      66      65        0    0    0 01:00:11            1
2001:db8:905:beef::2 4      65105      62      63        0    0    0 00:59:28            0

Total number of neighbors 2

! Show IPv6 BGP routes
vyos@vyos-01:~$ show ipv6 bgp
BGP table version is 2, local router ID is 192.0.2.105, vrf id 0
Default local pref 100, local AS 65105
Status codes:  s suppressed, d damped, h history, * valid, &gt; best, = multipath,
               i internal, r RIB-failure, S Stale, R Removed
Nexthop codes: @NNN nexthop&#39;s vrf id, &lt; announce-nh-self
Origin codes:  i - IGP, e - EGP, ? - incomplete

   Network          Next Hop            Metric LocPrf Weight Path
*&gt; 2001:db8:905:beef::2/128
                    fe80::5054:ff:fe50:4198
                                           101         32768 ?
*&gt; 2001:db8:999:beef::1/128
                    fe80::5fe6:3105:856b:b294
                                             0             0 65430 i

Displayed  2 routes and 2 total paths

! Ping the IPv6 netsvr Loopback (2001:DB8:999:BEEF::1)
vyos@vyos-01:~$ ping 2001:DB8:999:BEEF::1
PING 2001:DB8:999:BEEF::1(2001:db8:999:beef::1) 56 data bytes
64 bytes from 2001:db8:999:beef::1: icmp_seq=1 ttl=64 time=0.454 ms
64 bytes from 2001:db8:999:beef::1: icmp_seq=2 ttl=64 time=0.874 ms
^C
--- 2001:DB8:999:BEEF::1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 33ms
rtt min/avg/max/mdev = 0.454/0.664/0.874/0.210 ms
</code></pre></div><p><strong>vyos-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show IPv4 BGP neighbours
vyos@vyos-02:~$ show ip bgp summary

IPv4 Unicast Summary:
BGP router identifier 192.0.2.205, local AS number 65105 vrf-id 0
BGP table version 3
RIB entries 3, using 552 bytes of memory
Peers 1, using 20 KiB of memory

Neighbor        V         AS MsgRcvd MsgSent   TblVer  InQ OutQ  Up/Down State/PfxRcd
192.0.2.105     4      65105      67      64        0    0    0 01:01:51            3

Total number of neighbors 1

! Show IPv4 BGP routes
vyos@vyos-02:~$ show ip bgp
BGP table version is 3, local router ID is 192.0.2.205, vrf id 0
Default local pref 100, local AS 65105
Status codes:  s suppressed, d damped, h history, * valid, &gt; best, = multipath,
               i internal, r RIB-failure, S Stale, R Removed
Nexthop codes: @NNN nexthop&#39;s vrf id, &lt; announce-nh-self
Origin codes:  i - IGP, e - EGP, ? - incomplete

   Network          Next Hop            Metric LocPrf Weight Path
*&gt;i0.0.0.0/0        192.0.2.105                   100      0 i
*&gt;i10.100.105.0/24  192.0.2.105              0    100      0 ?
*&gt;i192.0.2.1/32     10.100.105.254           0    100      0 65430 i

Displayed  3 routes and 3 total paths

! Ping the IPv4 netsvr Loopback (192.0.2.1)
vyos@vyos-02:~$ ping 192.0.2.1
PING 192.0.2.1 (192.0.2.1) 56(84) bytes of data.
64 bytes from 192.0.2.1: icmp_seq=1 ttl=63 time=0.797 ms
64 bytes from 192.0.2.1: icmp_seq=2 ttl=63 time=1.20 ms
64 bytes from 192.0.2.1: icmp_seq=3 ttl=63 time=1.53 ms
^C
--- 192.0.2.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 9ms
rtt min/avg/max/mdev = 0.797/1.175/1.525/0.299 ms

! Show IPv6 BGP neighbours
vyos@vyos-02:~$ show ipv6 bgp summary

IPv6 Unicast Summary:
BGP router identifier 192.0.2.205, local AS number 65105 vrf-id 0
BGP table version 1
RIB entries 1, using 184 bytes of memory
Peers 1, using 20 KiB of memory

Neighbor             V         AS MsgRcvd MsgSent   TblVer  InQ OutQ  Up/Down State/PfxRcd
2001:db8:905:beef::1 4      65105      66      65        0    0    0 01:02:36            1

Total number of neighbors 1

! Show IPv6 BGP routes
vyos@vyos-02:~$ show ipv6 bgp
BGP table version is 1, local router ID is 192.0.2.205, vrf id 0
Default local pref 100, local AS 65105
Status codes:  s suppressed, d damped, h history, * valid, &gt; best, = multipath,
               i internal, r RIB-failure, S Stale, R Removed
Nexthop codes: @NNN nexthop&#39;s vrf id, &lt; announce-nh-self
Origin codes:  i - IGP, e - EGP, ? - incomplete

   Network          Next Hop            Metric LocPrf Weight Path
*&gt;i2001:db8:999:beef::1/128
                    2001:db8:105::ffff
                                             0    100      0 65430 i

Displayed  1 routes and 1 total paths

! Ping the IPv6 netsvr Loopback (2001:DB8:999:BEEF::1)
vyos@vyos-02:~$ ping 2001:db8:999:beef::1 interface 2001:db8:905:beef::2
PING 2001:db8:999:beef::1(2001:db8:999:beef::1) from 2001:db8:905:beef::2 : 56 data bytes
64 bytes from 2001:db8:999:beef::1: icmp_seq=1 ttl=63 time=0.815 ms
64 bytes from 2001:db8:999:beef::1: icmp_seq=2 ttl=63 time=1.09 ms
64 bytes from 2001:db8:999:beef::1: icmp_seq=3 ttl=63 time=1.62 ms
^C
--- 2001:db8:999:beef::1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 29ms
rtt min/avg/max/mdev = 0.815/1.177/1.624/0.336 ms
</code></pre></div><p>All looking good!</p>
<h3 id="snmp">SNMP</h3>
<p>In this section we enable SNMP so that we can monitor the routers. Again, no native Ansible module exists, so we use the <code>vyos_config</code> module.</p>
<h4 id="playbook-3">Playbook</h4>
<p>The contents of the playbook are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e"># tasks file for snmp</span>
- <span style="color:#66d9ef">name</span>: Enable SNMPv3
  <span style="color:#66d9ef">vyos_config</span>:
    <span style="color:#66d9ef">src</span>: snmpv3.j2
  <span style="color:#66d9ef">tags</span>:
    - snmp
</code></pre></div><h4 id="template">Template</h4>
<p>The template for this module looks like the below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">delete service snmp
set service snmp contact &quot;{{ snmp['contact'] }}&quot;
set service snmp location &quot;{{ snmp['location'] }}&quot;
set service snmp v3 engineid '000000000000000000000002'
set service snmp v3 group default mode 'ro'
set service snmp v3 group default view 'default'
set service snmp v3 user {{ snmp['user'] }} auth plaintext-password {{ snmp['auth_key'] }}
set service snmp v3 user {{ snmp['user'] }} auth type 'sha'
set service snmp v3 user {{ snmp['user'] }} group 'default'
set service snmp v3 user {{ snmp['user'] }} privacy plaintext-password {{ snmp['priv_key'] }}
set service snmp v3 user {{ snmp['user'] }} privacy type 'aes'
set service snmp v3 view default oid 1
</code></pre><p>We don&rsquo;t need to use any loops for this, so the template is almost identical to the generated configuration (other than some variables that will be replaced). If you wanted more than one SNMPv3 user and/or group, then you would need to add loops to this.</p>
<p>In a production scenario, you would want to randomly generate the engine IDs so that they are not identical on every router, but for the purposes of this lab it demonstrates the functionality.</p>
<p>We use <code>group_vars</code> for this, rather than <code>host_vars</code>, as the same SNMPv3 user is used on both the <strong>edge</strong> router and the <strong>internal</strong> router</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">snmp</span>:
  <span style="color:#66d9ef">location</span>: Yeti Home
  <span style="color:#66d9ef">contact</span>: The Hairy One
  <span style="color:#66d9ef">user</span>: yetiops
  <span style="color:#66d9ef">group</span>: yetiops_group
  <span style="color:#66d9ef">auth_key</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">            $ANSIBLE_VAULT;1.1;AES256</span>
            <span style="color:#ae81ff">383</span><span style="color:#75715e">###REDACTED###############################################################566</span>
            <span style="color:#ae81ff">363</span><span style="color:#75715e">###REDACTED###############################################################366</span>
            <span style="color:#ae81ff">343</span><span style="color:#75715e">###REDACTED###############################################################565</span>
            <span style="color:#ae81ff">326</span><span style="color:#75715e">###REDACTED###############################################################362</span>
            <span style="color:#ae81ff">3431</span>
  <span style="color:#66d9ef">priv_key</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">            $ANSIBLE_VAULT;1.1;AES256</span>
            <span style="color:#ae81ff">383</span><span style="color:#75715e">###REDACTED###############################################################566</span>
            <span style="color:#ae81ff">363</span><span style="color:#75715e">###REDACTED###############################################################366</span>
            <span style="color:#ae81ff">343</span><span style="color:#75715e">###REDACTED###############################################################565</span>
            <span style="color:#ae81ff">326</span><span style="color:#75715e">###REDACTED###############################################################362</span>
            <span style="color:#ae81ff">3431</span>
</code></pre></div><p>We use Ansible Vault again, so that we can store our credentials in version control, without storing them unencrypted.</p>
<p>The generated configuration looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set service snmp contact &#39;The Hairy One&#39;
set service snmp location &#39;Yeti Home&#39;
set service snmp v3 engineid &#39;000000000000000000000002&#39;
set service snmp v3 group default mode &#39;ro&#39;
set service snmp v3 group default view &#39;default&#39;
set service snmp v3 user yetiops auth encrypted-password &#39;###PASSWORD###&#39;
set service snmp v3 user yetiops auth type &#39;sha&#39;
set service snmp v3 user yetiops group &#39;default&#39;
set service snmp v3 user yetiops privacy encrypted-password &#39;###PASSWORD###&#39;
set service snmp v3 user yetiops privacy type &#39;aes&#39;
set service snmp v3 view default oid 1
</code></pre></div><h4 id="verification-6">Verification</h4>
<p>To check whether this is working, you will need either some form of monitoring system, or you can use something like <code>snmpwalk</code> to check: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! snmpwalk to vyos-01
$ snmpwalk -v3 -u yetiops -a SHA -A ###AUTH-KEY### -x AES -X ###PRIV-KEY### -l authPriv 10.15.30.63 
iso.3.6.1.2.1.1.1.0 = STRING: &#34;VyOS 1.3-rolling-202008020117&#34;
iso.3.6.1.2.1.1.2.0 = OID: iso.3.6.1.4.1.44641
iso.3.6.1.2.1.1.3.0 = Timeticks: (19787) 0:03:17.87
iso.3.6.1.2.1.1.4.0 = STRING: &#34;The Hairy One&#34;
iso.3.6.1.2.1.1.5.0 = STRING: &#34;vyos-01&#34;
iso.3.6.1.2.1.1.6.0 = STRING: &#34;Yeti Home&#34;
iso.3.6.1.2.1.1.7.0 = INTEGER: 14
iso.3.6.1.2.1.1.8.0 = Timeticks: (1) 0:00:00.01
iso.3.6.1.2.1.1.9.1.2.1 = OID: iso.3.6.1.6.3.11.3.1.1
iso.3.6.1.2.1.1.9.1.2.2 = OID: iso.3.6.1.6.3.15.2.1.1
iso.3.6.1.2.1.1.9.1.2.3 = OID: iso.3.6.1.6.3.10.3.1.1
iso.3.6.1.2.1.1.9.1.2.4 = OID: iso.3.6.1.6.3.1
iso.3.6.1.2.1.1.9.1.2.5 = OID: iso.3.6.1.6.3.16.2.2.1
iso.3.6.1.2.1.1.9.1.2.6 = OID: iso.3.6.1.2.1.49

! snmpwalk to vyos-02
$ snmpwalk -v3 -u yetiops -a SHA -A ###AUTH-KEY### -x AES -X ###PRIV-KEY### -l authPriv 10.15.30.64
iso.3.6.1.2.1.1.1.0 = STRING: &#34;VyOS 1.3-rolling-202008020117&#34;
iso.3.6.1.2.1.1.2.0 = OID: iso.3.6.1.4.1.44641
iso.3.6.1.2.1.1.3.0 = Timeticks: (618) 0:00:06.18
iso.3.6.1.2.1.1.4.0 = STRING: &#34;The Hairy One&#34;
iso.3.6.1.2.1.1.5.0 = STRING: &#34;vyos-02&#34;
iso.3.6.1.2.1.1.6.0 = STRING: &#34;Yeti Home&#34;
iso.3.6.1.2.1.1.7.0 = INTEGER: 14
iso.3.6.1.2.1.1.8.0 = Timeticks: (0) 0:00:00.00
iso.3.6.1.2.1.1.9.1.2.1 = OID: iso.3.6.1.6.3.11.3.1.1
iso.3.6.1.2.1.1.9.1.2.2 = OID: iso.3.6.1.6.3.15.2.1.1
iso.3.6.1.2.1.1.9.1.2.3 = OID: iso.3.6.1.6.3.10.3.1.1
iso.3.6.1.2.1.1.9.1.2.4 = OID: iso.3.6.1.6.3.1
iso.3.6.1.2.1.1.9.1.2.5 = OID: iso.3.6.1.6.3.16.2.2.1
iso.3.6.1.2.1.1.9.1.2.6 = OID: iso.3.6.1.2.1.49
iso.3.6.1.2.1.1.9.1.2.7 = OID: iso.3.6.1.2.1.4
iso.3.6.1.2.1.1.9.1.2.8 = OID: iso.3.6.1.2.1.50
iso.3.6.1.2.1.1.9.1.2.9 = OID: iso.3.6.1.6.3.13.3.1.3
iso.3.6.1.2.1.1.9.1.2.10 = OID: iso.3.6.1.2.1.92
iso.3.6.1.2.1.1.9.1.3.1 = STRING: &#34;The MIB for Message Processing and Dispatching.&#34;
iso.3.6.1.2.1.1.9.1.3.2 = STRING: &#34;The management information definitions for the SNMP User-based Security Model.&#34;
</code></pre></div><h3 id="nat">NAT</h3>
<p>In this section we are allowing the <strong>internal</strong> router to reach the internet via the <strong>edge</strong> router. The <strong>edge</strong> router has a default route to the internet that is learned from DHCP.</p>
<h4 id="playbook-4">Playbook</h4>
<p>The playbook looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for nat</span>
- <span style="color:#66d9ef">name</span>: Apply NAT Overload
  <span style="color:#66d9ef">vyos_config</span>:
    <span style="color:#66d9ef">src</span>: nat-overload.j2
  <span style="color:#66d9ef">tags</span>:
  - nat
</code></pre></div><p>Again, we are using <code>vyos_config</code> for this, as no Ansible module exists for NAT on VyOS.</p>
<h4 id="template-1">Template</h4>
<p>The template looks like the below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% if 'edge' in rtr_role %}
delete nat
{% for interface in interfaces %}
{% if 'nat' in interface %}
{% if 'outside' in interface['nat']['role'] %}
set nat source rule 100 source address '0.0.0.0/0'
set nat source rule 100 outbound-interface '{{ interface['vyos_if'] }}'
set nat source rule 100 translation address 'masquerade'
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
</code></pre><p>The above template removes all the existing NAT configuration so that we start fresh on every apply. This NAT rule allows any traffic leaving via a certain interface (the interface learning a default route from DHCP) to be subject to source NAT.</p>
<p>Our <code>host_vars</code> for this looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth0&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To the Internet&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;dhcp&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">nat</span>:
      <span style="color:#66d9ef">role</span>: <span style="color:#e6db74">&#34;outside&#34;</span>
</code></pre></div><p>This then generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">set nat source rule <span style="color:#ae81ff">100</span> outbound-interface <span style="color:#e6db74">&#39;eth0&#39;</span>
set nat source rule <span style="color:#ae81ff">100</span> source address <span style="color:#e6db74">&#39;0.0.0.0/0&#39;</span>
set nat source rule <span style="color:#ae81ff">100</span> translation address <span style="color:#e6db74">&#39;masquerade&#39;</span>
</code></pre></div><h4 id="verification-7">Verification</h4>
<p>We can now test from the <strong>internal</strong> router, to see if it can reach the internet: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Can we reach the internet?
vyos@vyos-02:~$ ping 1.1.1.1
PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data.
64 bytes from 1.1.1.1: icmp_seq=1 ttl=55 time=15.10 ms
64 bytes from 1.1.1.1: icmp_seq=2 ttl=55 time=16.7 ms
64 bytes from 1.1.1.1: icmp_seq=3 ttl=55 time=17.1 ms
^C
--- 1.1.1.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 5ms
rtt min/avg/max/mdev = 15.967/16.601/17.126/0.502 ms
 
vyos@vyos-02:~$ ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=114 time=19.7 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=114 time=21.7 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=114 time=29.4 ms
^C
--- 8.8.8.8 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 6ms
rtt min/avg/max/mdev = 19.714/23.600/29.388/4.173 ms

! What does this look like on the edge router?
vyos@vyos-01:~$ show nat source statistics

rule      pkts        bytes   interface
----      ----        -----   ---------
100          2          168   eth0
</code></pre></div><p>All looking good!</p>
<h3 id="aaa">AAA</h3>
<p>The final task is AAA (<strong>A</strong>uthentication, <strong>A</strong>uthorization and <strong>A</strong>ccounting). Like MikroTik, VyOS does not support TACACS+ so we configure the routers to authenticate against <strong>freeradius</strong> running on <strong>netsvr-01</strong>: -</p>
<h4 id="playbook-5">Playbook</h4>
<p>The contents of the playbook are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e"># tasks file for aaa</span>
- <span style="color:#66d9ef">name</span>: Enable RADIUS
  <span style="color:#66d9ef">vyos_config</span>:
    <span style="color:#66d9ef">src</span>: radius.j2
  <span style="color:#66d9ef">tags</span>:
  - aaa
</code></pre></div><p>This uses <code>vyos_config</code> with a Jinja2 template to configure AAA.</p>
<h4 id="template-2">Template</h4>
<p>The contents of the template are: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">set system login radius server {{ tacacs['ipv4'] }} key {{ radius['secret'] }}
set system login radius source-address {{ router_id }}
</code></pre><p>As with the MikroTik RADIUS configuration, we use the <code>tacacs</code> IPv4 address because at some point VyOS may support TACACS+, thus making transitioning between the two easier.</p>
<p>The relevant <code>host_vars</code> are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">router_id</span>: <span style="color:#ae81ff">192.0.2.105</span>
<span style="color:#66d9ef">radius</span>:
  <span style="color:#66d9ef">secret</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">          $ANSIBLE_VAULT;1.1;AES256</span>
          <span style="color:#ae81ff">356431</span><span style="color:#75715e">################REDACTED###########################31313136626333</span>
          <span style="color:#ae81ff">623664</span><span style="color:#75715e">################REDACTED###########################65366437633463</span>
          <span style="color:#ae81ff">623135</span><span style="color:#75715e">################REDACTED###########################33346233346665</span>
          <span style="color:#ae81ff">633265</span><span style="color:#75715e">################REDACTED###########################63333834396361</span>
          <span style="color:#ae81ff">333835</span><span style="color:#75715e">################REDACTED###########################13936</span>
</code></pre></div><p>The rest of our variables come from our <code>group_vars</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">tacacs</span>:
  <span style="color:#66d9ef">ipv4</span>: <span style="color:#ae81ff">192.0.2.1</span>
</code></pre></div><p>The above generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set system login radius server 192.0.2.1 key &#39;###RADIUS_SECRET###&#39;
set system login radius source-address &#39;192.0.2.105&#39;
</code></pre></div><h4 id="verification-8">Verification</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Can we login with the yetiops user?
$ ssh yetiops@10.15.30.64
----------------------------------------
|
| This banner was generated by Ansible
|
----------------------------------------
|
| You are logged into vyos-02
|
----------------------------------------
|
yetiops@10.15.30.64&#39;s password:
Creating directory &#39;/home/yetiops&#39;.
Linux vyos-02 4.19.136-amd64-vyos #1 SMP Sat Aug 1 08:40:04 UTC 2020 x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.


yetiops@vyos-02:~$

! What about a user that doesn&#39;t exist?
$ ssh jeff@10.15.30.64
----------------------------------------
|
| This banner was generated by Ansible
|
----------------------------------------
|
| You are logged into vyos-02
|
----------------------------------------
|
jeff@10.15.30.64&#39;s password:
Permission denied, please try again.
jeff@10.15.30.64&#39;s password:

vyos@vyos-02:~$ show log | grep -i jeff
Sep 24 18:25:38 vyos-02 sshd[2901]: Failed password for jeff from 10.15.30.253 port 63877 ssh2
Sep 24 18:25:39 vyos-02 sshd[2901]: Connection closed by authenticating user jeff 10.15.30.253 port 63877 [preauth]

! What do see in our radius log?
Thu Sep 24 19:25:36 2020 : Auth: (12) Login incorrect (No Auth-Type found: rejecting the user via Post-Auth-Type = Reject): [jeff/jkslfjlsfd] (from client vyos-02 port 2901 cli 10.15.30.253)

! What if freeradius goes away?
$ sudo systemctl stop radiusd

$ systemctl status radiusd
  radiusd.service - FreeRADIUS high performance RADIUS server.
   Loaded: loaded (/usr/lib/systemd/system/radiusd.service; enabled; vendor preset: disabled)
   Active: inactive (dead) since Thu 2020-09-24 19:28:38 BST; 14s ago
  Process: 1824 ExecStart=/usr/sbin/radiusd -d /etc/raddb (code=exited, status=0/SUCCESS)
  Process: 1539 ExecStartPre=/usr/sbin/radiusd -C (code=exited, status=0/SUCCESS)
  Process: 1528 ExecStartPre=/bin/chown -R radiusd.radiusd /var/run/radiusd (code=exited, status=0/SUCCESS)
 Main PID: 1826 (code=exited, status=0/SUCCESS)

Sep 24 19:13:09 netsvr-01 systemd[1]: Starting FreeRADIUS high performance RADIUS server....
Sep 24 19:13:13 netsvr-01 systemd[1]: Started FreeRADIUS high performance RADIUS server..
Sep 24 19:28:38 netsvr-01 systemd[1]: Stopping FreeRADIUS high performance RADIUS server....
Sep 24 19:28:38 netsvr-01 systemd[1]: Stopped FreeRADIUS high performance RADIUS server..

$ ssh yetiops@10.15.31.64
----------------------------------------
|
| This banner was generated by Ansible
|
----------------------------------------
|
| You are logged into vyos-02
|
----------------------------------------
|
yetiops@10.15.30.64&#39;s password:
Permission denied, please try again.
</code></pre></div><p>Success!</p>
<h3 id="parent-playbook">Parent playbook</h3>
<p>The parent playbook (i.e. the playbook that brings all the roles together) is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
- <span style="color:#66d9ef">hosts</span>: vyos
  <span style="color:#66d9ef">gather_facts</span>: <span style="color:#66d9ef">false</span>
  <span style="color:#66d9ef">tasks</span>:
    - <span style="color:#66d9ef">import_role</span>:
        <span style="color:#66d9ef">name</span>: system
    - <span style="color:#66d9ef">import_role</span>:
        <span style="color:#66d9ef">name</span>: interfaces
    - <span style="color:#66d9ef">import_role</span>:
        <span style="color:#66d9ef">name</span>: firewall
    - <span style="color:#66d9ef">import_role</span>:
        <span style="color:#66d9ef">name</span>: routing
    - <span style="color:#66d9ef">import_role</span>:
        <span style="color:#66d9ef">name</span>: snmp
    - <span style="color:#66d9ef">import_role</span>:
        <span style="color:#66d9ef">name</span>: nat
    - <span style="color:#66d9ef">import_role</span>:
        <span style="color:#66d9ef">name</span>: aaa
    - <span style="color:#66d9ef">name</span>: Save configuration
      <span style="color:#66d9ef">vyos_config</span>:
        <span style="color:#66d9ef">save</span>: <span style="color:#66d9ef">true</span>
</code></pre></div><p>Like IOS and EOS, we need to save the configuration at the end.</p>
<p>Any changes made before this will be committed to the running/in-memory configuration, but will not be saved to disk until the final <strong>Save configuration</strong> task.</p>
<p>This can be a little confusing if you are familiar with JunOS (and IOS-XR) as the commit operation makes configuration persist after a reboot, whereas in VyOS it does not. If anything the commit operation in VyOS is used so you can stage your configuration changes, and then apply them all at once (rather than instantly applying each change like in Cisco IOS).</p>
<h4 id="role-order">Role Order</h4>
<p>The order the roles are applied is identical to JunOS. The justification for the role order is detailed within <a href="/posts/ansible-for-networking-part-3-cisco-ios">Part 3 - Cisco IOS</a>. To summarize: -</p>
<ul>
<li><code>system</code> - Sets up logging, hostnames and banners
<ul>
<li>This ensure we have logging ready for if any of the other roles fail (that the Ansible debug output cannot help with)</li>
</ul>
</li>
<li><code>interfaces</code> - This is a prerequisite for most of the following tasks</li>
<li><code>firewall</code> - Apply before routing so that the device is not open to the world when publicly routable</li>
<li><code>routing</code> - Routing is required for NAT and AAA to function</li>
<li><code>snmp</code> - No dependency on any service, so this can go anywhere</li>
<li><code>nat</code> - Apply this after routing, otherwise the <strong>internal</strong> router has no default route to reach external destinations anyway</li>
<li><code>aaa</code> - It depends upon routing, and if configured incorrectly it can break login sessions</li>
</ul>
<h2 id="artifacts">Artifacts</h2>
<p>The final directory structure looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">$ tree -L 2
.
├── ansible.cfg
├── ansible.log
├── group_vars
│   └── vyos
├── host_vars
│   ├── vyos-01.yml
│   └── vyos-02.yml
├── inventory
├── roles
│   ├── aaa
│   ├── firewall
│   ├── interfaces
│   ├── nat
│   ├── routing
│   ├── snmp
│   └── system
└── vyos.yaml

10 directories, 7 files
</code></pre></div><p>The final contents of our <code>group_vars</code> are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">ansible_connection</span>: network_cli
<span style="color:#66d9ef">ansible_network_os</span>: vyos
<span style="color:#66d9ef">ansible_user</span>: vyos
<span style="color:#66d9ef">log_host</span>: <span style="color:#ae81ff">10.100.105.254</span>
<span style="color:#66d9ef">tacacs</span>:
  <span style="color:#66d9ef">ipv4</span>: <span style="color:#ae81ff">192.0.2.1</span>
  <span style="color:#66d9ef">secret</span>: supersecret
<span style="color:#66d9ef">snmp</span>:
  <span style="color:#66d9ef">location</span>: Yeti Home
  <span style="color:#66d9ef">contact</span>: The Hairy One
  <span style="color:#66d9ef">user</span>: yetiops
  <span style="color:#66d9ef">group</span>: yetiops_group
  <span style="color:#66d9ef">auth_key</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">            $ANSIBLE_VAULT;1.1;AES256</span>
            <span style="color:#ae81ff">383</span><span style="color:#75715e">###REDACTED###############################################################566</span>
            <span style="color:#ae81ff">363</span><span style="color:#75715e">###REDACTED###############################################################366</span>
            <span style="color:#ae81ff">343</span><span style="color:#75715e">###REDACTED###############################################################565</span>
            <span style="color:#ae81ff">326</span><span style="color:#75715e">###REDACTED###############################################################362</span>
            <span style="color:#ae81ff">3431</span>
  <span style="color:#66d9ef">priv_key</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">            $ANSIBLE_VAULT;1.1;AES256</span>
            <span style="color:#ae81ff">386</span><span style="color:#75715e">###REDACTED###############################################################764</span>
            <span style="color:#ae81ff">613</span><span style="color:#75715e">###REDACTED###############################################################630</span>
            <span style="color:#ae81ff">646</span><span style="color:#75715e">###REDACTED###############################################################331</span>
            <span style="color:#ae81ff">376</span><span style="color:#75715e">###REDACTED###############################################################137</span>
            <span style="color:#ae81ff">3563</span>
</code></pre></div><p>The final contents of our <code>host_vars</code> are: -</p>
<p><strong>vyos-01.yaml</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">router_id</span>: <span style="color:#ae81ff">192.0.2.105</span>
<span style="color:#66d9ef">rtr_role</span>: edge
<span style="color:#66d9ef">fw_addresses</span>:
  - <span style="color:#66d9ef">name</span>: netsvr
    <span style="color:#66d9ef">ip</span>: <span style="color:#ae81ff">10.100.105.254</span>
    <span style="color:#66d9ef">groups</span>:
      - external-bgp-peers-v4
      - netsvr-direct-v4
  - <span style="color:#66d9ef">name</span>: netsvr-v6
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:105::ffff&#34;</span>
    <span style="color:#66d9ef">groups</span>:
      - external-bgp-peers-v6
      - netsvr-direct-v6
  - <span style="color:#66d9ef">name</span>: netsvr-lo
    <span style="color:#66d9ef">ip</span>: <span style="color:#ae81ff">192.0.2.1</span>
    <span style="color:#66d9ef">groups</span>:
    - netsvr-loop-v4
  - <span style="color:#66d9ef">zone</span>: edge
    <span style="color:#66d9ef">name</span>: netsvr-lo-v6
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:999:beef::1&#34;</span>
    <span style="color:#66d9ef">groups</span>:
    - netsvr-loop-v6
  - <span style="color:#66d9ef">name</span>: internal-rtr
    <span style="color:#66d9ef">ip</span>: <span style="color:#ae81ff">192.0.2.205</span>
    <span style="color:#66d9ef">groups</span>:
    - internal-bgp-peers-v4
    - internal-rtr-loop-v4
  - <span style="color:#66d9ef">name</span>: internal-rtr-v6
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:905:beef::2&#34;</span>
    <span style="color:#66d9ef">groups</span>:
    - internal-bgp-peers-v6
    - internal-rtr-loop-v6
<span style="color:#66d9ef">fw_policies</span>:
  <span style="color:#66d9ef">mgmt</span>:
    <span style="color:#66d9ef">ipv4</span>:
      - <span style="color:#66d9ef">name</span>: mgmt_to_local_ipv4
        <span style="color:#66d9ef">zones</span>:
          <span style="color:#66d9ef">from</span>: mgmt
          <span style="color:#66d9ef">to</span>: local
        <span style="color:#66d9ef">rules</span>:
          - <span style="color:#66d9ef">protocol</span>: tcp
            <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">22</span>
            <span style="color:#66d9ef">action</span>: accept
          - <span style="color:#66d9ef">protocol</span>: udp
            <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">161</span>
            <span style="color:#66d9ef">action</span>: accept
  <span style="color:#66d9ef">ipv4</span>:
    - <span style="color:#66d9ef">name</span>: external_to_local_ipv4
      <span style="color:#66d9ef">zones</span>:
        <span style="color:#66d9ef">from</span>: external
        <span style="color:#66d9ef">to</span>: local
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">source_groups</span>: external-bgp-peers-v4
          <span style="color:#66d9ef">protocol</span>: tcp
          <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">179</span>
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: icmp
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: all
          <span style="color:#66d9ef">action</span>: reject
    - <span style="color:#66d9ef">name</span>: internal_to_local_ipv4
      <span style="color:#66d9ef">zones</span>:
        <span style="color:#66d9ef">from</span>: internal
        <span style="color:#66d9ef">to</span>: local
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">source_groups</span>: internal-bgp-peers-v4
          <span style="color:#66d9ef">protocol</span>: tcp
          <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">179</span>
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: ospf
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: icmp
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: all
          <span style="color:#66d9ef">action</span>: reject
    - <span style="color:#66d9ef">name</span>: external_to_internal_ipv4
      <span style="color:#66d9ef">zones</span>:
        <span style="color:#66d9ef">from</span>: external
        <span style="color:#66d9ef">to</span>: internal
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">protocol</span>: icmp
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: all
          <span style="color:#66d9ef">action</span>: reject
    - <span style="color:#66d9ef">name</span>: local_to_external_ipv4
      <span style="color:#66d9ef">zones</span>:
        <span style="color:#66d9ef">from</span>: local
        <span style="color:#66d9ef">to</span>: external
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">dest_groups</span>: external-bgp-peers-v4
          <span style="color:#66d9ef">protocol</span>: tcp
          <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">179</span>
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">dest_groups</span>: netsvr-direct-v4
          <span style="color:#66d9ef">protocol</span>: udp
          <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">514</span>
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">dest_groups</span>: netsvr-loop-v4
          <span style="color:#66d9ef">protocol</span>: tcp_udp
          <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">1812-1813</span>
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: icmp
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: all
          <span style="color:#66d9ef">action</span>: reject
    - <span style="color:#66d9ef">name</span>: local_to_internal_ipv4
      <span style="color:#66d9ef">zones</span>:
        <span style="color:#66d9ef">from</span>: local
        <span style="color:#66d9ef">to</span>: internal
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">dest_groups</span>: internal-bgp-peers-v4
          <span style="color:#66d9ef">protocol</span>: tcp
          <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">179</span>
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: ospf
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: icmp
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: all
          <span style="color:#66d9ef">action</span>: reject
    - <span style="color:#66d9ef">name</span>: internal_to_external_ipv4
      <span style="color:#66d9ef">zones</span>:
        <span style="color:#66d9ef">from</span>: internal
        <span style="color:#66d9ef">to</span>: external
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">dest_groups</span>: netsvr-direct-v4
          <span style="color:#66d9ef">protocol</span>: udp
          <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">514</span>
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">dest_groups</span>: netsvr-loop-v4
          <span style="color:#66d9ef">protocol</span>: tcp_udp
          <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">1812-1813</span>
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: icmp
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: all
          <span style="color:#66d9ef">action</span>: reject
  <span style="color:#66d9ef">ipv6</span>:
    - <span style="color:#66d9ef">name</span>: external_to_local_ipv6
      <span style="color:#66d9ef">zones</span>:
        <span style="color:#66d9ef">from</span>: external
        <span style="color:#66d9ef">to</span>: local
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">source_groups</span>: external-bgp-peers-v6
          <span style="color:#66d9ef">protocol</span>: tcp
          <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">179</span>
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: icmpv6
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: all
          <span style="color:#66d9ef">action</span>: reject
    - <span style="color:#66d9ef">name</span>: internal_to_local_ipv6
      <span style="color:#66d9ef">zones</span>:
        <span style="color:#66d9ef">from</span>: internal
        <span style="color:#66d9ef">to</span>: local
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">source_groups</span>: internal-bgp-peers-v6
          <span style="color:#66d9ef">protocol</span>: tcp
          <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">179</span>
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: ospf
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: icmpv6
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: all
          <span style="color:#66d9ef">action</span>: reject
    - <span style="color:#66d9ef">name</span>: external_to_internal_ipv6
      <span style="color:#66d9ef">zones</span>:
        <span style="color:#66d9ef">from</span>: external
        <span style="color:#66d9ef">to</span>: internal
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">protocol</span>: icmpv6
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: all
          <span style="color:#66d9ef">action</span>: reject
    - <span style="color:#66d9ef">name</span>: local_to_external_ipv6
      <span style="color:#66d9ef">zones</span>:
        <span style="color:#66d9ef">from</span>: local
        <span style="color:#66d9ef">to</span>: external
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">dest_groups</span>: external-bgp-peers-v6
          <span style="color:#66d9ef">protocol</span>: tcp
          <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">179</span>
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: ospf
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: icmpv6
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: all
          <span style="color:#66d9ef">action</span>: reject
    - <span style="color:#66d9ef">name</span>: local_to_internal_ipv6
      <span style="color:#66d9ef">zones</span>:
        <span style="color:#66d9ef">from</span>: local
        <span style="color:#66d9ef">to</span>: internal
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">dest_groups</span>: internal-bgp-peers-v6
          <span style="color:#66d9ef">protocol</span>: tcp
          <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">179</span>
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: ospf
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: icmpv6
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: all
          <span style="color:#66d9ef">action</span>: reject
    - <span style="color:#66d9ef">name</span>: internal_to_external_ipv6
      <span style="color:#66d9ef">zones</span>:
        <span style="color:#66d9ef">from</span>: internal
        <span style="color:#66d9ef">to</span>: external
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">protocol</span>: icmpv6
          <span style="color:#66d9ef">action</span>: accept
        - <span style="color:#66d9ef">protocol</span>: all
          <span style="color:#66d9ef">action</span>: reject
<span style="color:#66d9ef">route_maps</span>:
  <span style="color:#66d9ef">prefix_lists</span>:
    <span style="color:#66d9ef">ipv4</span>:
      - <span style="color:#66d9ef">name</span>: internal-nets-v4
        <span style="color:#66d9ef">addresses</span>:
           - <span style="color:#ae81ff">192.0.2.105</span>/<span style="color:#ae81ff">32</span>
           - <span style="color:#ae81ff">192.0.2.205</span>/<span style="color:#ae81ff">32</span>
           - <span style="color:#ae81ff">10.100.205.0</span>/<span style="color:#ae81ff">24</span>
        <span style="color:#66d9ef">action</span>: permit
      - <span style="color:#66d9ef">name</span>: external-nets-v4
        <span style="color:#66d9ef">addresses</span>:
           - <span style="color:#ae81ff">192.0.2.1</span>/<span style="color:#ae81ff">32</span>
           - <span style="color:#ae81ff">10.100.105.0</span>/<span style="color:#ae81ff">24</span>
        <span style="color:#66d9ef">action</span>: permit
    <span style="color:#66d9ef">ipv6</span>:
      - <span style="color:#66d9ef">name</span>: internal-nets-v6
        <span style="color:#66d9ef">addresses</span>:
           - <span style="color:#e6db74">&#34;2001:db8:905:beef::1/128&#34;</span>
           - <span style="color:#e6db74">&#34;2001:db8:905:beef::2/128&#34;</span>
           - <span style="color:#e6db74">&#34;2001:db8:905::/64&#34;</span>
        <span style="color:#66d9ef">action</span>: permit
      - <span style="color:#66d9ef">name</span>: external-nets-v6
        <span style="color:#66d9ef">addresses</span>:
           - <span style="color:#e6db74">&#34;2001:db8:999:beef::1/128&#34;</span>
        <span style="color:#66d9ef">action</span>: permit
  <span style="color:#66d9ef">rm</span>:
    - <span style="color:#66d9ef">name</span>: external-networks-v4
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">pfx_list</span>: external-nets-v4
          <span style="color:#66d9ef">action</span>: permit
        - <span style="color:#66d9ef">action</span>: deny
    - <span style="color:#66d9ef">name</span>: internal-networks-v4
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">pfx_list</span>: internal-nets-v4
          <span style="color:#66d9ef">action</span>: permit
        - <span style="color:#66d9ef">action</span>: deny
    - <span style="color:#66d9ef">name</span>: external-networks-v6
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">pfx_list6</span>: external-nets-v6
          <span style="color:#66d9ef">action</span>: permit
        - <span style="color:#66d9ef">action</span>: deny
    - <span style="color:#66d9ef">name</span>: internal-networks-v6
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">pfx_list6</span>: internal-nets-v6
          <span style="color:#66d9ef">action</span>: permit
        - <span style="color:#66d9ef">action</span>: deny
<span style="color:#66d9ef">bgp</span>:
  <span style="color:#66d9ef">local_as</span>: <span style="color:#ae81ff">65105</span>
  <span style="color:#66d9ef">redistribute</span>:
    <span style="color:#66d9ef">ospf</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">neighbours</span>:
    <span style="color:#66d9ef">ipv4</span>:
      - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">10.100.105.254</span>
        <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65430</span>
        <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;netsvr-01 IPv4&#34;</span>
        <span style="color:#66d9ef">route_map</span>:
          <span style="color:#66d9ef">in</span>: external-networks-v4
          <span style="color:#66d9ef">out</span>: internal-networks-v4
      - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">192.0.2.205</span>
        <span style="color:#66d9ef">loc_ip</span>: <span style="color:#ae81ff">192.0.2.105</span>
        <span style="color:#66d9ef">default_originate</span>: <span style="color:#66d9ef">true</span>
        <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;vyos-02 IPv4&#34;</span>
        <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65105</span>
        <span style="color:#66d9ef">route_map</span>:
          <span style="color:#66d9ef">out</span>: external-networks-v4
          <span style="color:#66d9ef">in</span>: internal-networks-v4
    <span style="color:#66d9ef">ipv6</span>:
      - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:105::ffff&#34;</span>
        <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65430</span>
        <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;netsvr-01 IPv6&#34;</span>
        <span style="color:#66d9ef">route_map</span>:
          <span style="color:#66d9ef">in</span>: external-networks-v6
          <span style="color:#66d9ef">out</span>: internal-networks-v6
      - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:905:beef::2&#34;</span>
        <span style="color:#66d9ef">loc_ip</span>: <span style="color:#e6db74">&#34;2001:db8:905:beef::1&#34;</span>
        <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;vyos-02 IPv6&#34;</span>
        <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65105</span>
        <span style="color:#66d9ef">route_map</span>:
          <span style="color:#66d9ef">out</span>: external-networks-v6
          <span style="color:#66d9ef">in</span>: internal-networks-v6
<span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth1&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Management&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.15.30.63/24&#34;</span>
    <span style="color:#66d9ef">zone</span>: <span style="color:#e6db74">&#34;mgmt&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth2&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;VLAN Bridge&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth2&#34;</span>
    <span style="color:#66d9ef">vif</span>: <span style="color:#ae81ff">105</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To netsvr&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.100.105.253/24&#34;</span>
    <span style="color:#66d9ef">ipv6_addr</span>: <span style="color:#e6db74">&#34;2001:db8:105::f/64&#34;</span>
    <span style="color:#66d9ef">zone</span>: <span style="color:#e6db74">&#34;external&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth2&#34;</span>
    <span style="color:#66d9ef">vif</span>: <span style="color:#ae81ff">205</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To vyos-02&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">zone</span>: <span style="color:#e6db74">&#34;internal&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.100.205.254/24&#34;</span>
    <span style="color:#66d9ef">ipv6_addr</span>: <span style="color:#e6db74">&#34;2001:db8:205::a/64&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth0&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To the Internet&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;dhcp&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">nat</span>:
      <span style="color:#66d9ef">role</span>: <span style="color:#e6db74">&#34;outside&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;lo&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;192.0.2.105/32&#34;</span>
    <span style="color:#66d9ef">ipv6_addr</span>: <span style="color:#e6db74">&#34;2001:db8:905:beef::1/128&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
<span style="color:#66d9ef">zones</span>:
  - <span style="color:#66d9ef">name</span>: external
    <span style="color:#66d9ef">description</span>: <span style="color:#e6db74">&#34;External facing interfaces&#34;</span>
  - <span style="color:#66d9ef">name</span>: internal
    <span style="color:#66d9ef">description</span>: <span style="color:#e6db74">&#34;Internal facing interfaces&#34;</span>
  - <span style="color:#66d9ef">name</span>: local
    <span style="color:#66d9ef">description</span>: <span style="color:#e6db74">&#34;Local router zone&#34;</span>
    <span style="color:#66d9ef">local</span>: <span style="color:#66d9ef">true</span>
  - <span style="color:#66d9ef">name</span>: mgmt
    <span style="color:#66d9ef">description</span>: <span style="color:#e6db74">&#34;Management zone&#34;</span>
<span style="color:#66d9ef">radius</span>:
  <span style="color:#66d9ef">secret</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">          $ANSIBLE_VAULT;1.1;AES256</span>
          <span style="color:#ae81ff">356</span><span style="color:#75715e">###REDACTED###############################################################33</span>
          <span style="color:#ae81ff">623</span><span style="color:#75715e">###REDACTED###############################################################63</span>
          <span style="color:#ae81ff">623</span><span style="color:#75715e">###REDACTED###############################################################65</span>
          <span style="color:#ae81ff">633</span><span style="color:#75715e">###REDACTED###############################################################61</span>
          <span style="color:#ae81ff">333</span><span style="color:#75715e">###REDACTED###########################################36</span>
</code></pre></div><p><strong>vyos-02.yaml</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">router_id</span>: <span style="color:#ae81ff">192.0.2.205</span>
<span style="color:#66d9ef">rtr_role</span>: internal
<span style="color:#66d9ef">route_maps</span>:
  <span style="color:#66d9ef">prefix_lists</span>:
    <span style="color:#66d9ef">ipv4</span>:
      - <span style="color:#66d9ef">name</span>: internal-nets-v4
        <span style="color:#66d9ef">addresses</span>:
           - <span style="color:#ae81ff">192.0.2.105</span>/<span style="color:#ae81ff">32</span>
           - <span style="color:#ae81ff">192.0.2.205</span>/<span style="color:#ae81ff">32</span>
           - <span style="color:#ae81ff">10.100.205.0</span>/<span style="color:#ae81ff">24</span>
        <span style="color:#66d9ef">action</span>: permit
      - <span style="color:#66d9ef">name</span>: external-nets-v4
        <span style="color:#66d9ef">addresses</span>:
           - <span style="color:#ae81ff">192.0.2.1</span>/<span style="color:#ae81ff">32</span>
           - <span style="color:#ae81ff">10.100.105.0</span>/<span style="color:#ae81ff">24</span>
        <span style="color:#66d9ef">action</span>: permit
      - <span style="color:#66d9ef">name</span>: default-route-v4
        <span style="color:#66d9ef">addresses</span>:
           - <span style="color:#ae81ff">0.0.0.0</span>/<span style="color:#ae81ff">0</span>
        <span style="color:#66d9ef">action</span>: permit
    <span style="color:#66d9ef">ipv6</span>:
      - <span style="color:#66d9ef">name</span>: internal-nets-v6
        <span style="color:#66d9ef">addresses</span>:
           - <span style="color:#e6db74">&#34;2001:db8:905:beef::1/128&#34;</span>
           - <span style="color:#e6db74">&#34;2001:db8:905:beef::2/128&#34;</span>
           - <span style="color:#e6db74">&#34;2001:db8:905::/64&#34;</span>
        <span style="color:#66d9ef">action</span>: permit
      - <span style="color:#66d9ef">name</span>: external-nets-v6
        <span style="color:#66d9ef">addresses</span>:
           - <span style="color:#e6db74">&#34;2001:db8:999:beef::1/128&#34;</span>
        <span style="color:#66d9ef">action</span>: permit
  <span style="color:#66d9ef">rm</span>:
    - <span style="color:#66d9ef">name</span>: external-networks-v4
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">pfx_list</span>: external-nets-v4
          <span style="color:#66d9ef">action</span>: permit
        - <span style="color:#66d9ef">pfx_list</span>: default-route-v4
          <span style="color:#66d9ef">action</span>: permit
        - <span style="color:#66d9ef">action</span>: deny
    - <span style="color:#66d9ef">name</span>: internal-networks-v4
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">pfx_list</span>: internal-nets-v4
          <span style="color:#66d9ef">action</span>: permit
        - <span style="color:#66d9ef">action</span>: deny
    - <span style="color:#66d9ef">name</span>: external-networks-v6
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">pfx_list6</span>: external-nets-v6
          <span style="color:#66d9ef">action</span>: permit
        - <span style="color:#66d9ef">action</span>: deny
    - <span style="color:#66d9ef">name</span>: internal-networks-v6
      <span style="color:#66d9ef">rules</span>:
        - <span style="color:#66d9ef">pfx_list6</span>: internal-nets-v6
          <span style="color:#66d9ef">action</span>: permit
        - <span style="color:#66d9ef">action</span>: deny
<span style="color:#66d9ef">bgp</span>:
  <span style="color:#66d9ef">local_as</span>: <span style="color:#ae81ff">65105</span>
  <span style="color:#66d9ef">neighbours</span>:
    <span style="color:#66d9ef">ipv4</span>:
      - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">192.0.2.105</span>
        <span style="color:#66d9ef">loc_ip</span>: <span style="color:#ae81ff">192.0.2.205</span>
        <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;vyos-02 IPv4&#34;</span>
        <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65105</span>
        <span style="color:#66d9ef">route_map</span>:
          <span style="color:#66d9ef">in</span>: external-networks-v4
          <span style="color:#66d9ef">out</span>: internal-networks-v4
    <span style="color:#66d9ef">ipv6</span>:
      - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:905:beef::1&#34;</span>
        <span style="color:#66d9ef">loc_ip</span>: <span style="color:#e6db74">&#34;2001:db8:905:beef::2&#34;</span>
        <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;vyos-02 IPv6&#34;</span>
        <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65105</span>
        <span style="color:#66d9ef">route_map</span>:
          <span style="color:#66d9ef">in</span>: external-networks-v6
          <span style="color:#66d9ef">out</span>: internal-networks-v6
<span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth1&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Management&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.15.30.34/24&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth2&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;VLAN Bridge&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;eth2&#34;</span>
    <span style="color:#66d9ef">vif</span>: <span style="color:#ae81ff">205</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To vyos-01&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.100.205.253/24&#34;</span>
    <span style="color:#66d9ef">ipv6_addr</span>: <span style="color:#e6db74">&#34;2001:db8:205::f/64&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
  - <span style="color:#66d9ef">vyos_if</span>: <span style="color:#e6db74">&#34;lo&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;192.0.2.205/32&#34;</span>
    <span style="color:#66d9ef">ipv6_addr</span>: <span style="color:#e6db74">&#34;2001:db8:905:beef::2/128&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
<span style="color:#66d9ef">radius</span>:
  <span style="color:#66d9ef">secret</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">          $ANSIBLE_VAULT;1.1;AES256</span>
          <span style="color:#ae81ff">356</span><span style="color:#75715e">###REDACTED###############################################################33</span>
          <span style="color:#ae81ff">623</span><span style="color:#75715e">###REDACTED###############################################################63</span>
          <span style="color:#ae81ff">623</span><span style="color:#75715e">###REDACTED###############################################################65</span>
          <span style="color:#ae81ff">633</span><span style="color:#75715e">###REDACTED###############################################################61</span>
          <span style="color:#ae81ff">333</span><span style="color:#75715e">###REDACTED###########################################36</span>
</code></pre></div><p>As with JunOS, there are quite a few variables, especially for building firewalls. However remember this is all defined in a static configuration file. If you use another system like <a href="https://github.com/netbox-community/netbox">Netbox</a> as a source of truth and data source for Ansible, you can manage many of these variables there instead. This lowers the barrier of entry for network configuration, meaning that those without access to the routers themselves would potentially be able to provision services for customers.</p>
<h2 id="running-the-playbooks">Running the playbooks</h2>
<p>Below is an Asciinema output of my terminal when running the playbooks, so you can see them being applied: -</p>
<script id="asciicast-361595" src="https://asciinema.org/a/361595.js" async></script>
<p>A few tasks are marked as changed, as we have seen previously in the IOS, JunOS and EOS posts. As Ansible is reading the configuration and checking the output matches the input, there are times where what is supplied is not identical to the configuration.</p>
<p>Again, using something like <code>changed_when</code> could tidy this up significantly.</p>
<h2 id="native-modules-versus-vyos_config">Native modules versus <code>vyos_config</code></h2>
<p>Below is a summary of how many different modules are used, and also how many in total were native modules (compared to using <code>vyos_config</code>).</p>
<table>
<thead>
<tr>
<th>Module</th>
<th>Used</th>
</tr>
</thead>
<tbody>
<tr>
<td>vyos_config</td>
<td>21</td>
</tr>
<tr>
<td>vyos_l3_interfaces</td>
<td>4</td>
</tr>
<tr>
<td>vyos_interfaces</td>
<td>2</td>
</tr>
<tr>
<td>vyos_banner</td>
<td>2</td>
</tr>
<tr>
<td>vyos_system</td>
<td>1</td>
</tr>
</tbody>
</table>
<p>Compared to IOS and JunOS, the majority of the tasks use the <code>vyos_config</code> module. As with JunOS, no modules exist for any routing protocols, firewalling or similar.</p>
<p>The time to configure these two VyOS routers is quite small, both complete in under 2 minutes, which is similar to JunOS. If you ran more VyOS routers and used the parallel execution features within Ansible, you can conceivably configure an entire core network, edge router network or VPN concentrator cluster in a similar amount of time. This significantly reduces the time required to make and implement changes.</p>
<h2 id="thoughts-on-vyos">Thoughts on VyOS</h2>
<p>VyOS is an interesting mixture of IOS and JunOS, bringing about many advantages of both.</p>
<p>For those coming from an IOS background (even most Juniper, Nokia or Extreme engineers will have some experience in IOS), most of the verification commands will be familiar to you. Everything from <code>show ip bgp summary</code> to <code>show mpls ldp binding</code> are close to (or the same as) what you would use in IOS.</p>
<p>If you come from a Juniper background, then the configuration approach will be immediately familiar, along with the commit-style approach to staging multiple changes into one &ldquo;apply&rdquo;.</p>
<p>VyOS is in a constant state of improvement, and with the ability to run it on commodity hardware, virtualised, in the cloud or anywhere you see fit, it is an incredible powerful option that I wouldn&rsquo;t hesitate to use and recommend it..</p>
<h2 id="summary">Summary</h2>
<p>Using Ansible with VyOS, or any configuration management solution for your network hardware brings huge benefits over manually managing your core infrastructure. Version controlled configuration changes, parallel execution, configuration consistency and using a source of truth to define the network (rather than network being the source of truth) can reduce mistakes and give more time for engineers to investigate new products, designs and improvements to their infrastructure.</p>
<p>If you are interested in seeing how others approach configuration management with VyOS, I would highly recommend the following from <a href="https://faelix.net">Faelix</a>: -</p>
<ul>
<li><a href="https://www.youtube.com/watch?v=lN7JExT_wrs">virtualUKNOF September 2020 - Salt + Netbox + VyOS = Network Automation + Routing Security</a></li>
<li><a href="https://faelix.net/news/202002/hphr/">Halophile Router</a></li>
<li><a href="https://faelix.net/news/201910/vyos-salt-netbox-peering-and-transit-upgrade/">Faelix - Our Peering and Transit Network Upgrade</a></li>
</ul>
<h2 id="future-parts-of-this-series">Future parts of this series</h2>
<p>For those who have followed this series so far, you will probably have noticed a few things: -</p>
<ul>
<li>The first 6 parts of the series took over 3 months to put together
<ul>
<li>This includes building the labs, preparing the roles, testing them, and then writing the posts</li>
</ul>
</li>
<li>This post comes out over 4 months after the last post</li>
<li>Many of the sections are almost word-for-word identical to the same sections in other posts</li>
</ul>
<p>In some cases, especially the MikroTik lab, it would take days and even weeks to create the correct roles. Additionally, writing the posts to describe these roles can often take as much (if not more) time as creating the roles themselves.</p>
<p>My career focus over the past few years has been more towards DevOps and Site Reliability Engineering, meaning the time spent on creating these Ansible roles is becoming less and less relevant to my day-to-day work.</p>
<p>Because of this, I have decided that rather than building individual posts for other vendors I am going to: -</p>
<ul>
<li>Build the Ansible roles for a number of other vendors
<ul>
<li>Cumulus</li>
<li>Extreme EXOS</li>
<li>OpenBSD</li>
<li>HP Procurve</li>
<li>HPE/H3C Comware/Huawei</li>
<li>Nokia (ex-Alcatel)</li>
<li>Cisco IOS-XR</li>
<li>Cisco NX-OS</li>
<li>Check Point GAiA firewalls</li>
<li>Fortinet Fortigate firewalls</li>
</ul>
</li>
<li>Commit them to my <a href="https://gitlab.com/stuh84/network-automation-ansible">Ansible Network Automation Repository</a> on Gitlab</li>
<li>Create a anthology/compendium post that covers any quirks, gotchyas or other interesting parts of working with across all the vendors rather than single posts per vendor</li>
</ul>
<p>With many of the tasks being almost identical except for vendor syntax changes, it makes more sense to provide a single repository to allow people to compare the differences for themselves.</p>
<p>I have enjoyed putting this series together so far and I now feel that the series has achieved it&rsquo;s goal of showing how to use Ansible to manage network infrastructure. Any differences in future roles will be down to vendor-specific syntax rather than with any significant changes to the Ansible roles themselves (in terms of logic or approach).</p>
<p>If you have found these posts useful, let me know if you&rsquo;d like to see other vendors covered in the Gitlab repository, any questions you have or any improvements you think could be made across all of them (pull/merge requests always welcome!).</p>
]]></content>
        </item>
        
        <item>
            <title>Prometheus - Using Service Discovery with Hetzner HCloud</title>
            <link>https://yetiops.net/posts/prometheus-service-discovery-hetzner-hcloud/</link>
            <pubDate>Fri, 04 Sep 2020 22:28:39 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/prometheus-service-discovery-hetzner-hcloud/</guid>
            <description>In a number of posts on this site I have covered Prometheus and some of the service discovery methods, including: -
 Consul DNS SRV Records AWS, Azure and Google Cloud Platform Digital Ocean OpenStack  In the next release of Prometheus (v2.21 at the time of writing) Prometheus will also be able to discover services for Eureka and Hetzner.
At the time of writing, the v2.21 release of Prometheus is available as a Release Candidate (available here), meaning we do not need to compile Prometheus from source to make use of it.</description>
            <content type="html"><![CDATA[<p>In a number of posts on this site I have covered Prometheus and some of the service discovery methods, including: -</p>
<ul>
<li><a href="/posts/prometheus-consul-node_exporter/">Consul</a></li>
<li><a href="/posts/prometheus-srv-discovery/">DNS SRV Records</a></li>
<li><a href="/posts/prometheus-service-discovery-aws-gcp-azure/">AWS, Azure and Google Cloud Platform</a></li>
<li><a href="/posts/prometheus-service-discovery-digitalocean/">Digital Ocean</a></li>
<li><a href="/posts/prometheus-service-discovery-openstack/">OpenStack</a></li>
</ul>
<p>In the next release of Prometheus (v2.21 at the time of writing) Prometheus will also be able to discover services for <a href="https://github.com/Netflix/eureka">Eureka</a> and <a href="https://www.hetzner.com">Hetzner</a>.</p>
<p>At the time of writing, the v2.21 release of Prometheus is available as a Release Candidate (available <a href="https://github.com/prometheus/prometheus/releases/tag/v2.21.0-rc.0">here</a>), meaning we do not need to compile Prometheus from source to make use of it. However if you intend to run this in production, it is advisable to wait for final release.</p>
<h2 id="hetzner">Hetzner</h2>
<h3 id="who-are-hetzner">Who are Hetzner?</h3>
<p>Hetzner are a German hosting company and data centre operator. They run data centres in three locations (Nuremberg, Falkenstein and Helsinki in Finland), and can provide everything from webmail to dedicated servers.</p>
<p>Hetzner&rsquo;s <a href="https://robot.your-server.de/">Robot</a> is the name they give to their dedicated server offering. The Hetzner <a href="https://www.hetzner.com/cloud">Cloud</a> provides virtual servers, load balancers, private networking and more.</p>
<p>The Prometheus Service Discovery enables discover servers from both Robot and Cloud.</p>
<h3 id="creating-an-account">Creating an account</h3>
<p>To sign up with Hetzner&rsquo;s Cloud, go to the Hetzner <a href="https://accounts.hetzner.com/signUp">Account Sign-Up</a> page. This will create an account which is usable across all of Hetzner&rsquo;s products.</p>
<p>Once you have created an account, you will be asked to create a project like so: -</p>
<p><img src="/img/prometheus/hcloud-addnewproject.png" alt="Add new project"></p>
<p><img src="/img/prometheus/hcloud-addnewproject-name.png" alt="Add new project - name"></p>
<p>Currently you cannot create resources in a specific project using Terraform, nor can you limit targets by project in Prometheus, so what name you choose is arbitrary.</p>
<h2 id="terraform">Terraform</h2>
<p>There is an official <a href="https://registry.terraform.io/providers/hetznercloud/hcloud/latest">Terraform Provider</a> for Hetzner&rsquo;s Cloud offering which will allow us to create virtual servers ready to be discovered by Prometheus.</p>
<p>For information on how to install Terraform and an example project structure, see <a href="http://localhost:1313/posts/prometheus-service-discovery-aws-gcp-azure/#configure-terraform">here</a>.</p>
<h3 id="create-an-api-token">Create an API Token</h3>
<p>Terraform uses Hetzner Cloud API keys to authenticate and provision resources. You can generate a key in the <strong>Security</strong> section of the <a href="https://console.hetzner.cloud">Hetzner Cloud Console</a>: -</p>
<p><img src="/img/prometheus/hcloud-security.png" alt="Hetzner Cloud Security"></p>
<p><img src="/img/prometheus/hcloud-generateapitoken.png" alt="Hetzner Cloud - Generate API Key"></p>
<p>You can configure the API Token directly in the Terraform provider configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;hcloud&#34;</span> {
  token <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;$API_TOKEN&#34;</span>
}
</code></pre></div><p>Alternatively, you can set an environment variable of <code>HCLOUD_TOKEN=&quot;$API_TOKEN&quot;</code> (e.g. <code>export HCLOUD_TOKEN=&quot;$API_TOKEN&quot;</code>) that the Terraform provider will use instead.</p>
<p>If you are using Terraform version 0.13, you can also add the <code>hcloud</code> provider to your list of required providers like so: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">terraform</span> {
  <span style="color:#66d9ef">required_providers</span> {
    hcloud <span style="color:#f92672">=</span> {
      source <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;hetznercloud/hcloud&#34;</span>
    }
    template <span style="color:#f92672">=</span> {
      source <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;hashicorp/template&#34;</span>
    }
  }
  required_version <span style="color:#f92672">=</span> &#34;&gt;<span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>.<span style="color:#ae81ff">13</span><span style="color:#960050;background-color:#1e0010">&#34;</span>
}
</code></pre></div><p>This configuration goes in a <code>versions.tf</code> file in the <code>basic-vms</code> folder.</p>
<h3 id="configure-terraform---virtual-servers">Configure Terraform - Virtual Servers</h3>
<p>Now that we have created an API token and configured the provider, we can create our Hetzner Cloud virtual servers.</p>
<p>Run <code>terraform init</code> to download and install the provider: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ terraform init 

Initializing the backend...

Initializing provider plugins...
- Finding latest version of hashicorp/template...
- Finding latest version of hetznercloud/hcloud...
- Installing hetznercloud/hcloud v1.20.1...
- Installed hetznercloud/hcloud v1.20.1 <span style="color:#f92672">(</span>signed by a HashiCorp partner, key ID 5219EACB3A77198B<span style="color:#f92672">)</span>
- Installing hashicorp/template v2.1.2...
- Installed hashicorp/template v2.1.2 <span style="color:#f92672">(</span>signed by HashiCorp<span style="color:#f92672">)</span>

Partner and community providers are signed by their developers.
If you<span style="color:#960050;background-color:#1e0010">&#39;</span>d like to know more about provider signing, you can read about it here:
https://www.terraform.io/docs/plugins/signing.html

The following providers <span style="color:#66d9ef">do</span> not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, we recommend adding version constraints in a required_providers block
in your configuration, with the constraint strings suggested below.

* hashicorp/template: version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;~&gt; 2.1.2&#34;</span>
* hetznercloud/hcloud: version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;~&gt; 1.20.1&#34;</span>

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running <span style="color:#e6db74">&#34;terraform plan&#34;</span> to see
any changes that are required <span style="color:#66d9ef">for</span> your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration <span style="color:#66d9ef">for</span> Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to <span style="color:#66d9ef">do</span> so <span style="color:#66d9ef">if</span> necessary.
</code></pre></div><h3 id="define-the-infrastructure---virtual-servers">Define the infrastructure - Virtual Servers</h3>
<p>You can now create the configuration files for your first virtual server. The below is from the file <code>hcloud.tf</code> in the <code>terraform/basic-vms</code> directory: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;hcloud_server&#34; &#34;yetiops-blog&#34;</span> {
  name               <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-blog&#34;</span>
  image              <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ubuntu-20.04&#34;</span>
  server_type        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cx11&#34;</span>
  user_data          <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">template_file</span>.<span style="color:#66d9ef">ubuntu</span>.<span style="color:#66d9ef">rendered</span>
  ssh_keys           <span style="color:#f92672">=</span> [
    <span style="color:#66d9ef">hcloud_ssh_key</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">ssh</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">key</span>.<span style="color:#66d9ef">name</span>
  ]
  labels <span style="color:#f92672">=</span> {
    &#34;prometheus&#34; <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>,
    &#34;node_exporter&#34; <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
  }
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;hcloud_ssh_key&#34; &#34;yetiops-ssh-key&#34;</span> {
  name       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-ssh-key&#34;</span>
  public_key <span style="color:#f92672">=</span> <span style="color:#66d9ef">file</span>(<span style="color:#e6db74">&#34;~/.ssh/id_ed25519.pub&#34;</span>)
}

<span style="color:#66d9ef">output</span> <span style="color:#e6db74">&#34;yetiops_blog_ipv4&#34;</span> {
  value <span style="color:#f92672">=</span> <span style="color:#66d9ef">hcloud_server</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">blog</span>.<span style="color:#66d9ef">ipv4_address</span>
}

<span style="color:#66d9ef">output</span> <span style="color:#e6db74">&#34;yetiops_blog_ipv6&#34;</span> {
  value <span style="color:#f92672">=</span> <span style="color:#66d9ef">hcloud_server</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">blog</span>.<span style="color:#66d9ef">ipv6_address</span>
}
</code></pre></div><p>Compared to some of the other posts, we are creating a small amount of infrastructure. This is because: -</p>
<ul>
<li>Hetzner Cloud does not provide firewall/security group resources (like an AWS Security Group or a Digital Ocean Firewall)</li>
<li>While you can specify a datacentre, Hetzner will choose one at random if you do not (rather than it being a prerequisite of creating an instance)</li>
</ul>
<p>Hetzner Cloud instances support labels for instances (consisting of key-value pairs). These can then be used to filter/match instances with Prometheus later.</p>
<p>We have also added some Terraform outputs as well. Outputs can be used in Terraform modules to expose attributes to state files that call the modules, or they can be used to show information when Terraform finishes an <code>apply</code> operation.</p>
<p>One part that does differ from our previous posts is that we extend the <code>user-data</code> being provided. This create a basic host-based firewall (leveraging <a href="https://help.ubuntu.com/community/UFW">UFW</a>) on the server: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e">#cloud-config</span>
<span style="color:#66d9ef">packages</span>:
 - prometheus-node-exporter

<span style="color:#66d9ef">ufw_enable</span>:
  - <span style="color:#75715e">&amp;ufw_enable</span> <span style="color:#e6db74">|
</span><span style="color:#e6db74">    ufw allow from ${my_ip}</span>
    ufw enable

<span style="color:#66d9ef">runcmd</span>:
   - [ sh, -c, <span style="color:#75715e">*ufw_enable</span> ]
</code></pre></div><p>We now also need to supply our IP as part of this: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;template_file&#34; &#34;ubuntu&#34;</span> {
  template <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${file(&#34;${path.module}/files/ubuntu.tpl&#34;)}&#34;</span>
  vars <span style="color:#f92672">=</span> {
    my_ip <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;###MY PUBLIC IP ADDRESS###&#34;</span>
  }
}
</code></pre></div><p>You could also use IPTables, nftables, firewalld or any other host-based firewall of your choice.</p>
<h3 id="build-the-infrastructure---virtual-servers">Build the infrastructure - Virtual Servers</h3>
<p>We can now apply our configuration, and see if it builds a Hetzner Cloud virtual server: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ terraform apply
data.template_file.ubuntu: Refreshing state...
data.template_cloudinit_config.ubuntu: Refreshing state...

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  <span style="color:#75715e"># hcloud_server.yetiops-blog will be created</span>
  + resource <span style="color:#e6db74">&#34;hcloud_server&#34;</span> <span style="color:#e6db74">&#34;yetiops-blog&#34;</span> <span style="color:#f92672">{</span>
      + backup_window <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + backups       <span style="color:#f92672">=</span> false
      + datacenter    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + image         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ubuntu-20.04&#34;</span>
      + ipv4_address  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + ipv6_address  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + ipv6_network  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + keep_disk     <span style="color:#f92672">=</span> false
      + labels        <span style="color:#f92672">=</span> <span style="color:#f92672">{</span>
          + <span style="color:#e6db74">&#34;node_exporter&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
          + <span style="color:#e6db74">&#34;prometheus&#34;</span>    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
        <span style="color:#f92672">}</span>
      + location      <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + name          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-blog&#34;</span>
      + server_type   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cx11&#34;</span>
      + ssh_keys      <span style="color:#f92672">=</span> <span style="color:#f92672">[</span>
          + <span style="color:#e6db74">&#34;mbp-ssh-key&#34;</span>,
        <span style="color:#f92672">]</span>
      + status        <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + user_data     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;w+GFzbnffrRD1nkvx62lUBl75Zw=&#34;</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># hcloud_ssh_key.yetiops-ssh-key will be created</span>
  + resource <span style="color:#e6db74">&#34;hcloud_ssh_key&#34;</span> <span style="color:#e6db74">&#34;yetiops-ssh-key&#34;</span> <span style="color:#f92672">{</span>
      + fingerprint <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + name        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-ssh-key&#34;</span>
      + public_key  <span style="color:#f92672">=</span> &lt;&lt;~EOT
            ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB5EDvM9LaPqNH5tTV2WOHHC+rc5iK7AWRJyMpvNLcvD stuh84@symphonyx
        EOT
    <span style="color:#f92672">}</span>

Plan: <span style="color:#ae81ff">2</span> to add, <span style="color:#ae81ff">0</span> to change, <span style="color:#ae81ff">0</span> to destroy.

Changes to Outputs:
  + yetiops_blog_ipv4 <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
  + yetiops_blog_ipv6 <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only <span style="color:#e6db74">&#39;yes&#39;</span> will be accepted to approve.

  Enter a value: yes

hcloud_ssh_key.yetiops-ssh-key: Creating...
hcloud_ssh_key.yetiops-ssh-key: Creation complete after 0s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>2022327<span style="color:#f92672">]</span>
hcloud_server.yetiops-blog: Creating...
hcloud_server.yetiops-blog: Creation complete after 7s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>7490230<span style="color:#f92672">]</span>

Apply complete! Resources: <span style="color:#ae81ff">2</span> added, <span style="color:#ae81ff">0</span> changed, <span style="color:#ae81ff">0</span> destroyed.

Outputs:

yetiops_blog_ipv4 <span style="color:#f92672">=</span> <span style="color:#75715e">###IPv4 Public IP###</span> 
yetiops_blog_ipv6 <span style="color:#f92672">=</span> <span style="color:#75715e">###IPv6 Public IP###</span> 
</code></pre></div><p>As we can see, we also have <strong>Outputs</strong> that display the IPv4 and IPv6 address of this server.</p>
<p>We can double check that Terraform is managing these resources now with <code>terraform state list</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ terraform state list
data.template_cloudinit_config.ubuntu
data.template_file.ubuntu
hcloud_server.yetiops-blog
hcloud_ssh_key.yetiops-ssh-key
</code></pre></div><p>We can check to see if the instance is in the Hetzner Cloud Console: -</p>
<p><img src="/img/prometheus/hetzner-cloud-console-instance.png" alt="Hetzner Cloud Console - Instance"></p>
<p>Now lets try SSH: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ssh root@$INSTANCE_PUBLIC_IP
Welcome to Ubuntu 20.04.1 LTS <span style="color:#f92672">(</span>GNU/Linux 5.4.0-42-generic x86_64<span style="color:#f92672">)</span>

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

root@yetiops-blog:~# ps aux | grep -i node
prometh+    <span style="color:#ae81ff">1276</span>  0.3  0.9 <span style="color:#ae81ff">485924</span> <span style="color:#ae81ff">18912</span> ?        Ssl  22:53   0:02 /usr/bin/prometheus-node-exporter
root        <span style="color:#ae81ff">1968</span>  0.0  0.0   <span style="color:#ae81ff">6300</span>   <span style="color:#ae81ff">732</span> pts/0    S+   23:05   0:00 grep --color<span style="color:#f92672">=</span>auto -i node
</code></pre></div><h2 id="prometheus">Prometheus</h2>
<p>Now that we have our virtual server in Hetzner Cloud, we can configure our Prometheus instance. I am using an Ubuntu 20.04 virtual machine in my lab for this.</p>
<h3 id="hetzner-cloud-service-discovery">Hetzner Cloud Service Discovery</h3>
<p>To allow Prometheus to discover virtual servers in Hetzner Cloud, use configuration like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">global</span>:
  <span style="color:#66d9ef">scrape_interval</span>:     15s
  <span style="color:#66d9ef">evaluation_interval</span>: 15s

<span style="color:#66d9ef">scrape_configs</span>:
  - <span style="color:#66d9ef">job_name</span>: <span style="color:#e6db74">&#39;prometheus&#39;</span>
    <span style="color:#66d9ef">static_configs</span>:
    - <span style="color:#66d9ef">targets</span>:
      - <span style="color:#e6db74">&#39;localhost:9090&#39;</span>
  - <span style="color:#66d9ef">job_name</span>: <span style="color:#e6db74">&#39;hetzner-nodes&#39;</span>
    <span style="color:#66d9ef">hetzner_sd_configs</span>:
      - <span style="color:#66d9ef">bearer_token</span>: <span style="color:#e6db74">&#34;###HETZNER_API_TOKEN###&#34;</span>
        <span style="color:#66d9ef">role</span>: <span style="color:#e6db74">&#34;hcloud&#34;</span>
    <span style="color:#66d9ef">relabel_configs</span>:
      - <span style="color:#66d9ef">source_labels</span>: [__meta_hetzner_hcloud_label_prometheus]
        <span style="color:#66d9ef">regex</span>: <span style="color:#66d9ef">true</span>.*
        <span style="color:#66d9ef">action</span>: keep
      - <span style="color:#66d9ef">source_labels</span>: [__meta_hetzner_hcloud_label_node_exporter]
        <span style="color:#66d9ef">regex</span>: <span style="color:#66d9ef">true</span>.*
        <span style="color:#66d9ef">action</span>: keep
      - <span style="color:#66d9ef">source_labels</span>: [__meta_hetzner_public_ipv4]
        <span style="color:#66d9ef">target_label</span>: __address__
        <span style="color:#66d9ef">replacement</span>: <span style="color:#e6db74">&#39;$1:9100&#39;</span>
      - <span style="color:#66d9ef">action</span>: labelmap
        <span style="color:#66d9ef">regex</span>: __meta_hetzner_hcloud_(.+)
      - <span style="color:#66d9ef">action</span>: labelmap
        <span style="color:#66d9ef">regex</span>: __meta_hetzner_server_(.+)
</code></pre></div><p>You can create an API token for Prometheus using the same method as for <a href="/posts/prometheus-service-discovery-hetzner-hcloud/#create-an-api-token">Terraform</a></p>
<p>There a few points to note in this configuration: -</p>
<ul>
<li>We are using the <code>hcloud</code> role
<ul>
<li>You can use <code>robot</code> if you have dedicated servers with Hetzner</li>
</ul>
</li>
<li>The labels (tags) are key-value pairs like in AWS, GCP or Azure, rather than the tags in Digital Ocean which are comma-separated values</li>
<li>We use two labelmaps to bring in most of the metadata in as additional labels
<ul>
<li>This is because some labels are exposed only under <code>__meta_hetzner_hcloud</code> and others under <code>__meta_hetzner_server</code></li>
</ul>
</li>
</ul>
<p>We can now look at the labels that the Hetzner Service Discovery generates: -</p>
<p><img src="/img/prometheus/hetzner-prom-sd.png" alt="Hetzner Prometheus Service Discovery Metadata"></p>
<p>Can we reach the <code>node_exporter</code> on the Instance?</p>
<p><img src="/img/prometheus/hetzner-prom-target.png" alt="Hetzner Prometheus Target"></p>
<p>Looks like we can!</p>
<h2 id="grafana">Grafana</h2>
<p>With the above, we can use any Node Exporter dashboard in Grafana to view all of the discovered instances. The <a href="https://grafana.com/grafana/dashboards/1860">Node Exporter Full</a> is always a good start when using the Node Exporter: -</p>
<p><img src="/img/prometheus/hetzner-grafana-node-exporter-full.png" alt="Digital Ocean Grafana"></p>
<p>If we add more virtual servers, they will also appear in this dashboard too.</p>
<h2 id="summary">Summary</h2>
<p>With each release of Prometheus, more and more discovery methods are being added. This means that even if you aren&rsquo;t running on some of the larger providers (e.g. AWS, Azure or GCP), you can start to look at using Prometheus to automatically discover your instances and services without needing to run either something like Consul or using <code>file_sd_configs</code> and configuration management.</p>
<p>I am excited to see what new methods will appear in future versions!</p>
]]></content>
        </item>
        
        <item>
            <title>Using Terraform and Cloud-Init to deploy and automatically monitor Proxmox instances - Windows</title>
            <link>https://yetiops.net/posts/proxmox-terraform-cloudinit-windows/</link>
            <pubDate>Thu, 03 Sep 2020 12:18:32 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/proxmox-terraform-cloudinit-windows/</guid>
            <description>In the last post I covered deploying Linux-based Proxmox instances using Terraform, leveraging Cloud-Init and cloud-config to register these instances with SaltStack.
The instances in the previous post were both Linux distributions (Debian and Fedora). This is because Cloud-Init was initially built for Linux, and then ported to other Unix-like systems (e.g. FreeBSD, OpenBSD and more).
Thanks to a company called Cloudbase Solutions an equivalent of Cloud-Init is also available for Windows, known as Cloudbase-Init.</description>
            <content type="html"><![CDATA[<p>In the <a href="/posts/proxmox-terraform-cloudinit-saltstack-prometheus/">last post</a> I covered deploying Linux-based Proxmox instances using Terraform, leveraging <a href="https://cloud-init.io/">Cloud-Init</a> and <code>cloud-config</code> to register these instances with SaltStack.</p>
<p>The instances in the previous post were both Linux distributions (Debian and Fedora). This is because Cloud-Init was initially built for Linux, and then ported to other Unix-like systems (e.g. FreeBSD, OpenBSD and more).</p>
<p>Thanks to a company called <a href="https://cloudbase.it">Cloudbase Solutions</a> an equivalent of Cloud-Init is also available for Windows, known as <a href="https://cloudbase.it/cloudbase-init/">Cloudbase-Init</a>.</p>
<h2 id="cloudbase-init">Cloudbase-Init</h2>
<p>Cloudbase-Init is written in Python, and was originally developed to allow customization of Windows images on OpenStack. However it also supports a number of different <a href="https://cloudbase-init.readthedocs.io/en/latest/services.html">metadata services</a> (everything from AWS and Azure to VMware).</p>
<p>Cloudbase-Init also supports a <a href="https://cloudbase-init.readthedocs.io/en/latest/userdata.html#cloud-config">subset</a> of the <code>cloud-config</code> format, including <code>write_files</code> (i.e. creating files on the instance), <code>users</code>, <code>groups</code>, <code>runcmd</code> (i.e. running arbitrary commands) and more.</p>
<h2 id="preparing-the-template-machine">Preparing the template machine</h2>
<p>The first step before creating your instances is to create a template Windows image. This image can then be used as a base image for other instances (using cloning).</p>
<p>Cloudbase Solutions provide <a href="https://github.com/cloudbase/windows-openstack-imaging-tools">tools</a> to build Windows images ready to be deployed with Cloudbase-Init installed, but they rely on Hyper-V on Windows. If you do not have access to a Windows machine to generate the images, do not have access to Hyper-V, or simply want to go through the process yourself, then you can install Cloudbase-Init in a Windows instance yourself.</p>
<h3 id="installing-windows">Installing Windows</h3>
<p>In this post, I am using Windows Server Core 2019 (i.e. without the Desktop Experience installed). As Proxmox uses KVM as its underlying hypervisor, you will also need to download the <a href="https://docs.fedoraproject.org/en-US/quick-docs/creating-windows-virtual-machines-using-virtio-drivers/index.html">VirtIO-Win</a> ISO. This contains all the necessary drivers for Windows for VirtIO-based NICs, storage and serial devices. Without this, you would need use emulated devices (i.e. emulated SATA or network devices), which are less efficient than the VirtIO equivalents.</p>
<p>First, download the VirtIO ISO and place it in the <code>/var/lib/vz/templates/iso</code> directory on your Proxmox host. This is where Proxmox expects to find all ISO images by default.</p>
<p>After this, place the ISO for your Windows version of choice in the same directory. If you already have the ISOs, then use something like <code>scp</code> or <code>rsync</code> to place it on the Proxmox server. Alternatively, you can download trial versions of Windows from the <a href="https://www.microsoft.com/en-us/evalcenter/">Microsoft Evaluation Center</a>. These trial versions can be activated later with the appropriate license key and service contract with Microsoft.</p>
<p>Now create the instance. The settings in the below images should work on a default Proxmox install, but you may need to change them if your environment differs (e.g. using Ceph/ZFS for storage, OpenVSwitch for networking): -</p>
<p><strong>Create the VM</strong>
<img src="/img/proxmox/proxmox-win-create-vm-1-create.png" alt="proxmox win create vm create"></p>
<p><strong>General Options</strong>
<img src="/img/proxmox/proxmox-win-create-vm-2-general.png" alt="proxmox win create vm general"></p>
<p><strong>Choose the OS</strong>
<img src="/img/proxmox/proxmox-win-create-vm-3-os.png" alt="proxmox win create vm os"></p>
<p><strong>System hardware (Disk controller and graphics</strong>
<img src="/img/proxmox/proxmox-win-create-vm-4-system.png" alt="proxmox win create vm system"></p>
<p><strong>Create the disk</strong>
<img src="/img/proxmox/proxmox-win-create-vm-5-disk.png" alt="proxmox win create vm disk"></p>
<p><strong>Choose the CPU and core count</strong>
<img src="/img/proxmox/proxmox-win-create-vm-6-cpu.png" alt="proxmox win create vm cpu"></p>
<p><strong>Choose the amount of memory</strong>
<img src="/img/proxmox/proxmox-win-create-vm-7-mem.png" alt="proxmox win create vm mem"></p>
<p><strong>Choose the network</strong>
<img src="/img/proxmox/proxmox-win-create-vm-8-network.png" alt="proxmox win create vm network"></p>
<p>After creating the instance, select it, go to <strong>Hardware</strong>, and then add a CD-ROM drive: -
<img src="/img/proxmox/proxmox-win-create-vm-9-add-cdrom.png" alt="proxmox win create vm add cdrom"></p>
<p>Choose the <strong>virtio-win.iso</strong> - this contains all the necessary Windows drivers: -
<img src="/img/proxmox/proxmox-win-create-vm-10-virtio.png" alt="proxmox win create vm virtio"></p>
<p>When you start the machine, you should now be presented with the following: -</p>
<p><strong>Loading files</strong>
<img src="/img/proxmox/proxmox-win-install-vm-1-loading-files.png" alt="Proxmox Windows - Loading Files"></p>
<p><strong>Booting</strong>
<img src="/img/proxmox/proxmox-win-install-vm-2-booting.png" alt="proxmox win install vm booting"></p>
<p><strong>Choose your language and timezone</strong>
<img src="/img/proxmox/proxmox-win-install-vm-3-choose-language.png" alt="proxmox win install vm choose language"></p>
<p><strong>Click Install Now</strong>
<img src="/img/proxmox/proxmox-win-install-vm-4-install.png" alt="proxmox win install vm install"></p>
<p><strong>Choose which version (Desktop Experience = GUI)</strong>
<img src="/img/proxmox/proxmox-win-install-vm-5-win-version.png" alt="proxmox win install vm win version"></p>
<p><strong>Choose Custom installation (because there is no existing installation to upgrade)</strong>
<img src="/img/proxmox/proxmox-win-install-vm-6-custom.png" alt="proxmox win install vm custom"></p>
<p>At this point, you will see that there is no hard disk to install to. This is because Windows does not include the VirtIO drivers by default: -</p>
<p><img src="/img/proxmox/proxmox-win-install-vm-7-no-disk.png" alt="proxmox win install vm no disk"></p>
<p>We can resolve this by clicking on <strong>Load Driver</strong>, which will give the option to <strong>browse</strong> for drivers: -</p>
<p><img src="/img/proxmox/proxmox-win-install-vm-8-browse.png" alt="proxmox win install vm browse"></p>
<p>Navigate to the driver with the <strong>virtio-win-0.x.x</strong> ISO: -</p>
<p><img src="/img/proxmox/proxmox-win-install-vm-8-virtio-cd.png" alt="proxmox win install vm virtio cd"></p>
<p>Scroll down to <strong>viostor</strong>: -</p>
<p><img src="/img/proxmox/proxmox-win-install-vm-9-viostor.png" alt="proxmox win install vm viostor"></p>
<p>Select the driver and click on <strong>Next</strong>: -</p>
<p><img src="/img/proxmox/proxmox-win-install-vm-10-select-driver.png" alt="proxmox win install vm select driver"></p>
<p>You should now see the disk to install to: -</p>
<p><img src="/img/proxmox/proxmox-win-install-vm-11-disk.png" alt="proxmox win install vm disk"></p>
<p>At this point, you can either click <strong>Next</strong> and finish the installation, or you can click <strong>Load Driver</strong> to add more VirtIO drivers. The main ones to install are: -</p>
<ul>
<li>Balloon - This allows for dynamic memory management, optimizing memory usage where appropriate</li>
<li>NetKVM - This installs the network drivers for the VirtIO network card</li>
<li>qxldod - If you require graphics drivers</li>
<li>vioserial - This installs serial drivers, which we will need to use later for Cloudbase-Init (mainly for logging)</li>
</ul>
<p>You can choose to install the drivers later, using either the GUI if you chose the desktop experience, or using <a href="https://docs.microsoft.com/en-us/windows-hardware/drivers/devtest/pnputil">pnputil</a> if using Server Core.</p>
<p>Once done, click <strong>Next</strong> to finish the installation: -</p>
<p><img src="/img/proxmox/proxmox-win-install-vm-12-copying.png" alt="Proxmox Windows Install - Copying Files"></p>
<p>Once Windows is installed and restarted, you will be given the option to create a password for the Administrator user: -</p>
<p><strong>Unlock the instance (using Ctrl, Alt and Delete</strong>
<img src="/img/proxmox/proxmox-win-install-vm-13-unlock.png" alt="proxmox win install vm unlock"></p>
<p><strong>Proxmox Ctrl, Alt and Delete in NoVNC</strong>
<img src="/img/proxmox/proxmox-win-install-vm-14-ctrlaltdel.png" alt="proxmox win install vm ctrlaltdel"></p>
<p><strong>Confirm changing of the password</strong>
<img src="/img/proxmox/proxmox-win-install-vm-15-change-pass.png" alt="proxmox win install vm change pass"></p>
<p><strong>Change the password</strong>
<img src="/img/proxmox/proxmox-win-install-vm-16-password.png" alt="proxmox win install vm password"></p>
<p><strong>Logged in as the Administrator</strong>
<img src="/img/proxmox/proxmox-win-install-vm-17-cmd-prompt.png" alt="proxmox win install vm cmd prompt"></p>
<p>At this point, we are now ready to customize the image.</p>
<h3 id="customization">Customization</h3>
<p>To prepare the image, the minimum we require for Cloud-Init is the Cloudbase-Init utility. Before installing Cloudbase-Init I also do the following: -</p>
<ul>
<li>Install OpenSSH Server for remote management</li>
<li>Install Salt</li>
<li>Disable the firewall and enable RDP</li>
</ul>
<p>The reason for disabling the firewall is that in the environments I work in, host-based firewalls are not used. Instead, they are managed either via network firewalls or via security groups (if used within a cloud provider). If you require the firewall to be enabled, then you will need to customize the firewall rules to match your environment.</p>
<h4 id="installing-openssh-server">Installing OpenSSH Server</h4>
<p>To install the OpenSSH Server on Windows 2019, you can follow the steps in <a href="/posts/prometheus-consul-saltstack-part-2-windows/#enabling-openssh-on-windows">this post</a>. We are then able to SSH into the machine: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ssh Administrator@10.15.31.25
Administrator@10.15.31.25<span style="color:#960050;background-color:#1e0010">&#39;</span>s password:

Microsoft Windows <span style="color:#f92672">[</span>Version 10.0.17763.737<span style="color:#f92672">]</span>
<span style="color:#f92672">(</span>c<span style="color:#f92672">)</span> <span style="color:#ae81ff">2018</span> Microsoft Corporation. All rights reserved.

administrator@WIN2019-01 C:<span style="color:#ae81ff">\U</span>sers<span style="color:#ae81ff">\A</span>dministrator&gt;
</code></pre></div><h4 id="install-salt">Install Salt</h4>
<p>To install the Salt Minion, again you can follow the steps in <a href="/posts/prometheus-consul-saltstack-part-2-windows/#installing-the-salt-minion">this post</a>. However, do not follow the steps for updating the minion configuration post-install. The minion configuration will be customized by Cloudbase-Init based upon the provided User Data.</p>
<h4 id="disable-the-firewall-and-enable-rdp">Disable the firewall and enable RDP</h4>
<p>While most tasks can be completed using Powershell in Windows Core, there is no inbuilt text editor. You can either install something like <a href="https://www.vim.org/download.php">ViM</a> or you can use RDP so that <code>notepad.exe</code> is available for file editing.</p>
<p>We also disable the firewall at this point, as otherwise we will need to add an RDP-specific rule to the firewall to allow us to access the machine.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-powershell" data-lang="powershell"><span style="color:#75715e"># Disable the firewall</span>
Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled false

<span style="color:#75715e"># Enable RDP</span>
Set-ItemProperty -Path <span style="color:#e6db74">&#39;HKLM:\System\CurrentControlSet\Control\Terminal Server&#39;</span> -name <span style="color:#e6db74">&#34;fDenyTSConnections&#34;</span> -value 0
</code></pre></div><p>You can also use the <code>sconfig</code> tool to enable RDP as well: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">PS C:<span style="color:#ae81ff">\U</span>sers<span style="color:#ae81ff">\A</span>dministrator&gt; sconfig

Microsoft <span style="color:#f92672">(</span>R<span style="color:#f92672">)</span> Windows Script Host Version 5.812
Copyright <span style="color:#f92672">(</span>C<span style="color:#f92672">)</span> Microsoft Corporation. All rights reserved.

Inspecting system...


<span style="color:#f92672">===============================================================================</span>
                         Server Configuration
<span style="color:#f92672">===============================================================================</span>

1<span style="color:#f92672">)</span> Domain/Workgroup:                    Workgroup:  WORKGROUP
2<span style="color:#f92672">)</span> Computer Name:                       TEST
3<span style="color:#f92672">)</span> Add Local Administrator
4<span style="color:#f92672">)</span> Configure Remote Management          Enabled

5<span style="color:#f92672">)</span> Windows Update Settings:             DownloadOnly
6<span style="color:#f92672">)</span> Download and Install Updates
7<span style="color:#f92672">)</span> Remote Desktop:                      Disabled

8<span style="color:#f92672">)</span> Network Settings
9<span style="color:#f92672">)</span> Date and Time
10<span style="color:#f92672">)</span> Telemetry settings                  Unknown
11<span style="color:#f92672">)</span> Windows Activation

12<span style="color:#f92672">)</span> Log Off User
13<span style="color:#f92672">)</span> Restart Server
14<span style="color:#f92672">)</span> Shut Down Server
15<span style="color:#f92672">)</span> Exit to Command Line

Enter number to <span style="color:#66d9ef">select</span> an option: <span style="color:#ae81ff">7</span>


<span style="color:#f92672">(</span>E<span style="color:#f92672">)</span>nable or <span style="color:#f92672">(</span>D<span style="color:#f92672">)</span>isable Remote Desktop? <span style="color:#f92672">(</span>Blank<span style="color:#f92672">=</span>Cancel<span style="color:#f92672">)</span> E

1<span style="color:#f92672">)</span> Allow only clients running Remote Desktop with Network Level Authentication <span style="color:#f92672">(</span>more secure<span style="color:#f92672">)</span>

2<span style="color:#f92672">)</span> Allow clients running any version of Remote Desktop <span style="color:#f92672">(</span>less secure<span style="color:#f92672">)</span>

Enter selection: <span style="color:#ae81ff">2</span>

Enabling Remote Desktop...
</code></pre></div><h3 id="installing-cloudbase-init">Installing Cloudbase-Init</h3>
<p>To install Cloudbase-Init, go to the <a href="https://cloudbase.it/cloudbase-init/">Cloudbase-Init product page</a>, scroll down to Downloads, and choose the version appropriate for your environment. I am running Windows 2019 on a 64-bit x86 processor, so I chose the <strong>Stable Cloudbase-init x64</strong> version.</p>
<p>The following steps will download and then install Cloudbase-Init: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-powershell" data-lang="powershell"><span style="color:#75715e"># Download the file</span>
PS C:\Users\Administrator&gt; Invoke-WebRequest -uri https<span style="color:#960050;background-color:#1e0010">:</span>//cloudbase.it/downloads/CloudbaseInitSetup_Stable_x64.msi -outfile cloudbase-init.msi

<span style="color:#75715e"># Install the MSI file non-interactively</span>
PS C:\Users\Administrator&gt; msiexec /i cloudbase-init.msi /qn /l*v log.txt
</code></pre></div><p>You can install Cloudbase-Init interactively and follow the on-screen instructions to customize the configuration. However we will need to change some options within the Cloudbase-Init configuration anyway, so there is little to benefit in using the GUI installer to configure the Cloudbase-Init service.</p>
<p>We can check that it is installed using: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-powershell" data-lang="powershell">PS C:\Users\Administrator&gt; Get-Service cloudbase-init

Status   Name               DisplayName
------   ----               -----------
Stopped  cloudbase-init     cloudbase-init
</code></pre></div><h3 id="configuring-cloudbase-init">Configuring Cloudbase-Init</h3>
<p>The default configuration files for Cloudbase-Init enable a number of different metadata services, most of which are not applicable to Proxmox.</p>
<p>We also need to enable the <code>User-Data</code> plugin otherwise Cloudbase-Init will not be able access any configuration we provide via Cloud-Init. The User Data can be in the form of Batch scripts (i.e. <code>cmd.exe</code>), Powershell, BASH (if it is installed natively under Windows), Python and <code>cloud-config</code>.</p>
<p>The following configuration files tell Cloudbase-Init to use the ConfigDrive2 format for user-data (the default format for a Windows guest on Proxmox), and enables the plugins required to make changes to the network, files and source changes from user-data: -</p>
<p><strong>C:\Program Files\Cloudbase Solutions\cloudbase-init\conf\cloudbase-init.conf</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#66d9ef">[DEFAULT]</span>
<span style="color:#a6e22e">username</span><span style="color:#f92672">=</span><span style="color:#e6db74">Admin</span>
<span style="color:#a6e22e">groups</span><span style="color:#f92672">=</span><span style="color:#e6db74">Administrators</span>
<span style="color:#a6e22e">inject_user_password</span><span style="color:#f92672">=</span><span style="color:#e6db74">true</span>
<span style="color:#a6e22e">config_drive_raw_hhd</span><span style="color:#f92672">=</span><span style="color:#e6db74">true</span>
<span style="color:#a6e22e">config_drive_cdrom</span><span style="color:#f92672">=</span><span style="color:#e6db74">true</span>
<span style="color:#a6e22e">config_drive_vfat</span><span style="color:#f92672">=</span><span style="color:#e6db74">true</span>
<span style="color:#a6e22e">bsdtar_path</span><span style="color:#f92672">=</span><span style="color:#e6db74">C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\bsdtar.exe</span>
<span style="color:#a6e22e">mtools_path</span><span style="color:#f92672">=</span><span style="color:#e6db74">C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\</span>
<span style="color:#a6e22e">verbose</span><span style="color:#f92672">=</span><span style="color:#e6db74">true</span>
<span style="color:#a6e22e">debug</span><span style="color:#f92672">=</span><span style="color:#e6db74">true</span>
<span style="color:#a6e22e">logdir</span><span style="color:#f92672">=</span><span style="color:#e6db74">C:\Program Files\Cloudbase Solutions\Cloudbase-Init\log\</span>
<span style="color:#a6e22e">logfile</span><span style="color:#f92672">=</span><span style="color:#e6db74">cloudbase-init.log</span>
<span style="color:#a6e22e">default_log_levels</span><span style="color:#f92672">=</span><span style="color:#e6db74">comtypes=INFO,suds=INFO,iso8601=WARN,requests=WARN</span>
<span style="color:#a6e22e">logging_serial_port_settings</span><span style="color:#f92672">=</span><span style="color:#e6db74">COM1,115200,N,8</span>
<span style="color:#a6e22e">mtu_use_dhcp_config</span><span style="color:#f92672">=</span><span style="color:#e6db74">true</span>
<span style="color:#a6e22e">ntp_use_dhcp_config</span><span style="color:#f92672">=</span><span style="color:#e6db74">true</span>
<span style="color:#a6e22e">local_scripts_path</span><span style="color:#f92672">=</span><span style="color:#e6db74">C:\Program Files\Cloudbase Solutions\Cloudbase-Init\LocalScripts\</span>
<span style="color:#a6e22e">check_latest_version</span><span style="color:#f92672">=</span><span style="color:#e6db74">true</span>
<span style="color:#a6e22e">metadata_services</span><span style="color:#f92672">=</span><span style="color:#e6db74">cloudbaseinit.metadata.services.configdrive.ConfigDriveService</span>
<span style="color:#a6e22e">plugins</span><span style="color:#f92672">=</span><span style="color:#e6db74">cloudbaseinit.plugins.common.networkconfig.NetworkConfigPlugin,
</span><span style="color:#e6db74">    cloudbaseinit.plugins.common.setuserpassword.SetUserPasswordPlugin,
</span><span style="color:#e6db74">    cloudbaseinit.plugins.common.userdata.UserDataPlugin,
</span><span style="color:#e6db74">    cloudbaseinit.plugins.common.sethostname.SetHostNamePlugin,
</span><span style="color:#e6db74">    cloudbaseinit.plugins.common.mtu.MTUPlugin,
</span><span style="color:#e6db74">    cloudbaseinit.plugins.windows.extendvolumes.ExtendVolumesPlugin</span>
</code></pre></div><p><strong>C:\Program Files\Cloudbase Solutions\cloudbase-init\conf\cloudbase-init-unattend.conf</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#66d9ef">[DEFAULT]</span>
<span style="color:#a6e22e">username</span><span style="color:#f92672">=</span><span style="color:#e6db74">Admin</span>
<span style="color:#a6e22e">groups</span><span style="color:#f92672">=</span><span style="color:#e6db74">Administrators</span>
<span style="color:#a6e22e">inject_user_password</span><span style="color:#f92672">=</span><span style="color:#e6db74">true</span>
<span style="color:#a6e22e">config_drive_raw_hhd</span><span style="color:#f92672">=</span><span style="color:#e6db74">true</span>
<span style="color:#a6e22e">config_drive_cdrom</span><span style="color:#f92672">=</span><span style="color:#e6db74">true</span>
<span style="color:#a6e22e">config_drive_vfat</span><span style="color:#f92672">=</span><span style="color:#e6db74">true</span>
<span style="color:#a6e22e">bsdtar_path</span><span style="color:#f92672">=</span><span style="color:#e6db74">C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\bsdtar.exe</span>
<span style="color:#a6e22e">mtools_path</span><span style="color:#f92672">=</span><span style="color:#e6db74">C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\</span>
<span style="color:#a6e22e">verbose</span><span style="color:#f92672">=</span><span style="color:#e6db74">true</span>
<span style="color:#a6e22e">debug</span><span style="color:#f92672">=</span><span style="color:#e6db74">true</span>
<span style="color:#a6e22e">logdir</span><span style="color:#f92672">=</span><span style="color:#e6db74">C:\Program Files\Cloudbase Solutions\Cloudbase-Init\log\</span>
<span style="color:#a6e22e">logfile</span><span style="color:#f92672">=</span><span style="color:#e6db74">cloudbase-init-unattend.log</span>
<span style="color:#a6e22e">default_log_levels</span><span style="color:#f92672">=</span><span style="color:#e6db74">comtypes=INFO,suds=INFO,iso8601=WARN,requests=WARN</span>
<span style="color:#a6e22e">logging_serial_port_settings</span><span style="color:#f92672">=</span><span style="color:#e6db74">COM1,115200,N,8</span>
<span style="color:#a6e22e">mtu_use_dhcp_config</span><span style="color:#f92672">=</span><span style="color:#e6db74">true</span>
<span style="color:#a6e22e">ntp_use_dhcp_config</span><span style="color:#f92672">=</span><span style="color:#e6db74">true</span>
<span style="color:#a6e22e">local_scripts_path</span><span style="color:#f92672">=</span><span style="color:#e6db74">C:\Program Files\Cloudbase Solutions\Cloudbase-Init\LocalScripts\</span>
<span style="color:#a6e22e">check_latest_version</span><span style="color:#f92672">=</span><span style="color:#e6db74">false</span>
<span style="color:#a6e22e">metadata_services</span><span style="color:#f92672">=</span><span style="color:#e6db74">cloudbaseinit.metadata.services.configdrive.ConfigDriveService</span>
<span style="color:#a6e22e">plugins</span><span style="color:#f92672">=</span><span style="color:#e6db74">cloudbaseinit.plugins.common.mtu.MTUPlugin,
</span><span style="color:#e6db74">    cloudbaseinit.plugins.common.sethostname.SetHostNamePlugin,
</span><span style="color:#e6db74">    cloudbaseinit.plugins.windows.extendvolumes.ExtendVolumesPlugin,
</span><span style="color:#e6db74">    cloudbaseinit.plugins.common.networkconfig.NetworkConfigPlugin,
</span><span style="color:#e6db74">    cloudbaseinit.plugins.common.userdata.UserDataPlugin</span>
<span style="color:#a6e22e">allow_reboot</span><span style="color:#f92672">=</span><span style="color:#e6db74">false</span>
<span style="color:#a6e22e">stop_service_on_exit</span><span style="color:#f92672">=</span><span style="color:#e6db74">false</span>
</code></pre></div><p>You can either edit these files in <code>notepad</code>, or you can use SCP (as we enabled SSH already) to transfer the files from another machine.</p>
<p>We also set all Cloudbase-Init logs to be exposed on the Serial port, allowing us to see the changes being made by Cloudbase-Init (as well as any errors/warnings) separately from the boot and login process itself on screen/VNC.</p>
<p>After this is done, you can then run <code>sysprep</code>, which &ldquo;generalizes&rdquo; a Windows image. This removes install-specific information (e.g. passwords, unique IDs etc) in preparation for the image to be reused/cloned. The Cloudbase-Init installation also includes an <code>Unattend</code> answer file. This is similar to a Kickstart or PXE script in the Linux world, providing answers to questions usually presented to users during initial setup.</p>
<p>The included <code>Unattend</code> file looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml"><span style="color:#75715e">&lt;?xml version=&#34;1.0&#34; encoding=&#34;utf-8&#34;?&gt;</span>
<span style="color:#f92672">&lt;unattend</span> <span style="color:#a6e22e">xmlns=</span><span style="color:#e6db74">&#34;urn:schemas-microsoft-com:unattend&#34;</span><span style="color:#f92672">&gt;</span>
  <span style="color:#f92672">&lt;settings</span> <span style="color:#a6e22e">pass=</span><span style="color:#e6db74">&#34;generalize&#34;</span><span style="color:#f92672">&gt;</span>
    <span style="color:#f92672">&lt;component</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;Microsoft-Windows-PnpSysprep&#34;</span> <span style="color:#a6e22e">processorArchitecture=</span><span style="color:#e6db74">&#34;amd64&#34;</span> <span style="color:#a6e22e">publicKeyToken=</span><span style="color:#e6db74">&#34;31bf3856ad364e35&#34;</span> <span style="color:#a6e22e">language=</span><span style="color:#e6db74">&#34;neutral&#34;</span> <span style="color:#a6e22e">versionScope=</span><span style="color:#e6db74">&#34;nonSxS&#34;</span> <span style="color:#960050;background-color:#1e0010">xm</span>
<span style="color:#a6e22e">lns:wcm=</span><span style="color:#e6db74">&#34;http://schemas.microsoft.com/WMIConfig/2002/State&#34;</span> <span style="color:#a6e22e">xmlns:xsi=</span><span style="color:#e6db74">&#34;http://www.w3.org/2001/XMLSchema-instance&#34;</span><span style="color:#f92672">&gt;</span>
      <span style="color:#f92672">&lt;PersistAllDeviceInstalls&gt;</span>true<span style="color:#f92672">&lt;/PersistAllDeviceInstalls&gt;</span>
    <span style="color:#f92672">&lt;/component&gt;</span>
  <span style="color:#f92672">&lt;/settings&gt;</span>
  <span style="color:#f92672">&lt;settings</span> <span style="color:#a6e22e">pass=</span><span style="color:#e6db74">&#34;oobeSystem&#34;</span><span style="color:#f92672">&gt;</span>
    <span style="color:#f92672">&lt;component</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;Microsoft-Windows-Shell-Setup&#34;</span> <span style="color:#a6e22e">processorArchitecture=</span><span style="color:#e6db74">&#34;amd64&#34;</span> <span style="color:#a6e22e">publicKeyToken=</span><span style="color:#e6db74">&#34;31bf3856ad364e35&#34;</span> <span style="color:#a6e22e">language=</span><span style="color:#e6db74">&#34;neutral&#34;</span> <span style="color:#a6e22e">versionScope=</span><span style="color:#e6db74">&#34;nonSxS&#34;</span> <span style="color:#960050;background-color:#1e0010">x</span>
<span style="color:#a6e22e">mlns:wcm=</span><span style="color:#e6db74">&#34;http://schemas.microsoft.com/WMIConfig/2002/State&#34;</span><span style="color:#f92672">&gt;</span>
      <span style="color:#f92672">&lt;OOBE&gt;</span>
        <span style="color:#f92672">&lt;HideEULAPage&gt;</span>true<span style="color:#f92672">&lt;/HideEULAPage&gt;</span>
        <span style="color:#f92672">&lt;NetworkLocation&gt;</span>Work<span style="color:#f92672">&lt;/NetworkLocation&gt;</span>
        <span style="color:#f92672">&lt;ProtectYourPC&gt;</span>1<span style="color:#f92672">&lt;/ProtectYourPC&gt;</span>
        <span style="color:#f92672">&lt;SkipMachineOOBE&gt;</span>true<span style="color:#f92672">&lt;/SkipMachineOOBE&gt;</span>
        <span style="color:#f92672">&lt;SkipUserOOBE&gt;</span>true<span style="color:#f92672">&lt;/SkipUserOOBE&gt;</span>
      <span style="color:#f92672">&lt;/OOBE&gt;</span>
    <span style="color:#f92672">&lt;/component&gt;</span>
  <span style="color:#f92672">&lt;/settings&gt;</span>
  <span style="color:#f92672">&lt;settings</span> <span style="color:#a6e22e">pass=</span><span style="color:#e6db74">&#34;specialize&#34;</span><span style="color:#f92672">&gt;</span>
    <span style="color:#f92672">&lt;component</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;Microsoft-Windows-Deployment&#34;</span> <span style="color:#a6e22e">processorArchitecture=</span><span style="color:#e6db74">&#34;amd64&#34;</span> <span style="color:#a6e22e">publicKeyToken=</span><span style="color:#e6db74">&#34;31bf3856ad364e35&#34;</span> <span style="color:#a6e22e">language=</span><span style="color:#e6db74">&#34;neutral&#34;</span> <span style="color:#a6e22e">versionScope=</span><span style="color:#e6db74">&#34;nonSxS&#34;</span> <span style="color:#960050;background-color:#1e0010">xm</span>
<span style="color:#a6e22e">lns:wcm=</span><span style="color:#e6db74">&#34;http://schemas.microsoft.com/WMIConfig/2002/State&#34;</span> <span style="color:#a6e22e">xmlns:xsi=</span><span style="color:#e6db74">&#34;http://www.w3.org/2001/XMLSchema-instance&#34;</span><span style="color:#f92672">&gt;</span>
      <span style="color:#f92672">&lt;RunSynchronous&gt;</span>
        <span style="color:#f92672">&lt;RunSynchronousCommand</span> <span style="color:#a6e22e">wcm:action=</span><span style="color:#e6db74">&#34;add&#34;</span><span style="color:#f92672">&gt;</span>
          <span style="color:#f92672">&lt;Order&gt;</span>1<span style="color:#f92672">&lt;/Order&gt;</span>
          <span style="color:#f92672">&lt;Path&gt;</span>cmd.exe /c &#34;&#34;C:\Program Files\Cloudbase Solutions\Cloudbase-Init\Python\Scripts\cloudbase-init.exe&#34; --config-file &#34;C:\Program Files\Cloudbase
Solutions\Cloudbase-Init\conf\cloudbase-init-unattend.conf&#34; &amp;amp;&amp;amp; exit 1 || exit 2&#34;<span style="color:#f92672">&lt;/Path&gt;</span>
          <span style="color:#f92672">&lt;Description&gt;</span>Run Cloudbase-Init to set the hostname<span style="color:#f92672">&lt;/Description&gt;</span>
          <span style="color:#f92672">&lt;WillReboot&gt;</span>OnRequest<span style="color:#f92672">&lt;/WillReboot&gt;</span>
        <span style="color:#f92672">&lt;/RunSynchronousCommand&gt;</span>
      <span style="color:#f92672">&lt;/RunSynchronous&gt;</span>
    <span style="color:#f92672">&lt;/component&gt;</span>
  <span style="color:#f92672">&lt;/settings&gt;</span>
<span style="color:#f92672">&lt;/unattend&gt;</span>
</code></pre></div><p>To start the <code>sysprep</code> process using this <code>Unattend</code> file, run: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-powershell" data-lang="powershell"><span style="color:#75715e"># Go to the Cloudbase-Init directory</span>
PS C:\&gt; cd <span style="color:#e6db74">&#39;C:\Program Files\Cloudbase Solutions\Cloudbase-Init\conf&#39;</span>

<span style="color:#75715e"># Run sysprep using the unattend.xml</span>
PS C:\Program Files\Cloudbase Solutions\Cloudbase-Init\conf&gt; c:\Windows\System32\Sysprep\sysprep.exe /generalize /oobe  /unattend<span style="color:#960050;background-color:#1e0010">:</span>Unattend.xml
</code></pre></div><p>This will prepare the instance and tell the system to run <code>cloudbase-init.exe</code> on boot.</p>
<h3 id="creating-a-template">Creating a template</h3>
<p>There are two ways to convert the machine into a template in Proxmox. The first is to right click on the created machine, and select <strong>Convert to template</strong>: -</p>
<p><img src="/img/proxmox/proxmox-win-convert-to-template.png" alt="Proxmox Windows Convert to Template"></p>
<p>The other is to find the ID of the machine in the Proxmox CLI, and then do the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># Retrieve the ID</span>
root@pve-01:~# qm list
      VMID NAME                 STATUS     MEM<span style="color:#f92672">(</span>MB<span style="color:#f92672">)</span>    BOOTDISK<span style="color:#f92672">(</span>GB<span style="color:#f92672">)</span> PID
      <span style="color:#ae81ff">9003</span> win2k19-cloudinit    stopped    <span style="color:#ae81ff">2048</span>              32.00 <span style="color:#ae81ff">0</span>

<span style="color:#75715e"># Convert to a template</span>
root@pve-01:~# qm template <span style="color:#ae81ff">9003</span>
</code></pre></div><p>We can now use this template when defining an instance within Terraform</p>
<h2 id="terraform">Terraform</h2>
<h3 id="create-the-cloud-init-template">Create the Cloud-Init template</h3>
<p>The first step to building an instance using Terraform is to prepare the Cloud-Init template.</p>
<p>There are some caveats to be aware of before we start: -</p>
<ul>
<li>Currently not all <code>cloud-config</code> modules are supported in Cloudbase-Init</li>
<li>The format that Proxmox provides some of the Cloud-Init data is not 100% compatible with Cloudbase-Init</li>
</ul>
<p>In regards to the first, Cloudbase-Init supports the following <code>cloud-config</code> modules (at the time of writing): -</p>
<ul>
<li><code>write_files</code> - Creating files (e.g. configuration files)</li>
<li><code>set_timezone</code></li>
<li><code>set_hostname</code></li>
<li><code>groups</code> - Creating local groups on the machine, as well as assigning existing users to them</li>
<li><code>users</code> - Creating local users</li>
<li><code>ntp</code> - Setting NTP servers</li>
<li><code>runcmd</code> - Running a set of arbitrary commands, executed with <code>cmd.exe</code> (not Powershell)</li>
</ul>
<p>In regards to the latter, there is an issue with how Proxmox provides the DNS Nameservers. Proxmox provides <code>dns_nameservers</code> as the option, whereas Cloudbase-Init expects <code>dns-nameservers</code> (associated <a href="https://github.com/cloudbase/cloudbase-init/issues/56">GitHub Issue</a>). This issue, combined with Cloudbase-Init not supporting <code>cloud-config</code> network module (to customize our network configuration)  means that we must rely on DHCP.</p>
<p>In most cloud environments, IP addressing normally ephemeral (i.e. not persistent). If you treat the instance in this manner, then DHCP provides the same sort of functionality. However if you are expecting to be able to use static addressed instances, then you may need to look at other methods than <code>cloud-config</code>.</p>
<p>The basic template we are going to use looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">Content-Type</span>: multipart/mixed; boundary=<span style="color:#e6db74">&#34;===============1598784645116016685==&#34;</span>
<span style="color:#66d9ef">MIME-Version</span>: <span style="color:#ae81ff">1.0</span>

--===============<span style="color:#ae81ff">1598784645116016685</span>==
<span style="color:#66d9ef">Content-Type</span>: text/cloud-config; charset=<span style="color:#e6db74">&#34;us-ascii&#34;</span>
<span style="color:#66d9ef">MIME-Version</span>: <span style="color:#ae81ff">1.0</span>
<span style="color:#66d9ef">Content-Transfer-Encoding</span>: 7bit
<span style="color:#66d9ef">Content-Disposition</span>: attachment; filename=<span style="color:#e6db74">&#34;cloud-config&#34;</span>


<span style="color:#66d9ef">write_files</span>:
  - <span style="color:#66d9ef">path</span>: c:\salt\conf\minion
    <span style="color:#66d9ef">content</span>: <span style="color:#e6db74">|
</span><span style="color:#e6db74">       master: salt-master.${domain}</span>
       <span style="color:#66d9ef">id</span>: ${hostname}.${domain}
       <span style="color:#66d9ef">nodename</span>: ${hostname}
       <span style="color:#66d9ef">startup_states</span>: highstate

--===============<span style="color:#ae81ff">1598784645116016685</span>==
<span style="color:#66d9ef">Content-Type</span>: text/x-cfninitdata; charset=<span style="color:#e6db74">&#34;us-ascii&#34;</span>
<span style="color:#66d9ef">MIME-Version</span>: <span style="color:#ae81ff">1.0</span>
<span style="color:#66d9ef">Content-Transfer-Encoding</span>: 7bit
<span style="color:#66d9ef">Content-Disposition</span>: attachment; filename=<span style="color:#e6db74">&#34;cfn-userdata&#34;</span>

<span style="color:#75715e">#ps1</span>

Restart-Service salt-minion
</code></pre></div><p>We use a feature called <a href="https://cloudbase-init.readthedocs.io/en/latest/userdata.html#multi-part-content">multi-part content</a> to leverage both <code>cloud-config</code> and Powershell to configure the instance. You could extend this to use more complex Powershell scripts, setting the timezone, creating users and more. In our file, we update the Salt Minion configuration (using the <code>write_files</code> module in <code>cloud-config</code>), and then restart the <code>salt-minion</code> service (using Powershell).</p>
<h3 id="define-the-instance">Define the instance</h3>
<p>The instance is defined similarly to the instances in <a href="/posts/proxmox-terraform-cloudinit-saltstack-prometheus/#define-an-instance">the previous post</a>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#75715e"># Source the Cloud Init Config file
</span><span style="color:#75715e"></span><span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;template_file&#34; &#34;cloud_init_win2k19_vm-01&#34;</span> {
  template  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${file(&#34;${path.module}/files/cloud_init_win2k19.cloud_config&#34;)}&#34;</span>

  vars <span style="color:#f92672">=</span> {
    hostname <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;win2k19-vm-01&#34;</span>
    domain <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops.lab&#34;</span>
  }
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e"># Create a local copy of the file, to transfer to Proxmox
</span><span style="color:#75715e"></span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;local_file&#34; &#34;cloud_init_win2k19_vm-01&#34;</span> {
  content   <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">template_file</span>.<span style="color:#66d9ef">cloud_init_win2k19_vm</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">01</span>.<span style="color:#66d9ef">rendered</span>
  filename  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${path.module}/files/user_data_cloud_init_win2k19_vm-01.cfg&#34;</span>
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e"># Transfer the file to the Proxmox Host
</span><span style="color:#75715e"></span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;null_resource&#34; &#34;cloud_init_win2k19_vm-01&#34;</span> {
  <span style="color:#66d9ef">connection</span> {
    type    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ssh&#34;</span>
    user    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;root&#34;</span>
    private_key <span style="color:#f92672">=</span> <span style="color:#66d9ef">file</span>(<span style="color:#e6db74">&#34;~/.ssh/id_rsa&#34;</span>)
    host    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;pve-01.yetiops.lab&#34;</span>
  }

  <span style="color:#66d9ef">provisioner</span> <span style="color:#e6db74">&#34;file&#34;</span> {
    source       <span style="color:#f92672">=</span> <span style="color:#66d9ef">local_file</span>.<span style="color:#66d9ef">cloud_init_win2k19_vm</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">01</span>.<span style="color:#66d9ef">filename</span>
    destination  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/var/lib/vz/snippets/cloud_init_win2k19_vm-01.yml&#34;</span>
  }
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e"># Create the VM
</span><span style="color:#75715e"></span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;proxmox_vm_qemu&#34; &#34;win2k19-vm&#34;</span> {<span style="color:#75715e">
</span><span style="color:#75715e">  ## Wait for the cloud-config file to exist
</span><span style="color:#75715e"></span>
    depends_on <span style="color:#f92672">=</span> [
    <span style="color:#66d9ef">null_resource</span>.<span style="color:#66d9ef">cloud_init_win2k19_vm</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">01</span>
  ]

  name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;win2k19-vm-01&#34;</span>
  target_node <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;pve-01&#34;</span><span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">  # Clone from debian-cloudinit template
</span><span style="color:#75715e"></span>  clone <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;win2k19-cloudinit&#34;</span><span style="color:#75715e">
</span><span style="color:#75715e">  #os_type = &#34;cloud-init&#34;
</span><span style="color:#75715e">
</span><span style="color:#75715e">  # Cloud init options
</span><span style="color:#75715e"></span>  cicustom <span style="color:#f92672">=</span> &#34;user<span style="color:#f92672">=</span><span style="color:#66d9ef">local</span><span style="color:#960050;background-color:#1e0010">:</span><span style="color:#66d9ef">snippets</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">cloud_init_win2k19_vm</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">01</span>.<span style="color:#66d9ef">yml</span><span style="color:#960050;background-color:#1e0010">&#34;</span>
  ipconfig0 <span style="color:#f92672">=</span> &#34;ip<span style="color:#f92672">=</span><span style="color:#66d9ef">dhcp</span><span style="color:#960050;background-color:#1e0010">&#34;</span>

  memory       <span style="color:#f92672">=</span> <span style="color:#ae81ff">2048</span>
  agent        <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span><span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">  # Set the boot disk paramters
</span><span style="color:#75715e"></span>  bootdisk <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;scsi0&#34;</span>
  scsihw       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;virtio-scsi-pci&#34;</span>

  <span style="color:#66d9ef">disk</span> {
    id              <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
    size            <span style="color:#f92672">=</span> <span style="color:#ae81ff">20</span>
    type            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;scsi&#34;</span>
    storage         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;local-lvm&#34;</span>
    storage_type    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;lvm&#34;</span>
    iothread        <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
  }<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">  # Set the network
</span><span style="color:#75715e"></span>  <span style="color:#66d9ef">network</span> {
    id <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
    model <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;virtio&#34;</span>
    bridge <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vmbr0&#34;</span>
  }<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">  # Ignore changes to the network
</span><span style="color:#75715e">  ## MAC address is generated on every apply, causing
</span><span style="color:#75715e">  ## TF to think this needs to be rebuilt on every apply
</span><span style="color:#75715e"></span>  <span style="color:#66d9ef">lifecycle</span> {
     ignore_changes <span style="color:#f92672">=</span> [
       <span style="color:#66d9ef">network</span>
     ]
  }
}
</code></pre></div><p>One notable difference is that we set the <code>ipconfig0 = &quot;ip=dhcp&quot;</code> option. Without this, the Terraform provider will <a href="https://github.com/Telmate/terraform-provider-proxmox/issues/186">crash</a>. We should be able to provide a static IP here, but as noted previously, the format Cloudbase-Init provides DNS nameservers in is not compatible with Cloudbase-Init, meaning that we would not be able to access any hostname-based service (e.g. our Salt server, or downloading dependencies).</p>
<p>Other than this, we should now be able to build a Windows instance.</p>
<h3 id="build-the-instance">Build the instance</h3>
<p>Now that we have defined the instance within Terraform, we can go ahead and build it: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ terraform apply
data.template_file.cloud_init_win2k19_vm-01: Refreshing state...

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  <span style="color:#75715e"># local_file.cloud_init_win2k19_vm-01 will be created</span>
  + resource <span style="color:#e6db74">&#34;local_file&#34;</span> <span style="color:#e6db74">&#34;cloud_init_win2k19_vm-01&#34;</span> <span style="color:#f92672">{</span>
      + content              <span style="color:#f92672">=</span> &lt;&lt;~EOT
            Content-Type: multipart/mixed; boundary<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;===============1598784645116016685==&#34;</span>
            MIME-Version: 1.0

            --<span style="color:#f92672">===============</span>1598784645116016685<span style="color:#f92672">==</span>
            Content-Type: text/cloud-config; charset<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;us-ascii&#34;</span>
            MIME-Version: 1.0
            Content-Transfer-Encoding: 7bit
            Content-Disposition: attachment; filename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;cloud-config&#34;</span>


            write_files:
              - path: c:<span style="color:#ae81ff">\s</span>alt<span style="color:#ae81ff">\c</span>onf<span style="color:#ae81ff">\m</span>inion
                content: |
                   master: salt-master.yetiops.lab
                   id: win2k19-vm-01.yetiops.lab
                   nodename: win2k19-vm-01
                   startup_states: highstate

            --<span style="color:#f92672">===============</span>1598784645116016685<span style="color:#f92672">==</span>
            Content-Type: text/x-cfninitdata; charset<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;us-ascii&#34;</span>
            MIME-Version: 1.0
            Content-Transfer-Encoding: 7bit
            Content-Disposition: attachment; filename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;cfn-userdata&#34;</span>

            <span style="color:#75715e">#ps1</span>

            Restart-Service salt-minion
        EOT
      + directory_permission <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0777&#34;</span>
      + file_permission      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0777&#34;</span>
      + filename             <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;./files/user_data_cloud_init_win2k19_vm-01.cfg&#34;</span>
      + id                   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># null_resource.cloud_init_win2k19_vm-01 will be created</span>
  + resource <span style="color:#e6db74">&#34;null_resource&#34;</span> <span style="color:#e6db74">&#34;cloud_init_win2k19_vm-01&#34;</span> <span style="color:#f92672">{</span>
      + id <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># proxmox_vm_qemu.win2k19-vm will be created</span>
  + resource <span style="color:#e6db74">&#34;proxmox_vm_qemu&#34;</span> <span style="color:#e6db74">&#34;win2k19-vm&#34;</span> <span style="color:#f92672">{</span>
      + agent        <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>
      + balloon      <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
      + bios         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;seabios&#34;</span>
      + boot         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cdn&#34;</span>
      + bootdisk     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;scsi0&#34;</span>
      + cicustom     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;user=local:snippets/cloud_init_win2k19_vm-01.yml&#34;</span>
      + clone        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;win2k19-cloudinit&#34;</span>
      + clone_wait   <span style="color:#f92672">=</span> <span style="color:#ae81ff">15</span>
      + cores        <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>
      + cpu          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;host&#34;</span>
      + force_create <span style="color:#f92672">=</span> false
      + full_clone   <span style="color:#f92672">=</span> true
      + hotplug      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;network,disk,usb&#34;</span>
      + id           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + ipconfig0    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ip=dhcp&#34;</span>
      + kvm          <span style="color:#f92672">=</span> true
      + memory       <span style="color:#f92672">=</span> <span style="color:#ae81ff">2048</span>
      + name         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;win2k19-vm-01&#34;</span>
      + numa         <span style="color:#f92672">=</span> false
      + onboot       <span style="color:#f92672">=</span> true
      + preprovision <span style="color:#f92672">=</span> true
      + scsihw       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;virtio-scsi-pci&#34;</span>
      + sockets      <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>
      + ssh_host     <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + ssh_port     <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + target_node  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;pve-01&#34;</span>
      + vcpus        <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
      + vlan         <span style="color:#f92672">=</span> -1
      + vmid         <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>

      + disk <span style="color:#f92672">{</span>
          + backup       <span style="color:#f92672">=</span> false
          + cache        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;none&#34;</span>
          + format       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;raw&#34;</span>
          + id           <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
          + iothread     <span style="color:#f92672">=</span> true
          + mbps         <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
          + mbps_rd      <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
          + mbps_rd_max  <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
          + mbps_wr      <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
          + mbps_wr_max  <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
          + replicate    <span style="color:#f92672">=</span> false
          + size         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;20&#34;</span>
          + storage      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;local-lvm&#34;</span>
          + storage_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;lvm&#34;</span>
          + type         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;scsi&#34;</span>
        <span style="color:#f92672">}</span>

      + network <span style="color:#f92672">{</span>
          + bridge    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vmbr0&#34;</span>
          + firewall  <span style="color:#f92672">=</span> false
          + id        <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
          + link_down <span style="color:#f92672">=</span> false
          + macaddr   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + model     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;virtio&#34;</span>
          + queues    <span style="color:#f92672">=</span> -1
          + rate      <span style="color:#f92672">=</span> -1
          + tag       <span style="color:#f92672">=</span> -1
        <span style="color:#f92672">}</span>
    <span style="color:#f92672">}</span>

Plan: <span style="color:#ae81ff">3</span> to add, <span style="color:#ae81ff">0</span> to change, <span style="color:#ae81ff">0</span> to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only <span style="color:#e6db74">&#39;yes&#39;</span> will be accepted to approve.

  Enter a value: yes

local_file.cloud_init_win2k19_vm-01: Creating...
local_file.cloud_init_win2k19_vm-01: Creation complete after 0s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>66f7140731ba60729758ed365fed0ba7a10f3766<span style="color:#f92672">]</span>
null_resource.cloud_init_win2k19_vm-01: Creating...
null_resource.cloud_init_win2k19_vm-01: Provisioning with <span style="color:#e6db74">&#39;file&#39;</span>...
null_resource.cloud_init_win2k19_vm-01: Creation complete after 0s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>219014806438120636<span style="color:#f92672">]</span>
proxmox_vm_qemu.win2k19-vm: Creating...
proxmox_vm_qemu.win2k19-vm: Still creating... <span style="color:#f92672">[</span>10s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.win2k19-vm: Still creating... <span style="color:#f92672">[</span>20s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.win2k19-vm: Still creating... <span style="color:#f92672">[</span>30s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.win2k19-vm: Still creating... <span style="color:#f92672">[</span>40s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.win2k19-vm: Still creating... <span style="color:#f92672">[</span>50s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.win2k19-vm: Still creating... <span style="color:#f92672">[</span>1m0s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.win2k19-vm: Still creating... <span style="color:#f92672">[</span>1m10s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.win2k19-vm: Still creating... <span style="color:#f92672">[</span>1m20s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.win2k19-vm: Still creating... <span style="color:#f92672">[</span>1m30s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.win2k19-vm: Still creating... <span style="color:#f92672">[</span>1m40s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.win2k19-vm: Still creating... <span style="color:#f92672">[</span>1m50s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.win2k19-vm: Still creating... <span style="color:#f92672">[</span>2m0s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.win2k19-vm: Still creating... <span style="color:#f92672">[</span>2m10s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.win2k19-vm: Still creating... <span style="color:#f92672">[</span>2m20s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.win2k19-vm: Still creating... <span style="color:#f92672">[</span>2m30s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.win2k19-vm: Still creating... <span style="color:#f92672">[</span>2m40s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.win2k19-vm: Still creating... <span style="color:#f92672">[</span>2m50s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.win2k19-vm: Still creating... <span style="color:#f92672">[</span>3m0s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.win2k19-vm: Still creating... <span style="color:#f92672">[</span>3m10s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.win2k19-vm: Still creating... <span style="color:#f92672">[</span>3m20s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.win2k19-vm: Still creating... <span style="color:#f92672">[</span>3m30s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.win2k19-vm: Still creating... <span style="color:#f92672">[</span>3m40s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.win2k19-vm: Still creating... <span style="color:#f92672">[</span>3m50s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.win2k19-vm: Still creating... <span style="color:#f92672">[</span>4m0s elapsed<span style="color:#f92672">]</span>

Error: error unmarshalling result Could not parse fe80::dc7e:d119:c72d:dbac%2 as IP

  on win-vm.tf line 34, in resource <span style="color:#e6db74">&#34;proxmox_vm_qemu&#34;</span> <span style="color:#e6db74">&#34;win2k19-vm&#34;</span>:
  34: resource <span style="color:#e6db74">&#34;proxmox_vm_qemu&#34;</span> <span style="color:#e6db74">&#34;win2k19-vm&#34;</span> <span style="color:#f92672">{</span>
</code></pre></div><p>The last part of this is due to running the Qemu Guest Agent. Proxmox sees that the IP address return is an IPv6 address, and returns it to the Terraform provider. Unfortunately the provider is expecting either an IPv4 address, or an IPv6 address without <code>%2</code> at the end of it. However if we look in the Terraform state, we are still managing this machine: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ terraform state list
data.template_file.cloud_init_win2k19_vm-01
local_file.cloud_init_win2k19_vm-01
null_resource.cloud_init_win2k19_vm-01
proxmox_vm_qemu.win2k19-vm
</code></pre></div><p>We can also verify it exists in the Proxmox console: -</p>
<p><img src="/img/proxmox/proxmox-win-tf-created-instance.png" alt="Proxmox Terraform Windows Instance created"></p>
<p>It appears that while the <code>terraform apply</code> operation produces an error, it is after all configuration has taken place. Terraform does however mark the instance as <strong>tainted</strong> (i.e. required to be destroyed and rebuilt). To avoid rebuilding the instance on your next <code>terraform apply</code>, run <code>terraform untaint proxmox_vm_qemu.win2k19-vm</code> to untaint it. You can then verify with a <code>terraform plan</code> that instance matches what we defined: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># Untaint the instance</span>
$ terraform untaint proxmox_vm_qemu.win2k19-vm
Resource instance proxmox_vm_qemu.win2k19-vm has been successfully untainted.

<span style="color:#75715e"># Run a plan to ensure no changes are required</span>
$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

data.template_file.cloud_init_win2k19_vm-01: Refreshing state...
local_file.cloud_init_win2k19_vm-01: Refreshing state... <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>66f7140731ba60729758ed365fed0ba7a10f3766<span style="color:#f92672">]</span>
null_resource.cloud_init_win2k19_vm-01: Refreshing state... <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>219014806438120636<span style="color:#f92672">]</span>
proxmox_vm_qemu.win2k19-vm: Refreshing state... <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>pve-01/qemu/100<span style="color:#f92672">]</span>
</code></pre></div><p>Alternatively, if you do not install the Qemu Guest Agent, then the Terraform provider will complete without issues. It is worthwhile running the Guest Agent (safer shutdowns, exposes information to the hypervisor regarding the instance), but it is not necessary to the day to day running of the machine.</p>
<h2 id="salt">Salt</h2>
<p>The Salt setup and states are the same as we used in <a href="/posts/prometheus-consul-saltstack-part-2-windows/">this post</a>. We use Salt to: -</p>
<ul>
<li>Deploy Consul as an agent</li>
<li>Register with the Consul server (the Salt server)</li>
<li>Deploy the Windows Exporter so that Prometheus can monitor the instance</li>
</ul>
<p>As per the previous post, we also run a highstate (i.e. all applicable Salt states) when the Minion is accepted by the Salt server.</p>
<h3 id="accept-the-key">Accept the key</h3>
<p>To accept the minion on the master, we need to check that it has registered first: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo salt-key -L
Accepted Keys:
pve-01.yetiops.lab
salt-master.yetiops.lab
Denied Keys:
Unaccepted Keys:
win2k19-vm-01.yetiops.lab
Rejected Keys:
</code></pre></div><p>Now we can accept the key: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo salt-key -a <span style="color:#e6db74">&#39;win2k19*&#39;</span>
The following keys are going to be accepted:
Unaccepted Keys:
win2k19-vm-01.yetiops.lab
Proceed? <span style="color:#f92672">[</span>n/Y<span style="color:#f92672">]</span> y
Key <span style="color:#66d9ef">for</span> minion win2k19-vm-01.yetiops.lab accepted.
</code></pre></div><p>We can check what states will be applied to this minion as well: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo salt <span style="color:#e6db74">&#39;win2k19-vm-01*&#39;</span> state.show_states
win2k19-vm-01.yetiops.lab:
    - consul.windows
    - exporters.windows_exporter.win_exporter
    - exporters.windows_exporter.windows_exporter
</code></pre></div><h3 id="verify-the-highstate">Verify the highstate</h3>
<p>As mentioned, the Minion configuration specifies that it will run a highstate when the key is accepted. We can verify this seeing if the instance has registered with Consul and that the Windows Exporter is running: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ consul members
Node             Address            Status  Type    Build  Protocol  DC       Segment
salt-master      10.15.31.249:8301  alive   server  1.8.1  <span style="color:#ae81ff">2</span>         yetiops  &lt;all&gt;
WIN-75R8JQ6IARE  10.15.31.166:8301  alive   client  1.7.2  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
pve-01           10.15.31.7:8301    alive   client  1.8.1  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;

$ curl 10.15.31.166:9182/metrics | grep -i <span style="color:#ae81ff">2019</span>
windows_os_info<span style="color:#f92672">{</span>product<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Microsoft Windows Server 2019 Standard Evaluation&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;10.0.17763&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>
</code></pre></div><p>Looks like they are working! It is worth nothing that we do not set a hostname in <code>cloud-config</code> for our machine, so it uses a randomly generated hostname instead.</p>
<p>As a last step of verification, we can run another highstate to ensure that no configuration files change, or any other packages are due to be installed: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo salt <span style="color:#e6db74">&#39;win2k19*&#39;</span> state.highstate
win2k19-vm-01.yetiops.lab:
----------
          ID: consul_user
    Function: user.present
        Name: Consul
      Result: True
     Comment: User Consul is present and up to date
     Started: 20:04:56.229276
    Duration: 29925.002 ms
     Changes:
----------
          ID: consul_base_dir
    Function: file.directory
        Name: C:<span style="color:#ae81ff">\c</span>onsul
      Result: True
     Comment: Directory C:<span style="color:#ae81ff">\c</span>onsul updated
     Started: 20:05:26.157279
    Duration: 24.024 ms
     Changes:
              ----------
              grant_perms:
                  ----------
                  Users:
                      ----------
                      perms:
                          full_control
              perms:
                  ----------
                  Users:
                      ----------
                      grant:
                          full_control
----------
          ID: consul_conf_dir
    Function: file.directory
        Name: C:<span style="color:#ae81ff">\c</span>onsul<span style="color:#ae81ff">\c</span>onf
      Result: True
     Comment: Directory C:<span style="color:#ae81ff">\c</span>onsul<span style="color:#ae81ff">\c</span>onf updated
     Started: 20:05:26.181303
    Duration: 25.994 ms
     Changes:
              ----------
              grant_perms:
                  ----------
                  Users:
                      ----------
                      perms:
                          full_control
              perms:
                  ----------
                  Users:
                      ----------
                      grant:
                          full_control
----------
          ID: consul_data_dir
    Function: file.directory
        Name: C:<span style="color:#ae81ff">\c</span>onsul<span style="color:#ae81ff">\d</span>ata
      Result: True
     Comment: Directory C:<span style="color:#ae81ff">\c</span>onsul<span style="color:#ae81ff">\d</span>ata updated
     Started: 20:05:26.208305
    Duration: 13.041 ms
     Changes:
              ----------
              grant_perms:
                  ----------
                  Users:
                      ----------
                      perms:
                          full_control
              perms:
                  ----------
                  Users:
                      ----------
                      grant:
                          full_control
----------
          ID: c:<span style="color:#ae81ff">\c</span>onsul<span style="color:#ae81ff">\c</span>onf<span style="color:#ae81ff">\c</span>onsul.hcl
    Function: file.managed
      Result: True
     Comment: File c:<span style="color:#ae81ff">\c</span>onsul<span style="color:#ae81ff">\c</span>onf<span style="color:#ae81ff">\c</span>onsul.hcl is in the correct state
     Started: 20:05:26.227289
    Duration: 795.02 ms
     Changes:
----------
          ID: modify_consul_service
    Function: module.run
        Name: service.modify
      Result: True
     Comment: Module <span style="color:#66d9ef">function</span> service.modify executed
     Started: 20:05:27.022309
    Duration: 23.919 ms
     Changes:
              ----------
              ret:
                  ----------
                  BinaryPath:
                      <span style="color:#e6db74">&#34;c:\consul\consul.exe&#34;</span> agent -config-dir<span style="color:#f92672">=</span>c:<span style="color:#ae81ff">\c</span>onsul<span style="color:#ae81ff">\c</span>onf
                  DisplayName:
                      HashiCorp Consul Client Agent
                  StartType:
                      Auto
----------
          ID: running_consul_service
    Function: service.running
        Name: consul
      Result: True
     Comment: The service consul is already running
     Started: 20:05:27.046228
    Duration: 5.0 ms
     Changes:
----------
          ID: c:<span style="color:#ae81ff">\c</span>onsul<span style="color:#ae81ff">\c</span>onf<span style="color:#ae81ff">\w</span>indows_exporter.hcl
    Function: file.managed
      Result: True
     Comment: File c:<span style="color:#ae81ff">\c</span>onsul<span style="color:#ae81ff">\c</span>onf<span style="color:#ae81ff">\w</span>indows_exporter.hcl is in the correct state
     Started: 20:05:27.051228
    Duration: 96.359 ms
     Changes:
----------
          ID: consul_reload_windows_exporter
    Function: cmd.run
        Name: c:<span style="color:#ae81ff">\c</span>onsul<span style="color:#ae81ff">\c</span>onsul.exe reload
      Result: True
     Comment: Command <span style="color:#e6db74">&#34;c:\consul\consul.exe reload&#34;</span> run
     Started: 20:05:27.149588
    Duration: 6710.107 ms
     Changes:
              ----------
              pid:
                  <span style="color:#ae81ff">1208</span>
              retcode:
                  <span style="color:#ae81ff">0</span>
              stderr:
              stdout:
                  Configuration reload triggered
----------
          ID: exporter_user
    Function: user.present
        Name: exporter
      Result: True
     Comment: User exporter is present and up to date
     Started: 20:05:33.859695
    Duration: 93.698 ms
     Changes:
----------
          ID: exporter_base_dir
    Function: file.directory
        Name: C:<span style="color:#ae81ff">\e</span>xporter
      Result: True
     Comment: Directory C:<span style="color:#ae81ff">\e</span>xporter updated
     Started: 20:05:33.953393
    Duration: 15.64 ms
     Changes:
              ----------
              grant_perms:
                  ----------
                  Users:
                      ----------
                      perms:
                          full_control
              perms:
                  ----------
                  Users:
                      ----------
                      grant:
                          full_control
----------
          ID: exporter_textfile_dir
    Function: file.directory
        Name: C:<span style="color:#ae81ff">\e</span>xporter<span style="color:#ae81ff">\t</span>extfile
      Result: True
     Comment: Directory C:<span style="color:#ae81ff">\e</span>xporter<span style="color:#ae81ff">\t</span>extfile updated
     Started: 20:05:33.969033
    Duration: 15.618 ms
     Changes:
              ----------
              grant_perms:
                  ----------
                  Users:
                      ----------
                      perms:
                          full_control
              perms:
                  ----------
                  Users:
                      ----------
                      grant:
                          full_control
----------
          ID: windows_exporter_dir
    Function: file.directory
        Name: C:<span style="color:#ae81ff">\e</span>xporter<span style="color:#ae81ff">\w</span>indows_exporter
      Result: True
     Comment: Directory C:<span style="color:#ae81ff">\e</span>xporter<span style="color:#ae81ff">\w</span>indows_exporter updated
     Started: 20:05:33.984651
    Duration: 0.0 ms
     Changes:
              ----------
              grant_perms:
                  ----------
                  Users:
                      ----------
                      perms:
                          full_control
              perms:
                  ----------
                  Users:
                      ----------
                      grant:
                          full_control

Summary <span style="color:#66d9ef">for</span> win2k19-vm-01.yetiops.lab
-------------
Succeeded: <span style="color:#ae81ff">13</span> <span style="color:#f92672">(</span>changed<span style="color:#f92672">=</span>8<span style="color:#f92672">)</span>
Failed:     <span style="color:#ae81ff">0</span>
-------------
Total states run:     <span style="color:#ae81ff">13</span>
Total run time:   37.743 s
</code></pre></div><p>Everything looks good!</p>
<h2 id="prometheus">Prometheus</h2>
<p>As noted in the previous post, we are using the same Prometheus setup as we do <a href="/posts/prometheus-consul-saltstack-part-1-linux/#configuring-prometheus">here</a>.</p>
<p>Also, as we are using the same Salt states, we should expect to see the instance like we do in <a href="/posts/prometheus-consul-saltstack-part-2-windows/#prometheus-targets">this post</a>: -</p>
<p><img src="/img/proxmox/proxmox-win-prom-targets.png" alt="Proxmox Windows Prometheus Targets"></p>
<p>There it is!</p>
<h2 id="grafana">Grafana</h2>
<p>Again, everything in <a href="/posts/prometheus-consul-saltstack-part-2-windows/#grafana-dashboards">this post</a> regarding Grafana is applicable too. We can use the <a href="https://grafana.com/grafana/dashboards/12422">Windows Node Dashboard</a> to verify the metrics: -</p>
<p><img src="/img/proxmox/proxmox-win-grafana.png" alt="Proxmox Windows Grafana Dashboard"></p>
<p>Alternatively you can use <a href="https://grafana.com/grafana/dashboards/6593">this</a> dashboard, which has now been updated to use the Windows Exporter metric naming (i.e <code>windows_</code> instead of <code>wmi_</code>, as the exporter changed from <code>wmi_exporter</code> to <code>windows_exporter</code>): -</p>
<p><img src="/img/proxmox/proxmox-win-grafana-2.png" alt="Proxmox Windows Grafana Dashboard - WMI Exporter"></p>
<h2 id="summary">Summary</h2>
<p>Using Cloudbase-Init, it is possible to customize a Windows image when it starts for the first time, allowing you to bootstrap a machine ready to be used in your environment. This takes away the work of installing machines manually, while also being able to manage Windows in a similar way to Linux and Unix variants (e.g. FreeBSD, OpenBSD) that support Cloud-Init.</p>
<p>As we have also seen, it is not without caveats currently. Some of this is down to Proxmox in how it presents Cloud-Init data, some of it is down to the Cloudbase-Init utility that is expecting configuration in a certain format but could potentially be in multiple formats (i.e. both <code>dns-nameservers</code> and <code>dns_nameservers</code> are possible within Cloud-Init, but Cloudbase-Init currently understands the former), and some are down to the Terraform provider that does not seem to understand some of values presented by the Proxmox API (specifically the IPv6 addressing).</p>
<p>Over time these will improve, as issues have been raised for all of these cases. This should not discourage you from using any of these tools, as all of them together bring Windows deployment on Proxmox on par with Linux (in terms of bootstrapping and Terraform usage).</p>
<p>Cloudbase-Init was initially created for use with OpenStack, and the fact it works with other providers and infrastructure is a nice bonus.</p>
]]></content>
        </item>
        
        <item>
            <title>Using Terraform and Cloud-Init to deploy and automatically monitor Proxmox instances</title>
            <link>https://yetiops.net/posts/proxmox-terraform-cloudinit-saltstack-prometheus/</link>
            <pubDate>Mon, 10 Aug 2020 20:10:10 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/proxmox-terraform-cloudinit-saltstack-prometheus/</guid>
            <description>In the last two series of posts on this site, I covered using SaltStack to deploy Consul and Prometheus Exporters, and also using different Prometheus service discovery mechanisms to gather instances to monitor in each provider.
There is no Prometheus discovery mechanism for Proxmox, nor a consistent way to discover tagged instances in Proxmox (regardless of which tool queries the Proxmox API). Instead, we can implement something like Consul to discover the instances.</description>
            <content type="html"><![CDATA[<p>In the last <a href="https://yetiops.net/posts/prometheus-consul-saltstack-part-1-linux/">two</a> <a href="https://yetiops.net/posts/prometheus-service-discovery-openstack/">series</a> of posts on this site, I covered using SaltStack to deploy <a href="https://consul.io">Consul</a> and <a href="https://prometheus.io">Prometheus</a> Exporters, and also using different Prometheus service discovery mechanisms to gather instances to monitor in each provider.</p>
<p>There is no Prometheus discovery mechanism for Proxmox, nor a consistent way to discover tagged instances in Proxmox (regardless of which tool queries the Proxmox API). Instead, we can implement something like Consul to discover the instances.</p>
<p>Also, Proxmox supports Cloud-Init, so we can bootstrap virtual machines and them register with Salt, install base packages and more without the cyclical dependency of needing to be registered to Salt to install Salt.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>As mentioned, we have covered some of what we are using here in different posts already: -</p>
<ul>
<li><a href="/posts/prometheus-consul-node_exporter">Prometheus: Discovering Services with Consul</a> - Using Consul with Prometheus to discover instances to monitor</li>
<li><a href="/posts/saltstack-introduction">Configuration Seasoning: Getting started with Saltstack</a> - A primer on SaltStack, including how to configure a basic Salt environment</li>
<li><a href="/posts/prometheus-consul-saltstack-part-1-linux/">Prometheus - Auto-deploying Consul and Exporters using Saltstack Part 1: Linux</a> - This post (and series) contains all of the Salt states we use in this post, as well as the Prometheus configuration</li>
<li>Prometheus Service Discovery posts on <a href="/posts/prometheus-service-discovery-aws-gcp-azure/">AWS, Azure and Google Cloud Platform</a>, <a href="/posts/prometheus-service-discovery-digitalocean/">Digital Ocean</a> and <a href="/posts/prometheus-service-discovery-openstack/">OpenStack</a> - This post cover using Terraform with various cloud providers, as well as making use of Cloud-Init</li>
</ul>
<p>By using elements of each of the above, we can treat Proxmox almost like another Cloud environment/provider.</p>
<h2 id="setting-up-proxmox">Setting up Proxmox</h2>
<p>Proxmox can be installed using the <a href="https://www.proxmox.com/en/downloads">ISOs</a> provided on the Proxmox website. Full instructions for how to install the Virtualisation Environment are available <a href="https://pve.proxmox.com/wiki/Installation">here</a>.</p>
<p>The following videos also do a good job of explaining it too: -</p>

<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe src="https://www.youtube.com/embed/I-e1_CTa4s0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allowfullscreen title="YouTube Video"></iframe>
</div>


<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe src="https://www.youtube.com/embed/MO4CaHn1EjM" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allowfullscreen title="YouTube Video"></iframe>
</div>


<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe src="https://www.youtube.com/embed/0cN-bFZMysE" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allowfullscreen title="YouTube Video"></iframe>
</div>

<p>Once installed, you should be able to view the Proxmox GUI on <code>https://$IP_OF_HOST:8006</code>: -</p>
<p><img src="/img/proxmox/proxmox-gui.png" alt="Proxmox GUI"></p>
<p>You can create Proxmox hosts in either standalone mode or multiple hosts in a cluster. The latter allows you to manage all of your instances from the UI of any host in the cluster. It also allows live migration of instances between Proxmox hosts (seamlessly, if using shared storage).</p>
<h2 id="terraform">Terraform</h2>
<p>Once Proxmox is installed, we can configure the Terraform Proxmox <a href="https://github.com/Telmate/terraform-provider-proxmox">provider</a>.</p>
<p>For information on how to install Terraform and an example project structure, see <a href="/posts/prometheus-service-discovery-aws-gcp-azure/#configure-terraform">here</a>.</p>
<h3 id="install-the-provider">Install the provider</h3>
<p>As the provider is from a third party, and not an official Terraform provider, you need to install the provider manually on your machine. Full instructions for this are available <a href="https://github.com/Telmate/terraform-provider-proxmox/blob/master/docs/installation.md">here</a>, but to summarize the steps: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># Clone the repository</span>
$ git clone https://github.com/Telmate/terraform-provider-proxmox

<span style="color:#75715e"># Go Install the provider and provisioner (requires a working Goland installation)</span>
$ cd terraform-provider-proxmox
$ go install github.com/Telmate/terraform-provider-proxmox/cmd/terraform-provider-proxmox
$ go install github.com/Telmate/terraform-provider-proxmox/cmd/terraform-provisioner-proxmox

<span style="color:#75715e"># Run the Makefile</span>
$ make

<span style="color:#75715e"># Create the Terraform plugins directory and move the generated plugins to it</span>
$ mkdir ~/.terraform.d/plugins
$ cp bin/terraform-provider-proxmox ~/.terraform.d/plugins
$ cp bin/terraform-provisioner-proxmox ~/.terraform.d/plugins
</code></pre></div><p>The provider will now be available to use within your Terraform environment.</p>
<h3 id="create-a-terraform-user-in-proxmox">Create a Terraform user in Proxmox</h3>
<p>Terraform requires a user to authenticate against the Proxmox API. You can create the user via the GUI, but you will need to use the CLI to assign it to the correct roles. Alternatively, you can create the user and assign roles with the CLI: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># Create the user</span>
pveum user add terraform-prov@pve --password $PASSWORD

<span style="color:#75715e"># Assign the user the correct role</span>
pveum aclmod / -user terraform-prov@pve -role Administrator
</code></pre></div><h3 id="configure-the-proxmox-provider">Configure the Proxmox provider</h3>
<p>Within your Terraform directory, create a directory called <code>proxmox</code> and then add a <code>provider.tf</code> file: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;proxmox&#34;</span> {
  pm_api_url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://$IP_OF_HOST:8006/api2/json&#34;</span>
  pm_user <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;terraform-prov@pve&#34;</span>
  pm_tls_insecure <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
}
</code></pre></div><p>You also need to supply the password, however it is not advisable to supply this as part of the configuration. Sensitive credentials should not be committed to a version control system (like Git). Instead, you can use an environment variable: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ export PM_PASS<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;PR0M0X_P8SS&#34;</span>
</code></pre></div><p>You should now be able to configure instances in Proxmox using Terraform.</p>
<h3 id="creating-a-template">Creating a template</h3>
<p>The Terraform provider can create instances using either the <code>iso</code> option (i.e. a CD-based installer) or via a <code>clone</code> (i.e. a copy of another instance). The former provides little benefit over installing via the GUI. Instead we can create a template instance ready to be cloned, with all the necessary dependencies for <a href="https://cloud-init.io/">Cloud-Init</a>.</p>
<p>There are two ways to achieve this: -</p>
<ul>
<li>Create your own instance, install the necessary dependencies and convert to a template</li>
<li>Use an image with the dependencies installed already.</li>
</ul>
<p>Proxmox recommend using the same <a href="https://docs.openstack.org/image-guide/obtain-images.html">images</a> used for <a href="https://openstack.org">OpenStack</a>, as OpenStack supports Cloud-Init. Also, Proxmox and OpenStack both use <a href="https://www.linux-kvm.org/page/Main_Page">KVM</a> for virtualisation, meaning the images use common dependencies and kernel modules.</p>
<p>To create the template, obtain one of the OpenStack images (I am using Debian Buster): -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># Navigate to the ISO directory for Proxmox</span>
$ cd /var/lib/vz/templates/isos

<span style="color:#75715e"># Source the image</span>
$ wget http://cdimage.debian.org/cdimage/openstack/current-10/debian-10-openstack-amd64.qcow2
</code></pre></div><p>Use the following commands to create a template within Proxmox, ready to be cloned: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># Create the instance</span>
qm create <span style="color:#ae81ff">9000</span> -name debian-cloudinit -memory <span style="color:#ae81ff">1024</span> -net0 virtio,bridge<span style="color:#f92672">=</span>vmbr0 -cores <span style="color:#ae81ff">1</span> -sockets <span style="color:#ae81ff">1</span>

<span style="color:#75715e"># Import the OpenStack disk image to Proxmox storage</span>
qm importdisk <span style="color:#ae81ff">9000</span> debian-10-openstack-amd64.qcow2 local-lvm

<span style="color:#75715e"># Attach the disk to the virtual machine</span>
qm set <span style="color:#ae81ff">9000</span> -scsihw virtio-scsi-pci -virtio0 local-lvm:vm-9000-disk-0

<span style="color:#75715e"># Add a serial output</span>
qm set <span style="color:#ae81ff">9000</span> -serial0 socket

<span style="color:#75715e"># Set the bootdisk to the imported Openstack disk</span>
qm set <span style="color:#ae81ff">9000</span> -boot c -bootdisk virtio0

<span style="color:#75715e"># Enable the Qemu agent</span>
qm set <span style="color:#ae81ff">9000</span> -agent <span style="color:#ae81ff">1</span>

<span style="color:#75715e"># Allow hotplugging of network, USB and disks</span>
qm set <span style="color:#ae81ff">9000</span> -hotplug disk,network,usb

<span style="color:#75715e"># Add a single vCPU (for now)</span>
qm set <span style="color:#ae81ff">9000</span> -vcpus <span style="color:#ae81ff">1</span>

<span style="color:#75715e"># Add a video output</span>
qm set <span style="color:#ae81ff">9000</span> -vga qxl

<span style="color:#75715e"># Set a second hard drive, using the inbuilt cloudinit drive</span>
qm set <span style="color:#ae81ff">9000</span> -ide2 local-lvm:cloudinit

<span style="color:#75715e"># Resize the primary boot disk (otherwise it will be around 2G by default)</span>
<span style="color:#75715e"># This step adds another 8G of disk space, but change this as you need to</span>
qm resize <span style="color:#ae81ff">9000</span> virtio0 +8G

<span style="color:#75715e"># Convert the VM to the template</span>
qm template <span style="color:#ae81ff">9000</span>
</code></pre></div><p>This template will now be usable with Terraform with the name <code>debian-cloudinit</code> (i.e. the name specified when creating the instance).</p>
<h3 id="define-an-instance">Define an instance</h3>
<p>You can now create the configuration files for your first Proxmox instance. The below is from the file <code>vm.tf</code> in the <code>terraform/proxmox</code> directory: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#75715e"># Source the Cloud Init Config file
</span><span style="color:#75715e"></span><span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;template_file&#34; &#34;cloud_init_deb10_vm-01&#34;</span> {
  template  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${file(&#34;${path.module}/files/cloud_init_deb10.cloud_config&#34;)}&#34;</span>

  vars <span style="color:#f92672">=</span> {
    ssh_key <span style="color:#f92672">=</span> <span style="color:#66d9ef">file</span>(<span style="color:#e6db74">&#34;~/.ssh/id_rsa.pub&#34;</span>)
    hostname <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vm-01&#34;</span>
    domain <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops.lab&#34;</span>
  }
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e"># Create a local copy of the file, to transfer to Proxmox
</span><span style="color:#75715e"></span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;local_file&#34; &#34;cloud_init_deb10_vm-01&#34;</span> {
  content   <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">template_file</span>.<span style="color:#66d9ef">cloud_init_deb10_vm</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">01</span>.<span style="color:#66d9ef">rendered</span>
  filename  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${path.module}/files/user_data_cloud_init_deb10_vm-01.cfg&#34;</span>
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e"># Transfer the file to the Proxmox Host
</span><span style="color:#75715e"></span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;null_resource&#34; &#34;cloud_init_deb10_vm-01&#34;</span> {
  <span style="color:#66d9ef">connection</span> {
    type    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ssh&#34;</span>
    user    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;root&#34;</span>
    private_key <span style="color:#f92672">=</span> <span style="color:#66d9ef">file</span>(<span style="color:#e6db74">&#34;~/.ssh/id_rsa&#34;</span>)
    host    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;10.15.31.7&#34;</span>
  }

  <span style="color:#66d9ef">provisioner</span> <span style="color:#e6db74">&#34;file&#34;</span> {
    source       <span style="color:#f92672">=</span> <span style="color:#66d9ef">local_file</span>.<span style="color:#66d9ef">cloud_init_deb10_vm</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">01</span>.<span style="color:#66d9ef">filename</span>
    destination  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/var/lib/vz/snippets/cloud_init_deb10_vm-01.yml&#34;</span>
  }
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e"># Create the VM
</span><span style="color:#75715e"></span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;proxmox_vm_qemu&#34; &#34;vm-01&#34;</span> {<span style="color:#75715e">
</span><span style="color:#75715e">  ## Wait for the cloud-config file to exist
</span><span style="color:#75715e"></span>
  depends_on <span style="color:#f92672">=</span> [
    <span style="color:#66d9ef">null_resource</span>.<span style="color:#66d9ef">cloud_init_deb10_vm</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">01</span>
  ]

  name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vm-01&#34;</span>
  target_node <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;pve-01&#34;</span><span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">  # Clone from debian-cloudinit template
</span><span style="color:#75715e"></span>  clone <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;debian-cloudinit&#34;</span>
  os_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cloud-init&#34;</span><span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">  # Cloud init options
</span><span style="color:#75715e"></span>  cicustom <span style="color:#f92672">=</span> &#34;user<span style="color:#f92672">=</span><span style="color:#66d9ef">local</span><span style="color:#960050;background-color:#1e0010">:</span><span style="color:#66d9ef">snippets</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">cloud_init_deb10_vm</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">01</span>.<span style="color:#66d9ef">yml</span><span style="color:#960050;background-color:#1e0010">&#34;</span>
  ipconfig0 <span style="color:#f92672">=</span> &#34;ip<span style="color:#f92672">=</span>10.15.31.99/24,gw<span style="color:#f92672">=</span><span style="color:#ae81ff">10</span>.<span style="color:#ae81ff">15</span>.<span style="color:#ae81ff">31</span>.<span style="color:#ae81ff">253</span><span style="color:#960050;background-color:#1e0010">&#34;</span>

  memory       <span style="color:#f92672">=</span> <span style="color:#ae81ff">512</span>
  agent        <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span><span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">  # Set the boot disk paramters
</span><span style="color:#75715e"></span>  bootdisk <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;scsi0&#34;</span>
  scsihw       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;virtio-scsi-pci&#34;</span>

  <span style="color:#66d9ef">disk</span> {
    id              <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
    size            <span style="color:#f92672">=</span> <span style="color:#ae81ff">10</span>
    type            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;scsi&#34;</span>
    storage         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;local-lvm&#34;</span>
    storage_type    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;lvm&#34;</span>
    iothread        <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
  }<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">  # Set the network
</span><span style="color:#75715e"></span>  <span style="color:#66d9ef">network</span> {
    id <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
    model <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;virtio&#34;</span>
    bridge <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vmbr0&#34;</span>
  }<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">  # Ignore changes to the network
</span><span style="color:#75715e">  ## MAC address is generated on every apply, causing
</span><span style="color:#75715e">  ## TF to think this needs to be rebuilt on every apply
</span><span style="color:#75715e"></span>  <span style="color:#66d9ef">lifecycle</span> {
     ignore_changes <span style="color:#f92672">=</span> [
       <span style="color:#66d9ef">network</span>
     ]
  }
}
</code></pre></div><p>To summarize the steps being taken here, we are: -</p>
<ul>
<li>Source the <code>cloud-config</code> file, with steps that the instance should take at first boot
<ul>
<li>We also supply variables to set the hostname, domain and our SSH key to the instance</li>
</ul>
</li>
<li>We generate a local file of the template with the variables applied</li>
<li>We use a <code>null_resource</code> in Terraform to copy the file to the Proxmox host
<ul>
<li>Null Resources are used instead of an existing provider</li>
<li>The provisioner is used to place the file in the remote location (i.e. the Proxmox host)</li>
</ul>
</li>
<li>We create a QEMU-based virtual machine
<ul>
<li>This depends on the <code>cloud-config</code> file being generated and transferred to the Proxmox host</li>
<li>We specify the Proxmox node to build it on</li>
<li>We tell it to clone the <code>debian-cloudinit</code> image (from the <a href="/posts/proxmox-terraform-cloudinit-saltstack-prometheus/#creating-a-template">Creating a template</a> section)</li>
<li>We use the <code>cicustom</code> option to specify our <code>cloud-config</code> template</li>
<li>We use <code>ipconfig0</code> to specify the IP of the first network interface</li>
<li>We tell it to have a disk size of 10G</li>
<li>We create the network, with an ID of 0 (which matches the <code>ipconfig0</code> ID)</li>
<li>We ignore changes to the network</li>
</ul>
</li>
</ul>
<p>The last step is because the Proxmox provider wants to recreate the interface on every apply, as the MAC address is randomly generated each time. You either need to ignore changes so that it doesn&rsquo;t want to recreate the interface, or specify the MAC address in the <code>network</code> block using the <code>macaddr</code> field, e.g.: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#75715e">  # Set the network
</span><span style="color:#75715e"></span>  <span style="color:#66d9ef">network</span> {
    id <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
    model <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;virtio&#34;</span>
    macaddr <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;5a:fe:6c:96:40:61&#34;</span> 
    bridge <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vmbr0&#34;</span>
  }
</code></pre></div><p>For standalone Proxmox hosts, the <code>target_node</code> will never change. To build on another host, you would need to use a different <code>provider.tf</code> file. If however you are administering a Proxmox cluster (where multiple Proxmox hosts are managed through the same management plane, you can specify a different <code>target_node</code> to the node that Terraform is communicating with.</p>
<p>The <code>target_node</code> name itself can be seen either in Proxmox UI, or by checking the <code>hostname -a</code> command: -</p>
<p><img src="/img/proxmox/proxmox-node-name.png" alt="Proxmox Node Name"></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ hostname -a
pve-01
</code></pre></div><p>If you are running a cluster, you can see the available nodes using <code>pvecm nodes</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ pvecm nodes

Membership information
~~~~~~~~~~~~~~~~~~~~~~
    Nodeid      Votes Name
         <span style="color:#ae81ff">1</span>          <span style="color:#ae81ff">1</span> pve-01
         <span style="color:#ae81ff">2</span>          <span style="color:#ae81ff">1</span> pve-02 <span style="color:#f92672">(</span>local<span style="color:#f92672">)</span>
</code></pre></div><h4 id="cloud-config">Cloud Config</h4>
<p>The contents of the <code>cloud-config</code> file are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e">#cloud-config</span>

<span style="color:#66d9ef">package_update</span>: <span style="color:#66d9ef">true</span>
<span style="color:#66d9ef">package_upgrade</span>: <span style="color:#66d9ef">true</span>

<span style="color:#75715e"># APT fails to acquire GPG keys if package dirmngr is missing</span>
<span style="color:#66d9ef">bootcmd</span>:
  - [ cloud-init-per, once, dirmngr-aptupdate, apt-get, update ]
  - [ cloud-init-per, once, dirmngr-aptinstall, apt-get, install, dirmngr, -y ]
  - [ cloud-init-per, once, dirmngr-aptinstall, apt-get, install, gnupg2, -y ]

<span style="color:#66d9ef">apt</span>:
  <span style="color:#66d9ef">sources</span>:
    <span style="color:#66d9ef">saltstack.list</span>:
      <span style="color:#66d9ef">source</span>: <span style="color:#e6db74">&#34;deb http://repo.saltstack.com/py3/debian/10/amd64/latest buster main&#34;</span>
      <span style="color:#66d9ef">filename</span>: saltstack.list
      <span style="color:#66d9ef">key</span>: <span style="color:#e6db74">|
</span><span style="color:#e6db74">        -----BEGIN PGP PUBLIC KEY BLOCK-----</span>
        <span style="color:#66d9ef">Version</span>: GnuPG v2

        mQENBFOpvpgBCADkP656H41i8fpplEEB8IeLhugyC2rTEwwSclb8tQNYtUiGdna9
        m38kb0OS2DDrEdtdQb2hWCnswxaAkUunb2qq18vd3dBvlnI+C4/xu5ksZZkRj+fW
        tArNR18V+2jkwcG26m8AxIrT+m4M6/bgnSfHTBtT5adNfVcTHqiT1JtCbQcXmwVw
        WbqS6v/LhcsBE//SHne4uBCK/GHxZHhQ5jz5h+3vWeV4gvxS3Xu6v1IlIpLDwUts
        kT1DumfynYnnZmWTGc6SYyIFXTPJLtnoWDb9OBdWgZxXfHEcBsKGha+bXO+m2tHA
        gNneN9i5f8oNxo5njrL8jkCckOpNpng18BKXABEBAAG0MlNhbHRTdGFjayBQYWNr
        YWdpbmcgVGVhbSA8cGFja2FnaW5nQHNhbHRzdGFjay5jb20+iQE4BBMBAgAiBQJT
        qb6YAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRAOCKFJ3le/vhkqB/0Q
        WzELZf4d87WApzolLG+zpsJKtt/ueXL1W1KA7JILhXB1uyvVORt8uA9FjmE083o1
        yE66wCya7V8hjNn2lkLXboOUd1UTErlRg1GYbIt++VPscTxHxwpjDGxDB1/fiX2o
        nK5SEpuj4IeIPJVE/uLNAwZyfX8DArLVJ5h8lknwiHlQLGlnOu9ulEAejwAKt9CU
        4oYTszYM4xrbtjB/fR+mPnYh2fBoQO4d/NQiejIEyd9IEEMd/03AJQBuMux62tjA
        /NwvQ9eqNgLw9NisFNHRWtP4jhAOsshv1WW+zPzu3ozoO+lLHixUIz7fqRk38q8Q
        9oNR31KvrkSNrFbA3D89uQENBFOpvpgBCADJ79iH10AfAfpTBEQwa6vzUI3Eltqb
        9aZ0xbZV8V/8pnuU7rqM7Z+nJgldibFk4gFG2bHCG1C5aEH/FmcOMvTKDhJSFQUx
        uhgxttMArXm2c22OSy1hpsnVG68G32Nag/QFEJ++3hNnbyGZpHnPiYgej3FrerQJ
        zv456wIsxRDMvJ1NZQB3twoCqwapC6FJE2hukSdWB5yCYpWlZJXBKzlYz/gwD/Fr
        GL578WrLhKw3UvnJmlpqQaDKwmV2s7MsoZogC6wkHE92kGPG2GmoRD3ALjmCvN1E
        PsIsQGnwpcXsRpYVCoW7e2nW4wUf7IkFZ94yOCmUq6WreWI4NggRcFC5ABEBAAGJ
        AR8EGAECAAkFAlOpvpgCGwwACgkQDgihSd5Xv74/NggA08kEdBkiWWwJZUZEy7cK
        WWcgjnRuOHd4rPeT+vQbOWGu6x4bxuVf9aTiYkf7ZjVF2lPn97EXOEGFWPZeZbH4
        vdRFH9jMtP+rrLt6+3c9j0M8SIJYwBL1+CNpEC/BuHj/Ra/cmnG5ZNhYebm76h5f
        T9iPW9fFww36FzFka4VPlvA4oB7ebBtquFg3sdQNU/MmTVV4jPFWXxh4oRDDR+8N
        1bcPnbB11b5ary99F/mqr7RgQ+YFF0uKRE3SKa7a+6cIuHEZ7Za+zhPaQlzAOZlx
        fuBmScum8uQTrEF5+Um5zkwC7EXTdH1co/+/V/fpOtxIg4XO4kcugZefVm5ERfVS
        MA==
        =dtMN
        -----END PGP PUBLIC KEY BLOCK----<span style="color:#e6db74">-
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">packages:</span>
  - jq
  - curl
  - qemu-guest-agent
  - salt-minion

<span style="color:#66d9ef">users</span>:
- <span style="color:#66d9ef">name</span>: stuart
  <span style="color:#66d9ef">groups</span>: sudo
  <span style="color:#66d9ef">shell</span>: /bin/bash
  <span style="color:#66d9ef">sudo</span>: [<span style="color:#e6db74">&#39;ALL=(ALL) NOPASSWD:ALL&#39;</span>]
  <span style="color:#66d9ef">ssh_authorized_keys</span>:
    - ${ssh_key}

<span style="color:#66d9ef">preserve_hostname</span>: <span style="color:#66d9ef">false</span>
<span style="color:#66d9ef">manage_etc_hosts</span>: <span style="color:#66d9ef">false</span>
<span style="color:#66d9ef">fqdn</span>: ${hostname}.${domain}

<span style="color:#66d9ef">configsalt</span>:
  - <span style="color:#75715e">&amp;config_salt</span> <span style="color:#e6db74">|
</span><span style="color:#e6db74">    echo &#34;master: salt-master.${domain}&#34; &gt;&gt; /etc/salt/minion</span>
    echo <span style="color:#e6db74">&#34;domain: ${domain}&#34;</span> &gt;&gt; /etc/salt/grains
    echo <span style="color:#e6db74">&#34;startup_states: &#39;highstate&#39;&#34;</span> &gt;&gt; /etc/salt/minion
    echo <span style="color:#e6db74">&#34;id: ${hostname}.${domain}&#34;</span> &gt;&gt; /etc/salt/minion
    hostnamectl set-hostname <span style="color:#e6db74">&#34;${hostname}.${domain}&#34;</span> &gt; /etc/hostname

<span style="color:#66d9ef">restartsalt</span>:
  - <span style="color:#75715e">&amp;restart_salt</span> <span style="color:#e6db74">|
</span><span style="color:#e6db74">    systemctl restart salt-minion.service</span>

<span style="color:#66d9ef">runcmd</span>:
 - [ sh, -c, <span style="color:#75715e">*config_salt</span> ]
 - [ sh, -c, <span style="color:#75715e">*restart_salt</span> ]
</code></pre></div><p>There are a few points to note in this: -</p>
<ul>
<li>The <code>bootcmd</code> section specifies commands that run before any other section
<ul>
<li>This is required here because Debian Cloud images do not include <code>gnupg2</code> or <code>dirmngr</code> by default, meaning adding GPG keys for additional repositories (like the Salt repository) will fail</li>
</ul>
</li>
<li>We add the official Salt Debian repository to get the latest version of the Salt Minion</li>
<li>The <code>runcmd</code> section will run user-created sections, which will always run last
<ul>
<li>This means that we can install packages and then configure them afterwards, without worrying about whether the package installation will override the configuration files</li>
</ul>
</li>
</ul>
<p>I also add myself as a user, so that if the <code>cloud-config</code> file fails, it is possible to troubleshoot. By default, the Debian image has no default password you can use, requiring SSH keys for the primary user.</p>
<p>We also configure Salt, ready for joining to our existing Salt server.</p>
<h3 id="build-the-instance">Build the instance</h3>
<p>Now that we have defined the instance within Terraform, we can go ahead and build it: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ terraform apply
data.template_file.cloud_init_deb10_vm-01: Refreshing state...

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  <span style="color:#75715e"># local_file.cloud_init_deb10_vm-01 will be created</span>
  + resource <span style="color:#e6db74">&#34;local_file&#34;</span> <span style="color:#e6db74">&#34;cloud_init_deb10_vm-01&#34;</span> <span style="color:#f92672">{</span>
      + content              <span style="color:#f92672">=</span> &lt;&lt;~EOT
            <span style="color:#75715e">#cloud-config</span>

            package_update: true
            package_upgrade: true

            <span style="color:#75715e"># APT fails to acquire GPG keys if package dirmngr is missing</span>
            bootcmd:
              - <span style="color:#f92672">[</span> cloud-init-per, once, dirmngr-aptupdate, apt-get, update <span style="color:#f92672">]</span>
              - <span style="color:#f92672">[</span> cloud-init-per, once, dirmngr-aptinstall, apt-get, install, dirmngr, -y <span style="color:#f92672">]</span>
              - <span style="color:#f92672">[</span> cloud-init-per, once, dirmngr-aptinstall, apt-get, install, gnupg2, -y <span style="color:#f92672">]</span>

            apt:
              sources:
                saltstack.list:
                  source: <span style="color:#e6db74">&#34;deb http://repo.saltstack.com/py3/debian/10/amd64/latest buster main&#34;</span>
                  filename: saltstack.list
                  key: |
                    -----BEGIN PGP PUBLIC KEY BLOCK-----
                    Version: GnuPG v2

                    mQENBFOpvpgBCADkP656H41i8fpplEEB8IeLhugyC2rTEwwSclb8tQNYtUiGdna9
                    m38kb0OS2DDrEdtdQb2hWCnswxaAkUunb2qq18vd3dBvlnI+C4/xu5ksZZkRj+fW
                    tArNR18V+2jkwcG26m8AxIrT+m4M6/bgnSfHTBtT5adNfVcTHqiT1JtCbQcXmwVw
                    WbqS6v/LhcsBE//SHne4uBCK/GHxZHhQ5jz5h+3vWeV4gvxS3Xu6v1IlIpLDwUts
                    kT1DumfynYnnZmWTGc6SYyIFXTPJLtnoWDb9OBdWgZxXfHEcBsKGha+bXO+m2tHA
                    gNneN9i5f8oNxo5njrL8jkCckOpNpng18BKXABEBAAG0MlNhbHRTdGFjayBQYWNr
                    YWdpbmcgVGVhbSA8cGFja2FnaW5nQHNhbHRzdGFjay5jb20+iQE4BBMBAgAiBQJT
                    qb6YAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRAOCKFJ3le/vhkqB/0Q
                    WzELZf4d87WApzolLG+zpsJKtt/ueXL1W1KA7JILhXB1uyvVORt8uA9FjmE083o1
                    yE66wCya7V8hjNn2lkLXboOUd1UTErlRg1GYbIt++VPscTxHxwpjDGxDB1/fiX2o
                    nK5SEpuj4IeIPJVE/uLNAwZyfX8DArLVJ5h8lknwiHlQLGlnOu9ulEAejwAKt9CU
                    4oYTszYM4xrbtjB/fR+mPnYh2fBoQO4d/NQiejIEyd9IEEMd/03AJQBuMux62tjA
                    /NwvQ9eqNgLw9NisFNHRWtP4jhAOsshv1WW+zPzu3ozoO+lLHixUIz7fqRk38q8Q
                    9oNR31KvrkSNrFbA3D89uQENBFOpvpgBCADJ79iH10AfAfpTBEQwa6vzUI3Eltqb
                    9aZ0xbZV8V/8pnuU7rqM7Z+nJgldibFk4gFG2bHCG1C5aEH/FmcOMvTKDhJSFQUx
                    uhgxttMArXm2c22OSy1hpsnVG68G32Nag/QFEJ++3hNnbyGZpHnPiYgej3FrerQJ
                    zv456wIsxRDMvJ1NZQB3twoCqwapC6FJE2hukSdWB5yCYpWlZJXBKzlYz/gwD/Fr
                    GL578WrLhKw3UvnJmlpqQaDKwmV2s7MsoZogC6wkHE92kGPG2GmoRD3ALjmCvN1E
                    PsIsQGnwpcXsRpYVCoW7e2nW4wUf7IkFZ94yOCmUq6WreWI4NggRcFC5ABEBAAGJ
                    AR8EGAECAAkFAlOpvpgCGwwACgkQDgihSd5Xv74/NggA08kEdBkiWWwJZUZEy7cK
                    WWcgjnRuOHd4rPeT+vQbOWGu6x4bxuVf9aTiYkf7ZjVF2lPn97EXOEGFWPZeZbH4
                    vdRFH9jMtP+rrLt6+3c9j0M8SIJYwBL1+CNpEC/BuHj/Ra/cmnG5ZNhYebm76h5f
                    T9iPW9fFww36FzFka4VPlvA4oB7ebBtquFg3sdQNU/MmTVV4jPFWXxh4oRDDR+8N
                    1bcPnbB11b5ary99F/mqr7RgQ+YFF0uKRE3SKa7a+6cIuHEZ7Za+zhPaQlzAOZlx
                    fuBmScum8uQTrEF5+Um5zkwC7EXTdH1co/+/V/fpOtxIg4XO4kcugZefVm5ERfVS
                    MA<span style="color:#f92672">==</span>
                    <span style="color:#f92672">=</span>dtMN
                    -----END PGP PUBLIC KEY BLOCK-----

            packages:
              - jq
              - curl
              - qemu-guest-agent
              - salt-minion

            users:
            - name: stuart
              groups: sudo
              shell: /bin/bash
              sudo: <span style="color:#f92672">[</span><span style="color:#e6db74">&#39;ALL=(ALL) NOPASSWD:ALL&#39;</span><span style="color:#f92672">]</span>
              ssh_authorized_keys:
                - ssh-rsa <span style="color:#75715e">###SSH PUBLIC KEY### stuh84@symphonyx</span>


            preserve_hostname: false
            manage_etc_hosts: false
            fqdn: vm-01.yetiops.lab

            configsalt:
              - &amp;config_salt |
                echo <span style="color:#e6db74">&#34;master: salt-master.yetiops.lab&#34;</span> &gt;&gt; /etc/salt/minion
                echo <span style="color:#e6db74">&#34;domain: yetiops.lab&#34;</span> &gt;&gt; /etc/salt/grains
                echo <span style="color:#e6db74">&#34;startup_states: &#39;highstate&#39;&#34;</span> &gt;&gt; /etc/salt/minion
                echo <span style="color:#e6db74">&#34;id: vm-01.yetiops.lab&#34;</span> &gt;&gt; /etc/salt/minion
                hostnamectl set-hostname <span style="color:#e6db74">&#34;vm-01.yetiops.lab&#34;</span> &gt; /etc/hostname

            restartsalt:
              - &amp;restart_salt |
                systemctl restart salt-minion.service

            runcmd:
             - <span style="color:#f92672">[</span> sh, -c, *config_salt <span style="color:#f92672">]</span>
             - <span style="color:#f92672">[</span> sh, -c, *restart_salt <span style="color:#f92672">]</span>
        EOT
      + directory_permission <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0777&#34;</span>
      + file_permission      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0777&#34;</span>
      + filename             <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;./files/user_data_cloud_init_deb10_vm-01.cfg&#34;</span>
      + id                   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># null_resource.cloud_init_deb10_vm-01 will be created</span>
  + resource <span style="color:#e6db74">&#34;null_resource&#34;</span> <span style="color:#e6db74">&#34;cloud_init_deb10_vm-01&#34;</span> <span style="color:#f92672">{</span>
      + id <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># proxmox_vm_qemu.vm-01 will be created</span>
  + resource <span style="color:#e6db74">&#34;proxmox_vm_qemu&#34;</span> <span style="color:#e6db74">&#34;vm-01&#34;</span> <span style="color:#f92672">{</span>
      + agent        <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
      + balloon      <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
      + bios         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;seabios&#34;</span>
      + boot         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cdn&#34;</span>
      + bootdisk     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;scsi0&#34;</span>
      + cicustom     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;user=local:snippets/cloud_init_deb10_vm-01.yml&#34;</span>
      + clone        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;debian-cloudinit&#34;</span>
      + clone_wait   <span style="color:#f92672">=</span> <span style="color:#ae81ff">15</span>
      + cores        <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>
      + cpu          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;host&#34;</span>
      + force_create <span style="color:#f92672">=</span> false
      + full_clone   <span style="color:#f92672">=</span> true
      + hotplug      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;network,disk,usb&#34;</span>
      + id           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + ipconfig0    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ip=10.15.31.99/24,gw=10.15.31.253&#34;</span>
      + kvm          <span style="color:#f92672">=</span> true
      + memory       <span style="color:#f92672">=</span> <span style="color:#ae81ff">512</span>
      + name         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vm-01&#34;</span>
      + numa         <span style="color:#f92672">=</span> false
      + onboot       <span style="color:#f92672">=</span> true
      + os_type      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cloud-init&#34;</span>
      + preprovision <span style="color:#f92672">=</span> true
      + scsihw       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;virtio-scsi-pci&#34;</span>
      + sockets      <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>
      + ssh_host     <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + ssh_port     <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + target_node  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;pve-01&#34;</span>
      + vcpus        <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
      + vlan         <span style="color:#f92672">=</span> -1
      + vmid         <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>

      + disk <span style="color:#f92672">{</span>
          + backup       <span style="color:#f92672">=</span> false
          + cache        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;none&#34;</span>
          + format       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;raw&#34;</span>
          + id           <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
          + iothread     <span style="color:#f92672">=</span> true
          + mbps         <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
          + mbps_rd      <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
          + mbps_rd_max  <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
          + mbps_wr      <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
          + mbps_wr_max  <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
          + replicate    <span style="color:#f92672">=</span> false
          + size         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;10&#34;</span>
          + storage      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;local-lvm&#34;</span>
          + storage_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;lvm&#34;</span>
          + type         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;scsi&#34;</span>
        <span style="color:#f92672">}</span>

      + network <span style="color:#f92672">{</span>
          + bridge    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vmbr0&#34;</span>
          + firewall  <span style="color:#f92672">=</span> false
          + id        <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
          + link_down <span style="color:#f92672">=</span> false
          + macaddr   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + model     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;virtio&#34;</span>
          + queues    <span style="color:#f92672">=</span> -1
          + rate      <span style="color:#f92672">=</span> -1
          + tag       <span style="color:#f92672">=</span> -1
        <span style="color:#f92672">}</span>
    <span style="color:#f92672">}</span>

Plan: <span style="color:#ae81ff">3</span> to add, <span style="color:#ae81ff">0</span> to change, <span style="color:#ae81ff">0</span> to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only <span style="color:#e6db74">&#39;yes&#39;</span> will be accepted to approve.

  Enter a value: yes

local_file.cloud_init_deb10_vm-01: Creating...
local_file.cloud_init_deb10_vm-01: Creation complete after 0s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>ba8ba5e0ff96906173046a818ba8305964d84315<span style="color:#f92672">]</span>
null_resource.cloud_init_deb10_vm-01: Creating...
null_resource.cloud_init_deb10_vm-01: Provisioning with <span style="color:#e6db74">&#39;file&#39;</span>...
null_resource.cloud_init_deb10_vm-01: Creation complete after 0s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>6696639824705285611<span style="color:#f92672">]</span>
proxmox_vm_qemu.vm-01: Creating...
proxmox_vm_qemu.vm-01: Still creating... <span style="color:#f92672">[</span>10s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.vm-01: Still creating... <span style="color:#f92672">[</span>20s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.vm-01: Still creating... <span style="color:#f92672">[</span>30s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.vm-01: Still creating... <span style="color:#f92672">[</span>40s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.vm-01: Still creating... <span style="color:#f92672">[</span>50s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.vm-01: Creation complete after 56s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>pve-01/qemu/100<span style="color:#f92672">]</span>

Apply complete! Resources: <span style="color:#ae81ff">3</span> added, <span style="color:#ae81ff">0</span> changed, <span style="color:#ae81ff">0</span> destroyed.
</code></pre></div><p>We can double check that Terraform is managing these resources now with <code>terraform state list</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ terraform state list
data.template_file.cloud_init_deb10_vm-01
local_file.cloud_init_deb10_vm-01
null_resource.cloud_init_deb10_vm-01
proxmox_vm_qemu.vm-01
</code></pre></div><p>There are far fewer resources created than in some recent posts (like the <a href="/posts/prometheus-service-discovery-openstack/#build-the-infrastructure---instance">OpenStack</a> post). This is because we are only managing the hypervisor. Any security groups, firewall policies, public IP allocations and otherwise are managed by other infrastructure in your network.</p>
<p>We can check to see if the instance is in the Proxmox UI: -</p>
<p><img src="/img/proxmox/proxmox-ui-vm-01.png" alt="Proxmox UI - vm-01"></p>
<p>Alternatively, we can verify this with <code>qm list</code> on the Proxmox node: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ qm list
      VMID NAME                 STATUS     MEM<span style="color:#f92672">(</span>MB<span style="color:#f92672">)</span>    BOOTDISK<span style="color:#f92672">(</span>GB<span style="color:#f92672">)</span> PID
       <span style="color:#ae81ff">100</span> vm-01                running    <span style="color:#ae81ff">512</span>               12.00 <span style="color:#ae81ff">1888</span>
      <span style="color:#ae81ff">9000</span> debian-cloudinit     stopped    <span style="color:#ae81ff">1024</span>              12.00 <span style="color:#ae81ff">0</span>
      <span style="color:#ae81ff">9001</span> fedora-cloudinit     stopped    <span style="color:#ae81ff">1024</span>              12.00 <span style="color:#ae81ff">0</span>
</code></pre></div><h2 id="salt">Salt</h2>
<p>The Salt setup and states are the same as we used in <a href="/posts/prometheus-consul-saltstack-part-1-linux/#setting-up-your-saltstack-environment">this post</a>. We use Salt to: -</p>
<ul>
<li>Turn the Salt server itself into a Consul server as well</li>
<li>Deploy the Consul and Node Exporter SystemD states to the Debian instance deployed on Proxmox in the previous steps</li>
</ul>
<p>In the Terraform section, we used <code>cloud-config</code> to deploy the latest version of Salt and to customise the Minion configuration. The generated configuration would be: -</p>
<p><strong>/etc/salt/minion</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">master: salt-master.yetiops.lab
startup_states: <span style="color:#e6db74">&#39;highstate&#39;</span>
id: vm-01.yetiops.lab
</code></pre></div><p><strong>/etc/salt/grains</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">domain: yetiops.lab
</code></pre></div><p>This will: -</p>
<ul>
<li>Register the instance with the Salt server (<code>salt-master.yetiops.lab</code>)</li>
<li>Use the <code>id</code> of <code>vm-01.yetiops.lab</code></li>
<li>Assign a grain (similar to a <strong>fact</strong> in Ansible) of the domain (<code>yetiops.lab</code>)</li>
<li>Run a <strong>highstate</strong> as a startup state</li>
</ul>
<p>The latter means that when the minion on the instance is registered and accepted by the Salt server, it will automatically run a highstate (i.e. run all matching Salt states)</p>
<h3 id="accept-the-key">Accept the key</h3>
<p>To accept the minion on the master, we need to check that it has registered first: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo salt-key -L
Accepted Keys:
pve-01.yetiops.lab
salt-master.yetiops.lab
Denied Keys:
Unaccepted Keys:
vm-01.yetiops.lab &lt;----
Rejected Keys:
</code></pre></div><p>Now we can accept the key: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo salt-key -a <span style="color:#e6db74">&#39;vm-01*&#39;</span>
The following keys are going to be accepted:
Unaccepted Keys:
vm-01.yetiops.lab
Proceed? <span style="color:#f92672">[</span>n/Y<span style="color:#f92672">]</span> y
Key <span style="color:#66d9ef">for</span> minion vm-01.yetiops.lab accepted.
</code></pre></div><p>We can check what states will be applied to this minion as well: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo salt <span style="color:#e6db74">&#39;vm-01*&#39;</span> state.show_states
vm-01.yetiops.lab:
    - consul
    - exporters.node_exporter.systemd
</code></pre></div><h3 id="verify-the-highstate">Verify the highstate</h3>
<p>As mentioned, the Minion configuration specifies that it will run a <code>highstate</code> when the key is accepted. We can verify this by looking at if it has registered with Consul and the Node Exporter is running: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ consul members
Node               Address            Status  Type    Build  Protocol  DC       Segment
salt-master        10.15.31.249:8301  alive   server  1.8.1  <span style="color:#ae81ff">2</span>         yetiops  &lt;all&gt;
pve-01             10.15.31.7:8301    alive   client  1.8.1  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
vm-01.yetiops.lab  10.15.31.154:8301  alive   client  1.8.1  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;

$ curl 10.15.31.99:9100/metrics | grep -i uname
<span style="color:#75715e"># HELP node_uname_info Labeled system information as provided by the uname system call.</span>
<span style="color:#75715e"># TYPE node_uname_info gauge</span>
node_uname_info<span style="color:#f92672">{</span>domainname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(none)&#34;</span>,machine<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;x86_64&#34;</span>,nodename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;vm-01.yetiops.lab&#34;</span>,release<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;4.19.0-10-cloud-amd64&#34;</span>,sysname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Linux&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;#1 SMP Debian 4.19.132-1 (2020-07-24)&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>
</code></pre></div><p>Looks like it has! Interestingly though, the machine has two different IPv4 addresses. This is because the machine has obtained an IPv4 address from DHCP, while also having an IPv4 address configured by <code>cloud-init</code> (the <code>ipconfig0</code> field in Terraform).</p>
<p>As a last step of verification, we can run another highstate to ensure that no configuration files change, or any other packages are due to be installed: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo salt <span style="color:#e6db74">&#39;vm-01*&#39;</span> state.highstate
vm-01.yetiops.lab:
----------
          ID: consul_binary
    Function: archive.extracted
        Name: /usr/local/bin
      Result: True
     Comment: Path /usr/local/bin/consul exists
     Started: 18:50:01.095502
    Duration: 1.162 ms
     Changes:
----------
          ID: consul_user
    Function: user.present
        Name: consul
      Result: True
     Comment: User consul is present and up to date
     Started: 18:50:01.097816
    Duration: 20.027 ms
     Changes:
----------
          ID: consul_group
    Function: group.present
        Name: consul
      Result: True
     Comment: Group consul is present and up to date
     Started: 18:50:01.118905
    Duration: 5.06 ms
     Changes:
----------
          ID: /etc/systemd/system/consul.service
    Function: file.managed
      Result: True
     Comment: File /etc/systemd/system/consul.service is in the correct state
     Started: 18:50:01.127738
    Duration: 23.481 ms
     Changes:
----------
          ID: /opt/consul
    Function: file.directory
      Result: True
     Comment: The directory /opt/consul is in the correct state
     Started: 18:50:01.151374
    Duration: 1.958 ms
     Changes:
----------
          ID: /etc/consul.d
    Function: file.directory
      Result: True
     Comment: The directory /etc/consul.d is in the correct state
     Started: 18:50:01.153452
    Duration: 1.072 ms
     Changes:
----------
          ID: /etc/consul.d/consul.hcl
    Function: file.managed
      Result: True
     Comment: File /etc/consul.d/consul.hcl is in the correct state
     Started: 18:50:01.154636
    Duration: 14.47 ms
     Changes:
----------
          ID: consul_service
    Function: service.running
        Name: consul
      Result: True
     Comment: The service consul is already running
     Started: 18:50:01.191179
    Duration: 56.911 ms
     Changes:
----------
          ID: /etc/consul.d/node_exporter.hcl
    Function: file.managed
      Result: True
     Comment: File /etc/consul.d/node_exporter.hcl is in the correct state
     Started: 18:50:01.248345
    Duration: 22.7 ms
     Changes:
----------
          ID: consul_reload_node_exporter
    Function: cmd.run
        Name: consul reload
      Result: True
     Comment: Command <span style="color:#e6db74">&#34;consul reload&#34;</span> run
     Started: 18:50:01.272477
    Duration: 236.698 ms
     Changes:
              ----------
              pid:
                  <span style="color:#ae81ff">3396</span>
              retcode:
                  <span style="color:#ae81ff">0</span>
              stderr:
              stdout:
                  Configuration reload triggered
----------
          ID: node_exporter_user
    Function: user.present
        Name: node_exporter
      Result: True
     Comment: User node_exporter is present and up to date
     Started: 18:50:01.509440
    Duration: 4.356 ms
     Changes:
----------
          ID: node_exporter_group
    Function: group.present
        Name: node_exporter
      Result: True
     Comment: Group node_exporter is present and up to date
     Started: 18:50:01.513927
    Duration: 0.839 ms
     Changes:
----------
          ID: /opt/prometheus/exporters/dist/textfile
    Function: file.directory
      Result: True
     Comment: The directory /opt/prometheus/exporters/dist/textfile is in the correct state
     Started: 18:50:01.514882
    Duration: 2.038 ms
     Changes:
----------
          ID: /etc/systemd/system/node_exporter.service
    Function: file.managed
      Result: True
     Comment: File /etc/systemd/system/node_exporter.service is in the correct state
     Started: 18:50:01.517043
    Duration: 12.165 ms
     Changes:
----------
          ID: node_exporter_service_reload
    Function: cmd.run
        Name: systemctl daemon-reload
      Result: True
     Comment: Command <span style="color:#e6db74">&#34;systemctl daemon-reload&#34;</span> run
     Started: 18:50:01.529790
    Duration: 200.263 ms
     Changes:
              ----------
              pid:
                  <span style="color:#ae81ff">3410</span>
              retcode:
                  <span style="color:#ae81ff">0</span>
              stderr:
              stdout:
----------
          ID: node_exporter_service
    Function: service.running
        Name: node_exporter
      Result: True
     Comment: The service node_exporter is already running
     Started: 18:50:01.730351
    Duration: 22.961 ms
     Changes:

Summary <span style="color:#66d9ef">for</span> vm-01.yetiops.lab
-------------
Succeeded: <span style="color:#ae81ff">16</span> <span style="color:#f92672">(</span>changed<span style="color:#f92672">=</span>2<span style="color:#f92672">)</span>
Failed:     <span style="color:#ae81ff">0</span>
-------------
Total states run:     <span style="color:#ae81ff">16</span>
Total run time:  626.161 ms
</code></pre></div><p>The only changes are two states which reload both Consul itself, and also running a <code>systemctl daemon-reload</code> (which is used for when SystemD unit files change). We could further improve the state files so that these only run when other files change.</p>
<h3 id="monitoring-the-hypervisor">Monitoring the hypervisor</h3>
<p>Not only can you monitor the instances running on Proxmox, but you can monitor Proxmox itself. Proxmox is based upon Debian, the current version (6.2 at the time of writing) running on Debian Buster. As far as our current Salt states are concerned, it is configured the same way as any other Debian machine.</p>
<p>This means we can install Consul on the Proxmox hosts, register them with the Consul server, and also run the Prometheus Node Exporter on them to retrieve node metrics: -</p>
<p><strong>/etc/salt/minion</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">master: salt-master.yetiops.lab
id: pve-01.yetiops.lab
nodename: pve-01
</code></pre></div><p><strong>Accept the key in Salt</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo salt-key -L
Accepted Keys:
vm-01.yetiops.lab
salt-master.yetiops.lab
Denied Keys:
Unaccepted Keys:
pve-01.yetiops.lab
Rejected Keys:

$ sudo salt-key -a <span style="color:#e6db74">&#39;pve-01*&#39;</span>
The following keys are going to be accepted:
Unaccepted Keys:
pve-01.yetiops.lab
Proceed? <span style="color:#f92672">[</span>n/Y<span style="color:#f92672">]</span> y
Key <span style="color:#66d9ef">for</span> minion pve-01.yetiops.lab accepted.
</code></pre></div><p><strong>Check the applied states</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo salt <span style="color:#e6db74">&#39;pve-01*&#39;</span> state.show_states
pve-01.yetiops.lab:
    - consul
    - exporters.node_exporter.systemd
</code></pre></div><p><strong>Check that Consul and Node Exporter works as expected</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ consul members
Node               Address            Status  Type    Build  Protocol  DC       Segment
salt-master        10.15.31.249:8301  alive   server  1.8.1  <span style="color:#ae81ff">2</span>         yetiops  &lt;all&gt;
pve-01             10.15.31.7:8301    alive   client  1.8.1  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
vm-01.yetiops.lab  10.15.31.154:8301  alive   client  1.8.1  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;

$ curl 10.15.31.7:9100/metrics | grep -i uname
<span style="color:#75715e"># HELP node_uname_info Labeled system information as provided by the uname system call.</span>
<span style="color:#75715e"># TYPE node_uname_info gauge</span>
node_uname_info<span style="color:#f92672">{</span>domainname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(none)&#34;</span>,machine<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;x86_64&#34;</span>,nodename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pve-01&#34;</span>,release<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;5.4.34-1-pve&#34;</span>,sysname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Linux&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;#1 SMP PVE 5.4.34-2 (Thu, 07 May 2020 10:02:02 +0200)&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>
</code></pre></div><h4 id="proxmox-metrics">Proxmox Metrics</h4>
<p>In addition, we can also use the <a href="https://github.com/prometheus-pve/prometheus-pve-exporter">Prometheus PVE Exporter</a> to monitor Proxmox-specific metrics. This is deployed like so: -</p>
<p><strong>Salt State</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">install_pve_exporter</span>:
  <span style="color:#66d9ef">pip.installed</span>:
    - <span style="color:#66d9ef">name</span>: prometheus-pve-exporter
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">reload_modules</span>: True

<span style="color:#66d9ef">prometheus_user</span>:
  <span style="color:#66d9ef">user.present</span>:
    - <span style="color:#66d9ef">name</span>: prometheus
    - <span style="color:#66d9ef">fullname</span>: Prometheus
    - <span style="color:#66d9ef">shell</span>: /bin/<span style="color:#66d9ef">false</span>

<span style="color:#66d9ef">/etc/prometheus/exporters</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: prometheus
    - <span style="color:#66d9ef">group</span>: prometheus
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/etc/prometheus/exporters/pve.yaml</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://exporters/pve_exporter/files/pve.yaml.j2
    - <span style="color:#66d9ef">user</span>: prometheus
    - <span style="color:#66d9ef">group</span>: prometheus
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0644</span>
    - <span style="color:#66d9ef">template</span>: jinja

<span style="color:#66d9ef">/etc/systemd/system/pve_exporter.service</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://exporters/pve_exporter/files/pve_exporter.service.j2
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0644</span>
    - <span style="color:#66d9ef">template</span>: jinja

<span style="color:#66d9ef">pve_exporter_service_reload</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: systemctl daemon-reload
    - <span style="color:#66d9ef">watch</span>:
      - <span style="color:#66d9ef">file</span>: /etc/systemd/system/pve_exporter.service

<span style="color:#66d9ef">pve_exporter_service</span>:
  <span style="color:#66d9ef">service.running</span>:
  - <span style="color:#66d9ef">name</span>: pve_exporter
  - <span style="color:#66d9ef">enable</span>: True
</code></pre></div><p>The exporter is built using Python, which can be installed using Python&rsquo;s PIP package/module management utility. We also include a state that will install PIP if it is not present already: -</p>
<p><strong>/srv/salt/states/pip/init.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">python3-pip</span>:
  pkg.installed
</code></pre></div><p>The service file is a SystemD unit file: -</p>
<p><strong>/srv/salt/states/exporters/pve_exporter/files/pve_exporter.service.j2</strong></p>
<pre><code>[Unit]
Description=Prometheus PVE Exporter
After=network.target

[Service]
User=prometheus
Group=prometheus
Type=simple
ExecStart=/usr/local/bin/pve_exporter /etc/prometheus/exporters/pve.yaml

[Install]
WantedBy=multi-user.target
</code></pre><p>The <code>pve_exporter</code> uses a YAML configuration file of its own. This defines the Proxmox user and password that is used to authenticate against the Proxmox API. We also use <code>verify_ssl: false</code> because we are using self-signed certificates for the Proxmox API (it is recommended to use a certificate signed by a verified certificate authority in production): -</p>
<p><strong>/srv/salt/states/exporters/pve_exporter/files/pve.yaml.j2</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">$ cat files/pve.yaml.j2
<span style="color:#66d9ef">default</span>:
  <span style="color:#66d9ef">user</span>: {{ pillar[<span style="color:#e6db74">&#39;exporters&#39;</span>][<span style="color:#e6db74">&#39;pve_exporter&#39;</span>][<span style="color:#e6db74">&#39;username&#39;</span>] }}
  <span style="color:#66d9ef">password</span>: {{ pillar[<span style="color:#e6db74">&#39;exporters&#39;</span>][<span style="color:#e6db74">&#39;pve_exporter&#39;</span>][<span style="color:#e6db74">&#39;password&#39;</span>] }}
  <span style="color:#66d9ef">verify_ssl</span>: <span style="color:#66d9ef">false</span>
</code></pre></div><p>The pillars specific to this are: -</p>
<p><strong>/srv/salt/pillars/exporters/proxmox.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">exporters</span>:
  <span style="color:#66d9ef">pve_exporter</span>:
    <span style="color:#66d9ef">username</span>: <span style="color:#e6db74">&#39;pve_monitor@pve&#39;</span>
    <span style="color:#66d9ef">password</span>: <span style="color:#e6db74">&#39;###PASSWORD###&#39;</span>
</code></pre></div><p>You can create this user with the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e"># Create the user</span>
pveum user add pve_monitor@pve --password $PASSWORD

<span style="color:#75715e"># Assign the user the correct role</span>
pveum aclmod / -user pve_monitor@pve -role PVEAuditor 
</code></pre></div><p>The PVEAuditor role is read-only, which is more than sufficient for monitoring.</p>
<p>Finally, we apply the following Consul pillar to Proxmox: -</p>
<p><strong>/srv/salt/pillars/consul/consul-proxmox.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul</span>:
  <span style="color:#66d9ef">prometheus_services</span>:
  - pve_exporter
  - node_exporter
</code></pre></div><p>This means that when the Consul state is applied, it deploys both the <code>node_exporter.hcl</code> Consul service file, and the <code>pve_exporter.hcl</code> Consul service file: -</p>
<p><strong>/srv/salt/states/consul/services/files/pve_exporter.hcl</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl">{<span style="color:#e6db74">&#34;service&#34;</span><span style="color:#960050;background-color:#1e0010">:</span>
  {<span style="color:#e6db74">&#34;name&#34;: &#34;pve_exporter&#34;</span>,
   <span style="color:#e6db74">&#34;tags&#34;: [&#34;pve_exporter&#34;, &#34;prometheus-pve&#34;</span>],
   <span style="color:#e6db74">&#34;port&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#ae81ff">9221</span>
  }
}
</code></pre></div><p>We can verify if this service is available in Consul like so: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ consul catalog services -node<span style="color:#f92672">=</span>pve-01
node_exporter
pve_exporter

$ consul catalog nodes -service<span style="color:#f92672">=</span>pve_exporter
Node    ID        Address     DC
pve-01  1e41c350  10.15.31.7  yetiops
</code></pre></div><h2 id="prometheus">Prometheus</h2>
<h3 id="proxmox-instances">Proxmox Instances</h3>
<p>We use the mostly the same Prometheus configuration as in this <a href="/posts/prometheus-consul-saltstack-part-1-linux/#configuring-prometheus">post</a>. This uses Consul for service discovery, as well as monitoring Prometheus itself: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">global</span>:
  <span style="color:#66d9ef">scrape_interval</span>:     15s
  <span style="color:#66d9ef">evaluation_interval</span>: 15s

<span style="color:#66d9ef">alerting</span>:
  <span style="color:#66d9ef">alertmanagers</span>:
  - <span style="color:#66d9ef">static_configs</span>:
    - <span style="color:#66d9ef">targets</span>:
      - localhost:<span style="color:#ae81ff">9093</span>

<span style="color:#66d9ef">rule_files</span>:
  - <span style="color:#e6db74">&#39;alerts/*.yml&#39;</span>
  - <span style="color:#e6db74">&#39;rules/*.yml&#39;</span>

<span style="color:#66d9ef">scrape_configs</span>:
  - <span style="color:#66d9ef">job_name</span>: <span style="color:#e6db74">&#39;prometheus&#39;</span>
    <span style="color:#66d9ef">static_configs</span>:
    - <span style="color:#66d9ef">targets</span>:
      - <span style="color:#e6db74">&#39;localhost:9090&#39;</span>
  - <span style="color:#66d9ef">job_name</span>: <span style="color:#e6db74">&#39;consul&#39;</span>
    <span style="color:#66d9ef">consul_sd_configs</span>:
      - <span style="color:#66d9ef">server</span>: <span style="color:#e6db74">&#39;10.15.31.249:8500&#39;</span>
    <span style="color:#66d9ef">relabel_configs</span>:
      - <span style="color:#66d9ef">source_labels</span>: [__meta_consul_tags]
        <span style="color:#66d9ef">regex</span>: .<span style="color:#75715e">*,prometheus,.*</span>
        <span style="color:#66d9ef">action</span>: keep
      - <span style="color:#66d9ef">source_labels</span>: [__meta_consul_service]
        <span style="color:#66d9ef">target_label</span>: job
      - <span style="color:#66d9ef">action</span>: labelmap
        <span style="color:#66d9ef">regex</span>: __meta_consul_(.+)
</code></pre></div><p>As SaltStack has already deployed Consul and the Node Exporter for us, we should already be able to see the <code>vm-01</code> instance in our Prometheus targets: -</p>
<p><img src="/img/proxmox/proxmox-prometheus-instance-target.png" alt="Proxmox Instances - Prometheus Targets"></p>
<p>I have also added the <code>labelmap</code> relabelling configuration (discussed <a href="/posts/prometheus-service-discovery-openstack/#prometheus">here</a>) so that all metadata received from Consul will be added as a label to each discovered metric: -</p>
<p><img src="/img/proxmox/proxmox-prometheus-consul-metadata.png" alt="Proxmox - Prometheus Consul Metadata"></p>
<p>Each tag that starts <code>__meta_consul_</code> will be added as a label, but with the <code>__meta_consul_</code> prefix removed: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">node_uname_info<span style="color:#f92672">{</span>address<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;10.15.31.154&#34;</span>,dc<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;yetiops&#34;</span>,domainname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(none)&#34;</span>,
  instance<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;10.15.31.154:9100&#34;</span>,job<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;node_exporter&#34;</span>,machine<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;x86_64&#34;</span>,
  node<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;vm-01.yetiops.lab&#34;</span>,nodename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;vm-01.yetiops.lab&#34;</span>,
  release<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;4.19.0-10-cloud-amd64&#34;</span>,service<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;node_exporter&#34;</span>,
  service_id<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;node_exporter&#34;</span>,service_port<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;9100&#34;</span>,sysname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Linux&#34;</span>,
  tagged_address_lan<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;10.15.31.154&#34;</span>,tagged_address_lan_ipv4<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;10.15.31.154&#34;</span>,
  tagged_address_wan<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;10.15.31.154&#34;</span>,tagged_address_wan_ipv4<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;10.15.31.154&#34;</span>,tags<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;,
</span><span style="color:#e6db74">  node_exporter,prometheus,&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;#1 SMP Debian 4.19.132-1 (2020-07-24)&#34;</span><span style="color:#f92672">}</span>
</code></pre></div><p>This might seem like a lot of labels for a single metric, and may be unnecessary for most implementations. It does however allow additional flexibility in how you match/ignore metrics with PromQL queries.</p>
<h3 id="proxmox-hosts">Proxmox Hosts</h3>
<p>In addition to monitoring the instances, we can monitor the Proxmox host itself. The <code>node_exporter</code> service will be discovered from Consul by the <code>job_name: consul</code>: -</p>
<p><img src="/img/proxmox/proxmox-prometheus-host-target-node.png" alt="Prometheus Targets - Proxmox Host and Node Exporter"></p>
<p>The last target in the list is our Proxmox host.</p>
<p>For the PVE Exporter, we need to use a different job for two reasons: -</p>
<ul>
<li>The metrics path is different from the default Prometheus metrics path (<code>/pve</code> rather than <code>/metrics</code>)</li>
<li>The Proxmox API can take a few seconds to return the metrics to the exporter, meaning that scraping it every 15 seconds may either miss metrics or overload the API/Exporter</li>
</ul>
<p>Therefore we add the following job configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  - <span style="color:#66d9ef">job_name</span>: <span style="color:#e6db74">&#39;consul-pve&#39;</span>
    <span style="color:#66d9ef">scrape_interval</span>: 30s
    <span style="color:#66d9ef">metrics_path</span>: /pve
    <span style="color:#66d9ef">consul_sd_configs</span>:
      - <span style="color:#66d9ef">server</span>: <span style="color:#e6db74">&#39;10.15.31.249:8500&#39;</span>
    <span style="color:#66d9ef">relabel_configs</span>:
      - <span style="color:#66d9ef">source_labels</span>: [__meta_consul_tags]
        <span style="color:#66d9ef">regex</span>: .<span style="color:#75715e">*,prometheus-pve,.*</span>
        <span style="color:#66d9ef">action</span>: keep
      - <span style="color:#66d9ef">source_labels</span>: [__meta_consul_service]
        <span style="color:#66d9ef">target_label</span>: job
</code></pre></div><p>This job matches on the <code>prometheus-pve</code> tag (rather than just <code>prometheus</code>), increases the scrape interval (i.e. the time between polling the exporter for metrics) to 30 seconds, and changes the <code>metrics_path</code> to <code>pve</code>.</p>
<p>We can verify the target is available with the below: -</p>
<p><img src="/img/proxmox/proxmox-prometheus-host-target-pve.png" alt="Prometheus Targets - Proxmox Host and PVE Exporter"></p>
<h2 id="grafana">Grafana</h2>
<p>Now that we have discovered the instances and the Proxmox hosts, we can start using dashboard in Grafana to view their metrics.</p>
<h3 id="node-exporter">Node Exporter</h3>
<p>For the Node Exporter, metrics, we can use the <a href="https://grafana.com/grafana/dashboards/1860">Node Exporter Full</a> dashboard that we have used in previous posts: -</p>
<p><strong>Listing the instances</strong>
<img src="/img/proxmox/proxmox-grafana-node-exporter-full-list.png" alt="Proxmox - Grafana Node Exporter Full Dashboard - Instance List"></p>
<p><strong>Metrics for vm-01</strong>
<img src="/img/proxmox/proxmox-grafana-node-exporter-vm-01.png" alt="Proxmox - Grafana Node Exporter Full Dashboard - vm-01"></p>
<p><strong>Metrics for pve-01 (Proxmox host)</strong>
<img src="/img/proxmox/proxmox-grafana-node-exporter-pve-01.png" alt="Proxmox - Grafana Node Exporter Full Dashboard - pve-01"></p>
<h3 id="pve-exporter">PVE Exporter</h3>
<p>The PVE Exporter also has dashboards available, like the <a href="https://grafana.com/grafana/dashboards/10347">Proxmox via Prometheus</a> dashboard. You can view the configured instances, memory/CPU/network/disk usage per instance, the Proxmox host storage and more: -</p>
<p><img src="/img/proxmox/proxmox-grafana-pve-exporter.png" alt="Proxmox - Grafana PVE Exporter Dashboard"></p>
<h2 id="what-about-other-distributions">What about other distributions?</h2>
<p>Cloud-Init and <code>cloud-config</code> is available for multiple operating systems (not just Debian derivatives). Most of the <code>cloud-config</code> syntax will be identical across different systems, with the only real differences being when adding extra repositories for their respective package managers (e.g. using <code>yum</code> or <code>dnf</code> rather than <code>apt</code>).</p>
<p>We will now apply the same steps as for the Debian instance, except this time for a Fedora-based instance: -</p>
<h3 id="create-the-template">Create the template</h3>
<p>To create the template, we use the same commands as <a href="/posts/proxmox-terraform-cloudinit-saltstack-prometheus/#creating-a-template">before</a>, but using a Fedora image (available from <a href="https://alt.fedoraproject.org/cloud/">here</a>). We are using the &ldquo;Cloud Base image for Openstack&rdquo;. This image is for x86_64 systems that includes Cloud-Init and other dependencies: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">qm create <span style="color:#ae81ff">9001</span> -name fedora-cloudinit -memory <span style="color:#ae81ff">1024</span> -net0 virtio,bridge<span style="color:#f92672">=</span>vmbr0 -cores <span style="color:#ae81ff">1</span> -sockets <span style="color:#ae81ff">1</span>
qm importdisk <span style="color:#ae81ff">9001</span> Fedora-Cloud-Base-32-1.6.x86_64.qcow2 local-lvm
qm set <span style="color:#ae81ff">9001</span> -scsihw virtio-scsi-pci -virtio0 local-lvm:vm-9001-disk-0
qm set <span style="color:#ae81ff">9001</span> -serial0 socket
qm set <span style="color:#ae81ff">9001</span> -boot c -bootdisk virtio0
qm set <span style="color:#ae81ff">9001</span> -agent <span style="color:#ae81ff">1</span>
qm set <span style="color:#ae81ff">9001</span> -hotplug disk,network,usb
qm set <span style="color:#ae81ff">9001</span> -vcpus <span style="color:#ae81ff">1</span>
qm set <span style="color:#ae81ff">9001</span> -vga qxl
qm set <span style="color:#ae81ff">9001</span> -ide2 local-lvm:cloudinit
qm resize <span style="color:#ae81ff">9001</span> virtio0 +8G
qm template <span style="color:#ae81ff">9001</span>
</code></pre></div><p>We can verify this has been created with <code>qm list</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">root@pve-01:~# qm list
      VMID NAME                 STATUS     MEM<span style="color:#f92672">(</span>MB<span style="color:#f92672">)</span>    BOOTDISK<span style="color:#f92672">(</span>GB<span style="color:#f92672">)</span> PID
       <span style="color:#ae81ff">100</span> vm-01                running    <span style="color:#ae81ff">512</span>               12.00 <span style="color:#ae81ff">1711</span>
      <span style="color:#ae81ff">9000</span> debian-cloudinit     stopped    <span style="color:#ae81ff">1024</span>              12.00 <span style="color:#ae81ff">0</span>
      <span style="color:#ae81ff">9001</span> fedora-cloudinit     stopped    <span style="color:#ae81ff">1024</span>              12.00 <span style="color:#ae81ff">0</span> &lt;-----
</code></pre></div><h3 id="define-an-instance-1">Define an instance</h3>
<p>We define the instance similarly to before, except with a couple of key differences: -</p>
<ul>
<li>The <code>cloud-config</code> file has some minor changes</li>
<li>We need to reference the Fedora <code>cloud-config</code> file in the Terraform configuration</li>
<li>The instance requires 1G of memory, otherwise you will hit Out-Of-Memory (OOM) conditions when attempting to install/upgrade packages</li>
</ul>
<p><strong>cloud-config</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e">#cloud-config</span>

<span style="color:#66d9ef">package_update</span>: <span style="color:#66d9ef">true</span>
<span style="color:#66d9ef">package_upgrade</span>: <span style="color:#66d9ef">true</span>

<span style="color:#66d9ef">packages</span>:
  - jq
  - curl
  - qemu-guest-agent
  - salt-minion
  - wget
  - policycoreutils-python-utils

<span style="color:#66d9ef">users</span>:
- <span style="color:#66d9ef">name</span>: stuart
  <span style="color:#66d9ef">groups</span>: wheel
  <span style="color:#66d9ef">shell</span>: /bin/bash
  <span style="color:#66d9ef">sudo</span>: [<span style="color:#e6db74">&#39;ALL=(ALL) NOPASSWD:ALL&#39;</span>]
  <span style="color:#66d9ef">ssh_authorized_keys</span>:
    - ${ssh_key}

<span style="color:#66d9ef">preserve_hostname</span>: <span style="color:#66d9ef">false</span>
<span style="color:#66d9ef">manage_etc_hosts</span>: <span style="color:#66d9ef">false</span>
<span style="color:#66d9ef">fqdn</span>: ${hostname}.${domain}

<span style="color:#66d9ef">configsalt</span>:
  - <span style="color:#75715e">&amp;config_salt</span> <span style="color:#e6db74">|
</span><span style="color:#e6db74">    echo &#34;master: salt-master.${domain}&#34; &gt;&gt; /etc/salt/minion</span>
    echo <span style="color:#e6db74">&#34;domain: ${domain}&#34;</span> &gt;&gt; /etc/salt/grains
    echo <span style="color:#e6db74">&#34;startup_states: &#39;highstate&#39;&#34;</span> &gt;&gt; /etc/salt/minion
    echo <span style="color:#e6db74">&#34;id: ${hostname}.${domain}&#34;</span> &gt;&gt; /etc/salt/minion
    hostnamectl set-hostname <span style="color:#e6db74">&#34;${hostname}.${domain}&#34;</span> &gt; /etc/hostname

<span style="color:#66d9ef">restartsalt</span>:
  - <span style="color:#75715e">&amp;restart_salt</span> <span style="color:#e6db74">|
</span><span style="color:#e6db74">    systemctl restart salt-minion.service</span>

<span style="color:#66d9ef">runcmd</span>:
 - [ sh, -c, <span style="color:#75715e">*config_salt</span> ]
 - [ sh, -c, <span style="color:#75715e">*restart_salt</span> ]
</code></pre></div><p>The differences from the Debian <code>cloud-config</code> file are: -</p>
<ul>
<li><code>wget</code> is not installed by default, so we add it to <code>packages</code> list</li>
<li>Salt requires <code>policycoreutils-python-utils</code> to manage SELinux policies as Fedora uses SELinux by default</li>
<li>There is no official Salt repository for Fedora currently, so we install the <code>salt-minion</code> package from the Fedora package repositories</li>
<li><code>wheel</code> is the group used by privileged users in Fedora, rather than <code>sudo</code> (like Debian, Ubuntu and derivatives)</li>
</ul>
<p><strong>vm-02.tf</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#75715e"># Source the Cloud Init Config file
</span><span style="color:#75715e"></span><span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;template_file&#34; &#34;cloud_init_fedora_vm-02&#34;</span> {
  template  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${file(&#34;${path.module}/files/cloud_init_fedora.cloud_config&#34;)}&#34;</span>

  vars <span style="color:#f92672">=</span> {
    ssh_key <span style="color:#f92672">=</span> <span style="color:#66d9ef">file</span>(<span style="color:#e6db74">&#34;~/.ssh/id_rsa.pub&#34;</span>)
    hostname <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vm-02&#34;</span>
    domain <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops.lab&#34;</span>
  }
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e"># Create a local copy of the file, to transfer to Proxmox
</span><span style="color:#75715e"></span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;local_file&#34; &#34;cloud_init_fedora_vm-02&#34;</span> {
  content   <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">template_file</span>.<span style="color:#66d9ef">cloud_init_fedora_vm</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">02</span>.<span style="color:#66d9ef">rendered</span>
  filename  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${path.module}/files/user_data_cloud_init_fedora_vm-02.cfg&#34;</span>
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e"># Transfer the file to the Proxmox Host
</span><span style="color:#75715e"></span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;null_resource&#34; &#34;cloud_init_fedora_vm-02&#34;</span> {
  <span style="color:#66d9ef">connection</span> {
    type    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ssh&#34;</span>
    user    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;root&#34;</span>
    private_key <span style="color:#f92672">=</span> <span style="color:#66d9ef">file</span>(<span style="color:#e6db74">&#34;~/.ssh/id_rsa&#34;</span>)
    host    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;10.15.31.7&#34;</span>
  }

  <span style="color:#66d9ef">provisioner</span> <span style="color:#e6db74">&#34;file&#34;</span> {
    source       <span style="color:#f92672">=</span> <span style="color:#66d9ef">local_file</span>.<span style="color:#66d9ef">cloud_init_fedora_vm</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">02</span>.<span style="color:#66d9ef">filename</span>
    destination  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/var/lib/vz/snippets/cloud_init_fedora_vm-02.yml&#34;</span>
  }
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e"># Create the VM
</span><span style="color:#75715e"></span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;proxmox_vm_qemu&#34; &#34;vm-02&#34;</span> {<span style="color:#75715e">
</span><span style="color:#75715e">  ## Wait for the cloud-config file to exist
</span><span style="color:#75715e"></span>
  depends_on <span style="color:#f92672">=</span> [
    <span style="color:#66d9ef">null_resource</span>.<span style="color:#66d9ef">cloud_init_fedora_vm</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">02</span>
  ]

  name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vm-02&#34;</span>
  target_node <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;pve-01&#34;</span><span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">  # Clone from debian-cloudinit template
</span><span style="color:#75715e"></span>  clone <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;fedora-cloudinit&#34;</span>
  os_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cloud-init&#34;</span><span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">  # Cloud init options
</span><span style="color:#75715e"></span>  cicustom <span style="color:#f92672">=</span> &#34;user<span style="color:#f92672">=</span><span style="color:#66d9ef">local</span><span style="color:#960050;background-color:#1e0010">:</span><span style="color:#66d9ef">snippets</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">cloud_init_fedora_vm</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">02</span>.<span style="color:#66d9ef">yml</span><span style="color:#960050;background-color:#1e0010">&#34;</span>
  ipconfig0 <span style="color:#f92672">=</span> &#34;ip<span style="color:#f92672">=</span>10.15.31.97/24,gw<span style="color:#f92672">=</span><span style="color:#ae81ff">10</span>.<span style="color:#ae81ff">15</span>.<span style="color:#ae81ff">31</span>.<span style="color:#ae81ff">253</span><span style="color:#960050;background-color:#1e0010">&#34;</span>

  memory       <span style="color:#f92672">=</span> <span style="color:#ae81ff">1024</span>
  agent        <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span><span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">  # Set the boot disk paramters
</span><span style="color:#75715e"></span>  bootdisk <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;scsi0&#34;</span>
  scsihw       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;virtio-scsi-pci&#34;</span>

  <span style="color:#66d9ef">disk</span> {
    id              <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
    size            <span style="color:#f92672">=</span> <span style="color:#ae81ff">10</span>
    type            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;scsi&#34;</span>
    storage         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;local-lvm&#34;</span>
    storage_type    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;lvm&#34;</span>
    iothread        <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
  }<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">  # Set the network
</span><span style="color:#75715e"></span>  <span style="color:#66d9ef">network</span> {
    id <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
    model <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;virtio&#34;</span>
    bridge <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vmbr0&#34;</span>
  }<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">  # Ignore changes to the network
</span><span style="color:#75715e">  ## MAC address is generated on every apply, causing
</span><span style="color:#75715e">  ## TF to think this needs to be rebuilt on every apply
</span><span style="color:#75715e"></span>  <span style="color:#66d9ef">lifecycle</span> {
     ignore_changes <span style="color:#f92672">=</span> [
       <span style="color:#66d9ef">network</span>
     ]
  }
}
</code></pre></div><p>As noted, the differences here are: -</p>
<ul>
<li>We reference the Fedora <code>cloud-config</code> file</li>
<li>The memory is increased to 1G</li>
</ul>
<h3 id="build-the-instance-1">Build the instance</h3>
<p>We can now build the Fedora instance, and see if it attempts to register with Salt: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">data.template_file.cloud_init_fedora_vm-02: Refreshing state...
data.template_file.cloud_init_deb10_vm-01: Refreshing state...
local_file.cloud_init_deb10_vm-01: Refreshing state... <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>ba8ba5e0ff96906173046a818ba8305964d84315<span style="color:#f92672">]</span>
null_resource.cloud_init_deb10_vm-01: Refreshing state... <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>6696639824705285611<span style="color:#f92672">]</span>
proxmox_vm_qemu.vm-01: Refreshing state... <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>pve-01/qemu/100<span style="color:#f92672">]</span>

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  <span style="color:#75715e"># local_file.cloud_init_fedora_vm-02 will be created</span>
  + resource <span style="color:#e6db74">&#34;local_file&#34;</span> <span style="color:#e6db74">&#34;cloud_init_fedora_vm-02&#34;</span> <span style="color:#f92672">{</span>
      + content              <span style="color:#f92672">=</span> &lt;&lt;~EOT
            <span style="color:#75715e">#cloud-config</span>

            package_update: true
            package_upgrade: true

            packages:
              - jq
              - curl
              - qemu-guest-agent
              - salt-minion
              - wget
              - policycoreutils-python-util

            users:
            - name: stuart
              groups: wheel
              shell: /bin/bash
              sudo: <span style="color:#f92672">[</span><span style="color:#e6db74">&#39;ALL=(ALL) NOPASSWD:ALL&#39;</span><span style="color:#f92672">]</span>
              ssh_authorized_keys:
                - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7kFxH6D3tU3vL+O1PMhgmdB7Hh4pIWzVyxamtPmqDosgwcchAfaW5hvbb2apCCRN8wop/pCfXkCNhDFkAuOND7pl8eO2coX4yQEViye9MuFu4hIf3pxBkKrSunKCxesi27rW7QUN2XRercM5CrtuFcCwS9+/JxKm/oScSgs4wEnPZOfcHaUQOnBw/oZBv5z//D8exnmrr2EE7VF5bAQixrM5Wkks4+t6nKmtdDmVcOiYfe2RUzNFZ5xrg0AZgJdY1e6jOJwFXUKv/OOW29oTA6BR5kULSDHdifihpY5keRcW+cvEo4zj65TrRtUiOIO1LUfsHDBK6kM3v3IvKNHyZ stuh84@symphonyx


            preserve_hostname: false
            manage_etc_hosts: false
            fqdn: vm-02.yetiops.lab

            configsalt:
              - &amp;config_salt |
                echo <span style="color:#e6db74">&#34;master: salt-master.yetiops.lab&#34;</span> &gt;&gt; /etc/salt/minion
                echo <span style="color:#e6db74">&#34;domain: yetiops.lab&#34;</span> &gt;&gt; /etc/salt/grains
                echo <span style="color:#e6db74">&#34;startup_states: &#39;highstate&#39;&#34;</span> &gt;&gt; /etc/salt/minion
                echo <span style="color:#e6db74">&#34;id: vm-02.yetiops.lab&#34;</span> &gt;&gt; /etc/salt/minion
                hostnamectl set-hostname <span style="color:#e6db74">&#34;vm-02.yetiops.lab&#34;</span> &gt; /etc/hostname

            restartsalt:
              - &amp;restart_salt |
                systemctl restart salt-minion.service

            runcmd:
             - <span style="color:#f92672">[</span> sh, -c, *config_salt <span style="color:#f92672">]</span>
             - <span style="color:#f92672">[</span> sh, -c, *restart_salt <span style="color:#f92672">]</span>
        EOT
      + directory_permission <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0777&#34;</span>
      + file_permission      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0777&#34;</span>
      + filename             <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;./files/user_data_cloud_init_fedora_vm-02.cfg&#34;</span>
      + id                   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># null_resource.cloud_init_fedora_vm-02 will be created</span>
  + resource <span style="color:#e6db74">&#34;null_resource&#34;</span> <span style="color:#e6db74">&#34;cloud_init_fedora_vm-02&#34;</span> <span style="color:#f92672">{</span>
      + id <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># proxmox_vm_qemu.vm-02 will be created</span>
  + resource <span style="color:#e6db74">&#34;proxmox_vm_qemu&#34;</span> <span style="color:#e6db74">&#34;vm-02&#34;</span> <span style="color:#f92672">{</span>
      + agent        <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>
      + balloon      <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
      + bios         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;seabios&#34;</span>
      + boot         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cdn&#34;</span>
      + bootdisk     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;scsi0&#34;</span>
      + cicustom     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;user=local:snippets/cloud_init_fedora_vm-02.yml&#34;</span>
      + clone        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;fedora-cloudinit&#34;</span>
      + clone_wait   <span style="color:#f92672">=</span> <span style="color:#ae81ff">15</span>
      + cores        <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>
      + cpu          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;host&#34;</span>
      + force_create <span style="color:#f92672">=</span> false
      + full_clone   <span style="color:#f92672">=</span> true
      + hotplug      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;network,disk,usb&#34;</span>
      + id           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + ipconfig0    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ip=10.15.31.97/24,gw=10.15.31.253&#34;</span>
      + kvm          <span style="color:#f92672">=</span> true
      + memory       <span style="color:#f92672">=</span> <span style="color:#ae81ff">1024</span>
      + name         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vm-02&#34;</span>
      + numa         <span style="color:#f92672">=</span> false
      + onboot       <span style="color:#f92672">=</span> true
      + os_type      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cloud-init&#34;</span>
      + preprovision <span style="color:#f92672">=</span> true
      + scsihw       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;virtio-scsi-pci&#34;</span>
      + sockets      <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>
      + ssh_host     <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + ssh_port     <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + target_node  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;pve-01&#34;</span>
      + vcpus        <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
      + vlan         <span style="color:#f92672">=</span> -1
      + vmid         <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>

      + disk <span style="color:#f92672">{</span>
          + backup       <span style="color:#f92672">=</span> false
          + cache        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;none&#34;</span>
          + format       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;raw&#34;</span>
          + id           <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
          + iothread     <span style="color:#f92672">=</span> true
          + mbps         <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
          + mbps_rd      <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
          + mbps_rd_max  <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
          + mbps_wr      <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
          + mbps_wr_max  <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
          + replicate    <span style="color:#f92672">=</span> false
          + size         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;10&#34;</span>
          + storage      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;local-lvm&#34;</span>
          + storage_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;lvm&#34;</span>
          + type         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;scsi&#34;</span>
        <span style="color:#f92672">}</span>

      + network <span style="color:#f92672">{</span>
          + bridge    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vmbr0&#34;</span>
          + firewall  <span style="color:#f92672">=</span> false
          + id        <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
          + link_down <span style="color:#f92672">=</span> false
          + macaddr   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + model     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;virtio&#34;</span>
          + queues    <span style="color:#f92672">=</span> -1
          + rate      <span style="color:#f92672">=</span> -1
          + tag       <span style="color:#f92672">=</span> -1
        <span style="color:#f92672">}</span>
    <span style="color:#f92672">}</span>

Plan: <span style="color:#ae81ff">3</span> to add, <span style="color:#ae81ff">0</span> to change, <span style="color:#ae81ff">0</span> to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only <span style="color:#e6db74">&#39;yes&#39;</span> will be accepted to approve.

  Enter a value: yes

local_file.cloud_init_fedora_vm-02: Creating...
local_file.cloud_init_fedora_vm-02: Creation complete after 0s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>d1529d4e3dbfdbe4e397695cbcfeb8468db09823<span style="color:#f92672">]</span>
null_resource.cloud_init_fedora_vm-02: Creating...
null_resource.cloud_init_fedora_vm-02: Provisioning with <span style="color:#e6db74">&#39;file&#39;</span>...
null_resource.cloud_init_fedora_vm-02: Creation complete after 0s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>5678816832361784306<span style="color:#f92672">]</span>
proxmox_vm_qemu.vm-02: Creating...
proxmox_vm_qemu.vm-02: Still creating... <span style="color:#f92672">[</span>10s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.vm-02: Still creating... <span style="color:#f92672">[</span>20s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.vm-02: Still creating... <span style="color:#f92672">[</span>30s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.vm-02: Still creating... <span style="color:#f92672">[</span>40s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.vm-02: Still creating... <span style="color:#f92672">[</span>50s elapsed<span style="color:#f92672">]</span>
proxmox_vm_qemu.vm-02: Creation complete after 53s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>pve-01/qemu/101<span style="color:#f92672">]</span>

Apply complete! Resources: <span style="color:#ae81ff">3</span> added, <span style="color:#ae81ff">0</span> changed, <span style="color:#ae81ff">0</span> destroyed.
</code></pre></div><p>Can we see it on the Salt server?</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo salt-key -L
Accepted Keys:
pve-01.yetiops.lab
salt-master.yetiops.lab
vm-01.yetiops.lab
Denied Keys:
Unaccepted Keys:
vm-02.yetiops.lab
Rejected Keys:
</code></pre></div><p>We can!</p>
<h3 id="accept-the-key-1">Accept the key</h3>
<p>We can now accept the key in Salt. As with the Debian instance, this will immediately start a <code>highstate</code>. The <code>highstate</code> deploys Consul and the Prometheus Node Exporter, meaning we should see it in Prometheus once it completes: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo salt-key -a <span style="color:#e6db74">&#39;vm-02*&#39;</span>
The following keys are going to be accepted:
Unaccepted Keys:
vm-02.yetiops.lab
Proceed? <span style="color:#f92672">[</span>n/Y<span style="color:#f92672">]</span> y
Key <span style="color:#66d9ef">for</span> minion vm-02.yetiops.lab accepted.
</code></pre></div><p>We can check what states will be applied to this minion as well: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo salt <span style="color:#e6db74">&#39;vm-02*&#39;</span> state.show_states
vm-02.yetiops.lab:
    - consul
    - exporters.node_exporter.systemd
</code></pre></div><h3 id="verify-the-highstate-1">Verify the highstate</h3>
<p>Once the initial <code>highstate</code> has ran, we should now be able to see <code>vm-02</code> (the Fedora instance) in Consul: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ consul members
Node               Address            Status  Type    Build  Protocol  DC       Segment
salt-master        10.15.31.249:8301  alive   server  1.8.1  <span style="color:#ae81ff">2</span>         yetiops  &lt;all&gt;
pve-01             10.15.31.7:8301    alive   client  1.8.1  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
vm-01.yetiops.lab  10.15.31.154:8301  alive   client  1.8.1  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
vm-02.yetiops.lab  10.15.31.97:8301   alive   client  1.8.1  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
</code></pre></div><p>We should also be able to check that the Node Exporter is running: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ curl 10.15.31.97:9100/metrics | grep -i uname
<span style="color:#75715e"># HELP node_uname_info Labeled system information as provided by the uname system call.</span>
<span style="color:#75715e"># TYPE node_uname_info gauge</span>
node_uname_info<span style="color:#f92672">{</span>domainname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(none)&#34;</span>,machine<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;x86_64&#34;</span>,nodename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;vm-02.yetiops.lab&#34;</span>,release<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;5.6.6-300.fc32.x86_64&#34;</span>,sysname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Linux&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;#1 SMP Tue Apr 21 13:44:19 UTC 2020&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>
</code></pre></div><p>All looking good!</p>
<p>We can also re-run the <code>highstate</code>, to ensure no configuration changes are required: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo salt <span style="color:#e6db74">&#39;vm-02*&#39;</span> state.highstate
vm-02.yetiops.lab:
----------
          ID: consul_binary
    Function: archive.extracted
        Name: /usr/local/bin
      Result: True
     Comment: Path /usr/local/bin/consul exists
     Started: 13:23:03.623496
    Duration: 2.937 ms
     Changes:
----------
          ID: consul_user
    Function: user.present
        Name: consul
      Result: True
     Comment: User consul is present and up to date
     Started: 13:23:03.627282
    Duration: 44.905 ms
     Changes:
----------
          ID: consul_group
    Function: group.present
        Name: consul
      Result: True
     Comment: Group consul is present and up to date
     Started: 13:23:03.673917
    Duration: 11.163 ms
     Changes:
----------
          ID: /etc/systemd/system/consul.service
    Function: file.managed
      Result: True
     Comment: File /etc/systemd/system/consul.service is in the correct state
     Started: 13:23:03.688362
    Duration: 48.745 ms
     Changes:
----------
          ID: /opt/consul
    Function: file.directory
      Result: True
     Comment: The directory /opt/consul is in the correct state
     Started: 13:23:03.737332
    Duration: 2.013 ms
     Changes:
----------
          ID: /etc/consul.d
    Function: file.directory
      Result: True
     Comment: The directory /etc/consul.d is in the correct state
     Started: 13:23:03.739451
    Duration: 0.817 ms
     Changes:
----------
          ID: /etc/consul.d/consul.hcl
    Function: file.managed
      Result: True
     Comment: File /etc/consul.d/consul.hcl is in the correct state
     Started: 13:23:03.740365
    Duration: 32.16 ms
     Changes:
----------
          ID: consul_service
    Function: service.running
        Name: consul
      Result: True
     Comment: The service consul is already running
     Started: 13:23:03.796163
    Duration: 107.187 ms
     Changes:
----------
          ID: /etc/consul.d/node_exporter.hcl
    Function: file.managed
      Result: True
     Comment: File /etc/consul.d/node_exporter.hcl is in the correct state
     Started: 13:23:03.903681
    Duration: 24.074 ms
     Changes:
----------
          ID: consul_reload_node_exporter
    Function: cmd.run
        Name: consul reload
      Result: True
     Comment: Command <span style="color:#e6db74">&#34;consul reload&#34;</span> run
     Started: 13:23:03.935968
    Duration: 124.716 ms
     Changes:
              ----------
              pid:
                  <span style="color:#ae81ff">17428</span>
              retcode:
                  <span style="color:#ae81ff">0</span>
              stderr:
              stdout:
                  Configuration reload triggered
----------
          ID: node_exporter_user
    Function: user.present
        Name: node_exporter
      Result: True
     Comment: User node_exporter is present and up to date
     Started: 13:23:04.060975
    Duration: 4.172 ms
     Changes:
----------
          ID: node_exporter_group
    Function: group.present
        Name: node_exporter
      Result: True
     Comment: Group node_exporter is present and up to date
     Started: 13:23:04.065357
    Duration: 0.784 ms
     Changes:
----------
          ID: /opt/prometheus/exporters/dist/textfile
    Function: file.directory
      Result: True
     Comment: The directory /opt/prometheus/exporters/dist/textfile is in the correct state
     Started: 13:23:04.066249
    Duration: 1.028 ms
     Changes:
----------
          ID: /etc/systemd/system/node_exporter.service
    Function: file.managed
      Result: True
     Comment: File /etc/systemd/system/node_exporter.service is in the correct state
     Started: 13:23:04.067377
    Duration: 14.781 ms
     Changes:
----------
          ID: node_exporter_selinux_fcontext
    Function: selinux.fcontext_policy_present
        Name: /usr/local/bin/node_exporter
      Result: True
     Comment: SELinux policy <span style="color:#66d9ef">for</span> <span style="color:#e6db74">&#34;/usr/local/bin/node_exporter&#34;</span> already present with specified filetype <span style="color:#e6db74">&#34;all files&#34;</span> and sel_type <span style="color:#e6db74">&#34;bin_t&#34;</span>.
     Started: 13:23:04.084381
    Duration: 488.474 ms
     Changes:
----------
          ID: node_exporter_selinux_fcontext_applied
    Function: selinux.fcontext_policy_applied
        Name: /usr/local/bin/node_exporter
      Result: True
     Comment: SElinux policies are already applied <span style="color:#66d9ef">for</span> filespec <span style="color:#e6db74">&#34;/usr/local/bin/node_exporter&#34;</span>
     Started: 13:23:04.573102
    Duration: 8.826 ms
     Changes:
----------
          ID: node_exporter_service_reload
    Function: cmd.run
        Name: systemctl daemon-reload
      Result: True
     Comment: Command <span style="color:#e6db74">&#34;systemctl daemon-reload&#34;</span> run
     Started: 13:23:04.582755
    Duration: 267.884 ms
     Changes:
              ----------
              pid:
                  <span style="color:#ae81ff">17441</span>
              retcode:
                  <span style="color:#ae81ff">0</span>
              stderr:
              stdout:
----------
          ID: node_exporter_service
    Function: service.running
        Name: node_exporter
      Result: True
     Comment: The service node_exporter is already running
     Started: 13:23:04.850912
    Duration: 38.384 ms
     Changes:

Summary <span style="color:#66d9ef">for</span> vm-02.yetiops.lab
-------------
Succeeded: <span style="color:#ae81ff">18</span> <span style="color:#f92672">(</span>changed<span style="color:#f92672">=</span>2<span style="color:#f92672">)</span>
Failed:     <span style="color:#ae81ff">0</span>
-------------
Total states run:     <span style="color:#ae81ff">18</span>
Total run time:    1.223 s
</code></pre></div><p>As before, no configuration changes, just a couple of states which could be improved to not run unless other files have changed first.</p>
<h3 id="prometheus-targets">Prometheus Targets</h3>
<p>We should now be able to see the Fedora instance in Prometheus: -</p>
<p><img src="/img/proxmox/proxmox-prometheus-instance-target-vm-02.png" alt="Proxmox - vm-02 Instance Prometheus Target"></p>
<p>There it is!</p>
<h3 id="grafana-1">Grafana</h3>
<p>We should now also be able to see this instance in our Node Exporter dashboard in Grafana: -</p>
<p><img src="/img/proxmox/proxmox-grafana-node-exporter-vm-02.png" alt="Proxmox - Grafana vm-02 Node Exporter Dashboard"></p>
<h3 id="any-other-distributions">Any other distributions?</h3>
<p>Any distribution which supports Cloud-Init and all of the <code>cloud-config</code> options should work with the method shown here. For a full list of distributions and systems supporting Cloud-Init, you can check: -</p>
<ul>
<li><a href="https://cloudinit.readthedocs.io/en/latest/topics/availability.html">Cloud-Init Availability</a></li>
<li><a href="https://pve.proxmox.com/wiki/Cloud-Init_Support">Proxmox Cloud-Init Documentation</a></li>
<li><a href="https://docs.openstack.org/image-guide/obtain-images.html">OpenStack Images</a></li>
</ul>
<p>While it may not be true for every system, it is reasonably safe to assume that if a distribution or system runs on a public cloud provider (e.g. AWS or Azure) or on OpenStack, then it will probably work with Proxmox using Cloud-Init too.</p>
<h2 id="summary">Summary</h2>
<p>While many workloads are now moving to public clouds, there are still many use cases for on-premise hypervisors like Proxmox. Whether this is in a homelab, running and managing your own hardware in a data centre (but without the need for OpenStack&rsquo;s feature-set) or simply as an alternative to VMWare and its licensing model.</p>
<p>Using Terraform, Cloud-Init and SaltStack together can reduce the time and effort to manage instances on Proxmox, as well as providing some of the benefits you would find from using a cloud provider (e.g. Infrastructure-as-Code, ephemeral instances, monitoring automation).</p>
]]></content>
        </item>
        
        <item>
            <title>Prometheus - Using Service Discovery with OpenStack</title>
            <link>https://yetiops.net/posts/prometheus-service-discovery-openstack/</link>
            <pubDate>Wed, 15 Jul 2020 19:17:17 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/prometheus-service-discovery-openstack/</guid>
            <description>In a previous post I mentioned that Prometheus has a number of ways to discover services. This ranges from statically defined configuration to consuming APIs from different cloud providers. So far, we have covered: -
 AWS, Azure and Google Cloud Platform Digital Ocean  In this post we will cover discovering services running on OpenStack.
What is OpenStack? OpenStack is a way of running your own Infrastructure-as-a-service platform. It enables you to deploy compute, storage, networking and other resources similar to the major cloud providers.</description>
            <content type="html"><![CDATA[<p>In a previous <a href="/posts/prometheus-service-discovery-aws-gcp-azure/">post</a> I mentioned that Prometheus has a number of ways to discover services. This ranges from statically defined configuration to consuming APIs from different cloud providers. So far, we have covered: -</p>
<ul>
<li><a href="/posts/prometheus-service-discovery-aws-gcp-azure/">AWS, Azure and Google Cloud Platform</a></li>
<li><a href="/posts/prometheus-service-discovery-digitalocean/">Digital Ocean</a></li>
</ul>
<p>In this post we will cover discovering services running on OpenStack.</p>
<h2 id="what-is-openstack">What is OpenStack?</h2>
<p>OpenStack is a way of running your own Infrastructure-as-a-service platform. It enables you to deploy compute, storage, networking and other resources similar to the major cloud providers. It began as a joint venture between <a href="https://www.rackspace.com/">Rackspace</a> and <a href="https://www.nasa.gov/">NASA</a> to provide an Open Source cloud solution.</p>
<p>Initially, OpenStack was embraced by a number of different sectors of the IT industry. Some companies saw it as a way to become an alternative to the likes of AWS or Azure (e.g. <a href="https://en.wikipedia.org/wiki/HPE_Helion">HPE Helion</a>, <a href="https://www.ovh.co.uk/">OVH</a>, <a href="https://carrier.huawei.com/en/products/IT/cloud-computing">Huawei</a>). For others, it enabled them to use their existing hardware while being able to spin up resources in a cloud-like way.</p>
<p>In recent years, OpenStack has become increasingly popular with telecommunications providers. Many of these providers own or have access to telecoms exchanges, and have large data centre footprints spanning cities, countries and even continents. OpenStack allows them to create multi-region clouds of their own, without the need to leverage a public cloud provider.</p>
<h2 id="the-lab">The lab</h2>
<p>OpenStack usually requires a fleet of high-powered servers managed as a cluster (or multiple clusters) of compute, storage and other services. If you want to test out OpenStack on a single machine, there are multiple options available: -</p>
<ul>
<li><a href="https://opendev.org/openstack/devstack">DevStack</a> - Installs OpenStack on a virtual machine (or physical machine) running Ubuntu or Fedora and turns it into a mini OpenStack cloud</li>
<li><a href="https://microstack.run/">MicroStack</a> - A Canonical project that deploys a OpenStack inside of a Snap package</li>
<li><a href="https://github.com/openstack/kolla-ansible">Kolla-Ansible</a> - An Ansible project that deploys all the OpenStack services as containers</li>
</ul>
<p>I decided to use MicroStack on an Ubuntu 18.04 LTS virtual machine.</p>
<h3 id="microstack">MicroStack</h3>
<p>To install and run MicroStack, run the following commands: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># Install MicroStack with Classic confinement (i.e. not fully sandboxed)</span>
$ sudo snap install microstack --classic --beta

<span style="color:#75715e"># Initialize MicroStack</span>
$ sudo microstack.init --auto
</code></pre></div><p>After a few minutes (ranging from 5 to 20 minutes, depending on your machine) MicroStack will have finished initializing. The web interface will be available at <code>http://10.20.20.1</code>: -</p>
<p><img src="/img/prometheus/microstack-login.png" alt="MicroStack Login Page"></p>
<p>The credentials for this page are: -</p>
<ul>
<li>Username: <code>admin</code></li>
<li>Password: <code>keystone</code></li>
</ul>
<p><img src="/img/prometheus/microstack-dashboard.png" alt="MicroStack Dashboard"></p>
<p>You can create a virtual machine (<strong>instance</strong>) from here, or from the command line.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># Launch the instance</span>
$ microstack.launch cirros --name test

Server test launched! <span style="color:#f92672">(</span>status is BUILD<span style="color:#f92672">)</span>

Access it with <span style="color:#e6db74">`</span>ssh -i $HOME/.ssh/id_microstack<span style="color:#e6db74">`</span> &lt;username&gt;@10.20.20.143

<span style="color:#75715e"># SSH to it, with the MicroStack generated SSH key</span>
$ ssh -i ~/.ssh/id_microstack cirros@10.20.20.143

<span style="color:#75715e"># Check that this machine is running the buildroot OS (which is what CirrOS uses)</span>
$ cat /etc/os-release
NAME<span style="color:#f92672">=</span>Buildroot
VERSION<span style="color:#f92672">=</span>2015.05-g31af4e3-dirty
ID<span style="color:#f92672">=</span>buildroot
VERSION_ID<span style="color:#f92672">=</span>2015.05
PRETTY_NAME<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Buildroot 2015.05&#34;</span>
</code></pre></div><p>Notice that all the commands are prefixed with <code>microstack.</code>. This is because OpenStack runs inside a Snap package.</p>
<h3 id="adding-additional-images">Adding additional images</h3>
<p>By default, MicroStack only includes the CirrOS image. While it is useful for testing that OpenStack can provision resources, it does not provide the ability to install packages.</p>
<p>Many Linux distributions create OpenStack-ready images, a list of which can be found <a href="https://docs.openstack.org/image-guide/obtain-images.html">here</a>.</p>
<p>For this post, I used a Debian Buster image: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># Download the image</span>
$ wget https://cdimage.debian.org/cdimage/openstack/current/debian-10.4.3-20200610-openstack-amd64.qcow2

<span style="color:#75715e"># Add the image to OpenStack</span>
$ microstack.openstack image create <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>            --container-format bare <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>            --disk-format qcow2 <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>            --file debian-10.4.3-20200610-openstack-amd64.qcow2 <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>            debian-buster

<span style="color:#75715e"># Verify that the image is defined</span>
$ microstack.openstack image list
+--------------------------------------+---------------+--------+
| ID                                   | Name          | Status |
+--------------------------------------+---------------+--------+
| eb4ec0e3-4ca4-4f4e-8b2a-58a5c4a21bbc | cirros        | active |
| 8559ed3c-a9ba-43a0-8508-a637be6dedce | debian-buster | active |
+--------------------------------------+---------------+--------+
</code></pre></div><p>This image (and many others) allow the use of <code>cloud-init</code> and <code>cloud-config</code>, allowing you to specify first-time boot configuration and package installation.</p>
<h2 id="terraform">Terraform</h2>
<p>Terraform has an official <a href="https://www.terraform.io/docs/providers/openstack/index.html">OpenStack Provider</a> that allows you to manage the resources on OpenStack. It can create everything from compute instances, object storage, containers and more.</p>
<p>For information on how to install Terraform and an example project structure, see <a href="/posts/prometheus-service-discovery-aws-gcp-azure/#configure-terraform">here</a>.</p>
<h3 id="create-application-credentials">Create application credentials</h3>
<p>You can use OpenStack user credentials (even the <code>admin</code> account) with Terraform if you choose. However, using <strong>application credentials</strong> allows you to restrict what access Terraform has, and can be revoked and/or rotated if the credentials are exposed in some way.</p>
<p>To generate an application credential, you can either use the CLI or the web interface.</p>
<p><strong>CLI</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ microstack.openstack application credential create --role admin terraform_prov
+--------------+----------------------------------------------------------------------------------------+
| Field        | Value                                                                                  |
+--------------+----------------------------------------------------------------------------------------+
| description  | None                                                                                   |
| expires_at   | None                                                                                   |
| id           | 13d10c3fab064589b375a8ef5373c7c4                                                       |
| name         | terraform_prov                                                                         |
| project_id   | d29ad3cbb72c41be867c95561fb6224d                                                       |
| roles        | admin                                                                                  |
| secret       | <span style="color:#75715e">###SECRET###                                                                           |</span>
| system       | None                                                                                   |
| unrestricted | False                                                                                  |
| user_id      | 2ef25a15d1ae4d9b9f50e740da61ba6f                                                       |
+--------------+----------------------------------------------------------------------------------------+
</code></pre></div><p><strong>Web UI</strong><br>
Go to <strong>Identity</strong> &gt; <strong>Application Credentials</strong> and then click <strong>Create Application Credential</strong>
<img src="/img/prometheus/microstack-app-credential.png" alt="MicroStack Create Application Credential"></p>
<p>You can assign the application credential a member, reader or admin role. As Terraform will be creating resources, the admin role is applicable.</p>
<p>A couple of points to note here are: -</p>
<ul>
<li>The <code>project_id</code> is the OpenStack project in which you create your resources (think of this like a tenant or your account in a public cloud)</li>
<li>The <code>user_id</code> is the ID of this application credential</li>
</ul>
<p>Either note down these credentials, or download them in a <code>clouds.yaml</code> file or an <strong>OpenRC</strong> file. The latter can be sourced in your <code>.bashrc</code>, <code>.zshrc</code> or the <strong>rc</strong> file for the shell of your choice.</p>
<h3 id="provider-configuration">Provider Configuration</h3>
<p>In my Terraform directory, I have create a directory called <code>openstack</code>. In this directory, the provider configuration looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;openstack&#34;</span> {
  user_name                     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;admin&#34;</span>
  auth_url                      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;http://10.20.20.1:5000/v3&#34;</span>
  region                        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;microstack&#34;</span>
  user_id                       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;2ef25a15d1ae4d9b9f50e740da61ba6f&#34;</span>
  application_credential_id     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;13d10c3fab064589b375a8ef5373c7c4&#34;</span>
  application_credential_secret <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;### SECRET ###&#34;</span>
  tenant_id                     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;d29ad3cbb72c41be867c95561fb6224d&#34;</span>
}
</code></pre></div><p>The <code>user_name</code> is <code>admin</code> because the Application Credentials were created within the <code>admin</code> account. If they were created for another OpenStack user, you would change this to the username for that account.</p>
<p>The <code>auth_url</code> uses the <strong>v3</strong> Keystone API endpoint (which will be <code>http://10.20.20.1:5000/v3</code> for any default Microstack install).</p>
<p>The <code>region</code> is <code>microstack</code>. This can be verified with the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ microstack.openstack region list
+------------+---------------+-------------+
| Region     | Parent Region | Description |
+------------+---------------+-------------+
| microstack | None          |             |
+------------+---------------+-------------+
</code></pre></div><p>The <code>user_id</code>, <code>application_credential_id</code> and <code>application_credential_secret</code> are from the credential we created in the previous stage.</p>
<p>The <code>tenant_id</code> is the same as the <code>project_id</code>.</p>
<p>If you have sourced the credentials using the <strong>OpenRC</strong> file in the previous stage, you can remove all of the fields in the provider. Terraform will use environment variables instead. You would then have something like: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;openstack&#34;</span> {
}
</code></pre></div><p>After this, run <code>terraform init</code> to download the OpenStack provider, ready to start creating resource in OpenStack: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ terraform init 
Initializing the backend...

Initializing provider plugins...
- Checking <span style="color:#66d9ef">for</span> available provider plugins...
- Downloading plugin <span style="color:#66d9ef">for</span> provider <span style="color:#e6db74">&#34;openstack&#34;</span> <span style="color:#f92672">(</span>terraform-providers/openstack<span style="color:#f92672">)</span> 1.29.0...

The following providers <span style="color:#66d9ef">do</span> not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;...&#34;</span> constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* provider.openstack: version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;~&gt; 1.29&#34;</span>

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running <span style="color:#e6db74">&#34;terraform plan&#34;</span> to see
any changes that are required <span style="color:#66d9ef">for</span> your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration <span style="color:#66d9ef">for</span> Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to <span style="color:#66d9ef">do</span> so <span style="color:#66d9ef">if</span> necessary.
</code></pre></div><h3 id="define-the-infrastructure---instances">Define the infrastructure - Instances</h3>
<p>You can now create the configuration files for your first compute instance. The below is from the file <code>instance.tf</code> in the <code>terraform/openstack</code> directory: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;openstack_images_image_v2&#34; &#34;debian-buster&#34;</span> {
  name        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;debian-buster&#34;</span>
  most_recent <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;openstack_compute_flavor_v2&#34; &#34;m1-micro&#34;</span> {
  name  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;m1.micro&#34;</span>
  ram   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;512&#34;</span>
  vcpus <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;1&#34;</span>
  disk  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;5&#34;</span>
}


<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;openstack_compute_keypair_v2&#34; &#34;symphonyx&#34;</span> {
    name       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;symphonyx&#34;</span>
    public_key <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;### SSH PUBLIC KEY ###&#34;</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;openstack_compute_instance_v2&#34; &#34;debian-buster&#34;</span> {
  name            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;debian-buster&#34;</span>
  image_id        <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">openstack_images_image_v2</span>.<span style="color:#66d9ef">debian</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">buster</span>.<span style="color:#66d9ef">id</span>
  flavor_id       <span style="color:#f92672">=</span> <span style="color:#66d9ef">openstack_compute_flavor_v2</span>.<span style="color:#66d9ef">m1</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">micro</span>.<span style="color:#66d9ef">id</span>
  key_pair        <span style="color:#f92672">=</span> <span style="color:#66d9ef">openstack_compute_keypair_v2</span>.<span style="color:#66d9ef">symphonyx</span>.<span style="color:#66d9ef">name</span>
  security_groups <span style="color:#f92672">=</span> [
    <span style="color:#66d9ef">openstack_networking_secgroup_v2</span>.<span style="color:#66d9ef">buster</span>.<span style="color:#66d9ef">name</span>
  ]
  user_data       <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">template_file</span>.<span style="color:#66d9ef">debian</span>.<span style="color:#66d9ef">template</span>

  metadata <span style="color:#f92672">=</span> {
    prometheus <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
    node_exporter <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
  }

  <span style="color:#66d9ef">network</span> {
    name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;test&#34;</span>
  }
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;openstack_networking_floatingip_v2&#34; &#34;debian-buster-fip&#34;</span> {
    pool <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;external&#34;</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;openstack_compute_floatingip_associate_v2&#34; &#34;debian-buster-fip&#34;</span> {
    floating_ip <span style="color:#f92672">=</span> <span style="color:#66d9ef">openstack_networking_floatingip_v2</span>.<span style="color:#66d9ef">debian</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">buster</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">fip</span>.<span style="color:#66d9ef">address</span>
    instance_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">openstack_compute_instance_v2</span>.<span style="color:#66d9ef">debian</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">buster</span>.<span style="color:#66d9ef">id</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;openstack_networking_secgroup_v2&#34; &#34;buster&#34;</span> {
    name        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;buster&#34;</span>
    description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Buster Security Group&#34;</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;openstack_networking_secgroup_rule_v2&#34; &#34;node_exporter&#34;</span> {
  direction         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ingress&#34;</span>
  ethertype         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;IPv4&#34;</span>
  protocol          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
  port_range_min    <span style="color:#f92672">=</span> <span style="color:#ae81ff">9100</span>
  port_range_max    <span style="color:#f92672">=</span> <span style="color:#ae81ff">9100</span>
  remote_ip_prefix  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>
  security_group_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">openstack_networking_secgroup_v2</span>.<span style="color:#66d9ef">buster</span>.<span style="color:#66d9ef">id</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;openstack_networking_secgroup_rule_v2&#34; &#34;ssh&#34;</span> {
  direction         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ingress&#34;</span>
  ethertype         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;IPv4&#34;</span>
  protocol          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
  port_range_min    <span style="color:#f92672">=</span> <span style="color:#ae81ff">22</span>
  port_range_max    <span style="color:#f92672">=</span> <span style="color:#ae81ff">22</span>
  remote_ip_prefix  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>
  security_group_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">openstack_networking_secgroup_v2</span>.<span style="color:#66d9ef">buster</span>.<span style="color:#66d9ef">id</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;openstack_networking_secgroup_rule_v2&#34; &#34;icmp_v4&#34;</span> {
  direction         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ingress&#34;</span>
  ethertype         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;IPv4&#34;</span>
  protocol          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;icmp&#34;</span>
  remote_ip_prefix  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>
  security_group_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">openstack_networking_secgroup_v2</span>.<span style="color:#66d9ef">buster</span>.<span style="color:#66d9ef">id</span>
}
</code></pre></div><p>To summarize what we are doing here, we: -</p>
<ul>
<li>Use a data source to match the Debian Buster OpenStack image we defined earlier</li>
<li>Define an OpenStack flavour
<ul>
<li>Flavours are the &ldquo;size&rdquo; of an instance (CPU, RAM and Disk)</li>
<li>You may not need to do this, depending on your instance size requirements</li>
</ul>
</li>
<li>Add our SSH key so that we can manage the instance</li>
<li>Create an OpenStack instance called <code>debian-buster</code>, using the flavour, image and keypair previously defined. We also: -
<ul>
<li>Associate a security group (i.e. a firewall)</li>
<li>OpenStack supports <code>user_data</code>, so we supply a <code>cloud-config</code> template to the instance</li>
<li>Add metadata (i.e. tags) for <code>prometheus</code> and <code>node_exporter</code></li>
<li>Associate the network &ldquo;test&rdquo; (which is predefined in MicroStack)</li>
</ul>
</li>
<li>Create a floating IP and associate it with this instance
<ul>
<li>The instance can be destroyed and recreated, but retain the same floating IP (similar to Elastic IPs in AWS)</li>
</ul>
</li>
<li>Create the security group, and add rules for inbound SSH, ICMP (i.e. ping) and also allow the <code>node_exporter</code> port (<code>TCP9100</code>)</li>
</ul>
<p>The <code>user_data</code> is defined using the same approach as <a href="https://yetiops.net/posts/prometheus-service-discovery-aws-gcp-azure/#define-the-infrastructure---ec2s">here</a>, but with the name changed to <code>debian</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;template_file&#34; &#34;debian&#34;</span> {
  template <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${file(&#34;${path.module}/files/debian.tpl&#34;)}&#34;</span>
}

<span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;template_cloudinit_config&#34; &#34;debian&#34;</span> {
  gzip          <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>
  base64_encode <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>

  <span style="color:#66d9ef">part</span> {
    filename     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;init.cfg&#34;</span>
    content_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;text/cloud-config&#34;</span>
    content      <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">template_file</span>.<span style="color:#66d9ef">debian</span>.<span style="color:#66d9ef">rendered</span>
  }
}
</code></pre></div><p>The contents of the template are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#cloud-config</span>
packages:
 - prometheus-node-exporter
</code></pre></div><h3 id="build-the-infrastructure---instance">Build the infrastructure - Instance</h3>
<p>We can now apply our configuration, and see if it builds an OpenStack instance: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ terraform apply
data.openstack_images_image_v2.debian-buster: Refreshing state...
data.template_file.debian: Refreshing state...
data.template_cloudinit_config.debian: Refreshing state...

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  <span style="color:#75715e"># openstack_compute_flavor_v2.m1-micro will be created</span>
  + resource <span style="color:#e6db74">&#34;openstack_compute_flavor_v2&#34;</span> <span style="color:#e6db74">&#34;m1-micro&#34;</span> <span style="color:#f92672">{</span>
      + disk         <span style="color:#f92672">=</span> <span style="color:#ae81ff">5</span>
      + extra_specs  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + name         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;m1.micro&#34;</span>
      + ram          <span style="color:#f92672">=</span> <span style="color:#ae81ff">512</span>
      + region       <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + rx_tx_factor <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>
      + vcpus        <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># openstack_compute_floatingip_associate_v2.debian-buster-fip will be created</span>
  + resource <span style="color:#e6db74">&#34;openstack_compute_floatingip_associate_v2&#34;</span> <span style="color:#e6db74">&#34;debian-buster-fip&#34;</span> <span style="color:#f92672">{</span>
      + floating_ip <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + instance_id <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + region      <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># openstack_compute_instance_v2.debian-buster will be created</span>
  + resource <span style="color:#e6db74">&#34;openstack_compute_instance_v2&#34;</span> <span style="color:#e6db74">&#34;debian-buster&#34;</span> <span style="color:#f92672">{</span>
      + access_ip_v4        <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + access_ip_v6        <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + all_metadata        <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + all_tags            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + availability_zone   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + flavor_id           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + flavor_name         <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + force_delete        <span style="color:#f92672">=</span> false
      + id                  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + image_id            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;8559ed3c-a9ba-43a0-8508-a637be6dedce&#34;</span>
      + image_name          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + key_pair            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;symphonyx&#34;</span>
      + metadata            <span style="color:#f92672">=</span> <span style="color:#f92672">{</span>
          + <span style="color:#e6db74">&#34;node_exporter&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
          + <span style="color:#e6db74">&#34;prometheus&#34;</span>    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
        <span style="color:#f92672">}</span>
      + name                <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;debian-buster&#34;</span>
      + power_state         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;active&#34;</span>
      + region              <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + security_groups     <span style="color:#f92672">=</span> <span style="color:#f92672">[</span>
          + <span style="color:#e6db74">&#34;buster&#34;</span>,
        <span style="color:#f92672">]</span>
      + stop_before_destroy <span style="color:#f92672">=</span> false
      + user_data           <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;2169d8a3e100623d34bf1a7b2f6bd924a8997bfb&#34;</span>

      + network <span style="color:#f92672">{</span>
          + access_network <span style="color:#f92672">=</span> false
          + fixed_ip_v4    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + fixed_ip_v6    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + floating_ip    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + mac            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + name           <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;test&#34;</span>
          + port           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + uuid           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
        <span style="color:#f92672">}</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># openstack_compute_keypair_v2.symphonyx will be created</span>
  + resource <span style="color:#e6db74">&#34;openstack_compute_keypair_v2&#34;</span> <span style="color:#e6db74">&#34;symphonyx&#34;</span> <span style="color:#f92672">{</span>
      + fingerprint <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + name        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;symphonyx&#34;</span>
      + private_key <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + public_key  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;### SSH PUBLIC KEY ###&#34;</span>
      + region      <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># openstack_networking_floatingip_v2.debian-buster-fip will be created</span>
  + resource <span style="color:#e6db74">&#34;openstack_networking_floatingip_v2&#34;</span> <span style="color:#e6db74">&#34;debian-buster-fip&#34;</span> <span style="color:#f92672">{</span>
      + address    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + all_tags   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + dns_domain <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + dns_name   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + fixed_ip   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id         <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + pool       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;external&#34;</span>
      + port_id    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + region     <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + tenant_id  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># openstack_networking_secgroup_rule_v2.icmp_v4 will be created</span>
  + resource <span style="color:#e6db74">&#34;openstack_networking_secgroup_rule_v2&#34;</span> <span style="color:#e6db74">&#34;icmp_v4&#34;</span> <span style="color:#f92672">{</span>
      + direction         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ingress&#34;</span>
      + ethertype         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;IPv4&#34;</span>
      + id                <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + port_range_max    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + port_range_min    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + protocol          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;icmp&#34;</span>
      + region            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + remote_group_id   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + remote_ip_prefix  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>
      + security_group_id <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + tenant_id         <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># openstack_networking_secgroup_rule_v2.node_exporter will be created</span>
  + resource <span style="color:#e6db74">&#34;openstack_networking_secgroup_rule_v2&#34;</span> <span style="color:#e6db74">&#34;node_exporter&#34;</span> <span style="color:#f92672">{</span>
      + direction         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ingress&#34;</span>
      + ethertype         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;IPv4&#34;</span>
      + id                <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + port_range_max    <span style="color:#f92672">=</span> <span style="color:#ae81ff">9100</span>
      + port_range_min    <span style="color:#f92672">=</span> <span style="color:#ae81ff">9100</span>
      + protocol          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
      + region            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + remote_group_id   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + remote_ip_prefix  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>
      + security_group_id <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + tenant_id         <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># openstack_networking_secgroup_rule_v2.ssh will be created</span>
  + resource <span style="color:#e6db74">&#34;openstack_networking_secgroup_rule_v2&#34;</span> <span style="color:#e6db74">&#34;ssh&#34;</span> <span style="color:#f92672">{</span>
      + direction         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ingress&#34;</span>
      + ethertype         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;IPv4&#34;</span>
      + id                <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + port_range_max    <span style="color:#f92672">=</span> <span style="color:#ae81ff">22</span>
      + port_range_min    <span style="color:#f92672">=</span> <span style="color:#ae81ff">22</span>
      + protocol          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
      + region            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + remote_group_id   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + remote_ip_prefix  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>
      + security_group_id <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + tenant_id         <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># openstack_networking_secgroup_v2.buster will be created</span>
  + resource <span style="color:#e6db74">&#34;openstack_networking_secgroup_v2&#34;</span> <span style="color:#e6db74">&#34;buster&#34;</span> <span style="color:#f92672">{</span>
      + all_tags    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Buster Security Group&#34;</span>
      + id          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + name        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;buster&#34;</span>
      + region      <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + tenant_id   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
    <span style="color:#f92672">}</span>

Plan: <span style="color:#ae81ff">9</span> to add, <span style="color:#ae81ff">0</span> to change, <span style="color:#ae81ff">0</span> to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only <span style="color:#e6db74">&#39;yes&#39;</span> will be accepted to approve.

  Enter a value: yes

openstack_compute_flavor_v2.m1-micro: Creating...
openstack_compute_keypair_v2.symphonyx: Creating...
openstack_networking_secgroup_v2.buster: Creating...
openstack_networking_floatingip_v2.debian-buster-fip: Creating...
openstack_compute_keypair_v2.symphonyx: Creation complete after 1s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>symphonyx<span style="color:#f92672">]</span>
openstack_networking_secgroup_v2.buster: Creation complete after 1s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>b498273c-53f7-4489-9bf7-83da3de24802<span style="color:#f92672">]</span>
openstack_networking_secgroup_rule_v2.node_exporter: Creating...
openstack_networking_secgroup_rule_v2.icmp_v4: Creating...
openstack_networking_secgroup_rule_v2.ssh: Creating...
openstack_networking_secgroup_rule_v2.icmp_v4: Creation complete after 0s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>13705ec0-fb8b-4d91-a7e1-912b1bd8b2e3<span style="color:#f92672">]</span>
openstack_networking_secgroup_rule_v2.node_exporter: Creation complete after 0s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>f46cafb7-001c-4543-9476-e32e56c3f0d2<span style="color:#f92672">]</span>
openstack_networking_secgroup_rule_v2.ssh: Creation complete after 0s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>4ac2c918-90ba-4cd5-82d7-ecccc96ee634<span style="color:#f92672">]</span>
openstack_compute_flavor_v2.m1-micro: Creation complete after 1s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>bd9cefe4-05b7-44b2-bc26-8c74f7ea708d<span style="color:#f92672">]</span>
openstack_compute_instance_v2.debian-buster: Creating...
openstack_networking_floatingip_v2.debian-buster-fip: Creation complete after 7s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>6943a249-25a7-46eb-b729-c67330c8eca6<span style="color:#f92672">]</span>
openstack_compute_instance_v2.debian-buster: Still creating... <span style="color:#f92672">[</span>10s elapsed<span style="color:#f92672">]</span>
openstack_compute_instance_v2.debian-buster: Creation complete after 12s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>aaa3aa7d-6d21-4aba-a872-1722604f4954<span style="color:#f92672">]</span>
openstack_compute_floatingip_associate_v2.debian-buster-fip: Creating...
openstack_compute_floatingip_associate_v2.debian-buster-fip: Creation complete after 2s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>10.20.20.156/aaa3aa7d-6d21-4aba-a872-1722604f4954/<span style="color:#f92672">]</span>

Apply complete! Resources: <span style="color:#ae81ff">9</span> added, <span style="color:#ae81ff">0</span> changed, <span style="color:#ae81ff">0</span> destroyed.
</code></pre></div><p>We can double check that Terraform is managing these resources now with <code>terraform state list</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ terraform state list
data.openstack_images_image_v2.debian-buster
data.template_cloudinit_config.debian
data.template_file.debian
openstack_compute_flavor_v2.m1-micro
openstack_compute_floatingip_associate_v2.debian-buster-fip
openstack_compute_instance_v2.debian-buster
openstack_compute_keypair_v2.symphonyx
openstack_networking_floatingip_v2.debian-buster-fip
openstack_networking_secgroup_rule_v2.icmp_v4
openstack_networking_secgroup_rule_v2.node_exporter
openstack_networking_secgroup_rule_v2.ssh
openstack_networking_secgroup_v2.buster
</code></pre></div><p>We can check to see if the instance is in the OpenStack Console: -</p>
<p><img src="/img/prometheus/microstack-instance.png" alt="MicroStack Instance in Console"></p>
<p>Now lets try SSH: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ssh debian@10.20.20.156
Linux debian-buster 4.19.0-9-cloud-amd64 <span style="color:#75715e">#1 SMP Debian 4.19.118-2+deb10u1 (2020-06-07) x86_64</span>

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms <span style="color:#66d9ef">for</span> each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.

$ ps aux | grep -i node
prometh+  <span style="color:#ae81ff">2206</span>  0.5  3.7 <span style="color:#ae81ff">558272</span> <span style="color:#ae81ff">18876</span> ?        Ssl  18:26   0:02 /usr/bin/prometheus-node-exporter
debian    <span style="color:#ae81ff">2581</span>  0.0  0.1   <span style="color:#ae81ff">6144</span>   <span style="color:#ae81ff">884</span> pts/0    S+   18:35   0:00 grep -i node
</code></pre></div><h2 id="prometheus">Prometheus</h2>
<p>Now that we have our Instance created in OpenStack, we can configure Prometheus to discover it.</p>
<h3 id="create-an-application-credential-for-prometheus">Create an application credential for Prometheus</h3>
<p>Prometheus requires an application credential to discover instances in OpenStack (see <a href="/posts/prometheus-service-discovery-openstack/#create-application-credentials">here</a>). When creating the credential, assign it the <code>reader</code> role. The Prometheus user does not need to make any changes within OpenStack, only discover from it.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ microstack.openstack application credential create --role reader prometheus_sd 
</code></pre></div><h3 id="openstack-instance-service-discovery">OpenStack Instance Service Discovery</h3>
<p>To allow Prometheus to discover instances in OpenStack, use configuration like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  - <span style="color:#66d9ef">job_name</span>: <span style="color:#e6db74">&#39;openstack&#39;</span>
    <span style="color:#66d9ef">openstack_sd_configs</span>:
      - <span style="color:#66d9ef">role</span>: <span style="color:#e6db74">&#39;instance&#39;</span>
        <span style="color:#66d9ef">region</span>: <span style="color:#e6db74">&#39;microstack&#39;</span>
        <span style="color:#66d9ef">identity_endpoint</span>: <span style="color:#e6db74">&#39;http://10.20.20.1:5000/v3/&#39;</span>
        <span style="color:#66d9ef">domain_name</span>: <span style="color:#e6db74">&#39;default&#39;</span>
        <span style="color:#66d9ef">userid</span>: <span style="color:#e6db74">&#39;2ef25a15d1ae4d9b9f50e740da61ba6f&#39;</span>
        <span style="color:#66d9ef">application_credential_id</span>: <span style="color:#e6db74">&#39;3f48628678514e909ad3c01d8f9ccf90&#39;</span>
        <span style="color:#66d9ef">application_credential_secret</span>: <span style="color:#e6db74">&#39;### APPLICATION CREDENTIAL SECRET ###&#39;</span>
    <span style="color:#66d9ef">relabel_configs</span>:
      - <span style="color:#66d9ef">source_labels</span>: [__meta_openstack_public_ip]
        <span style="color:#66d9ef">target_label</span>: __address__
        <span style="color:#66d9ef">replacement</span>: <span style="color:#e6db74">&#39;$1:9100&#39;</span>
      - <span style="color:#66d9ef">source_labels</span>: [__meta_openstack_tag_prometheus]
        <span style="color:#66d9ef">regex</span>: <span style="color:#66d9ef">true</span>.*
        <span style="color:#66d9ef">action</span>: keep
      - <span style="color:#66d9ef">source_labels</span>: [__meta_openstack_tag_node_exporter]
        <span style="color:#66d9ef">regex</span>: <span style="color:#66d9ef">true</span>.*
        <span style="color:#66d9ef">action</span>: keep
      - <span style="color:#66d9ef">action</span>: labelmap
        <span style="color:#66d9ef">regex</span>: __meta_openstack_(.+)
</code></pre></div><p>Some points to note here are: -</p>
<ul>
<li>You can choose between the <code>hypervisor</code> or <code>instance</code> role
<ul>
<li><code>hypervisor</code> discovers OpenStack hosts - useful if you have multiple hosts using the same Keystone identity endpoint</li>
<li><code>instance</code> discovers the instances running on the Openstack hosts</li>
<li>The <code>domain_name</code> may differ - <code>default</code> is the fallback in case you have no others configured</li>
</ul>
</li>
<li>The <code>public_ip</code> used in the <code>relabel_configs</code> section is equivalent to the Floating IP created by Terraform</li>
</ul>
<p>The other values are explained in the <a href="/posts/prometheus-service-discovery-openstack/#provider-configuration">Terraform Provider</a> section.</p>
<p>The tag relabelling configuration is similar to how we match tags in the <a href="/posts/prometheus-service-discovery-aws-gcp-azure/">AWS, Azure and Google Cloud Platform</a> post.</p>
<p>We also use something called a <code>label_map</code>. Any metadata discovered that matches the regular expression <code>__meta_openstack_(.+)</code> will be added as a label to all metrics received from discovered hosts. If we look at the metadata discovered for the instance, this will make a lot more sense: -</p>
<p><img src="/img/prometheus/microstack-metadata.png" alt="MicroStack Instance metadata"></p>
<p>Every discovered label in the above that has the prefix <code>__meta_openstack_</code> will now be added to the instance and all metrics: -</p>
<p><img src="/img/prometheus/microstack-target.png" alt="MicroStack Instance target with labels"></p>
<p>As we can see, we have labels of <code>instance_name</code>, <code>private_ip</code>, <code>instance_status</code> and more. Without the <code>labelmap</code>, we would only see the <code>instance</code> and the <code>job</code>.</p>
<h3 id="openstack-hypervisor-service-discovery">OpenStack Hypervisor Service Discovery</h3>
<p>If we want to monitor the hypervisor as well, we can use the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  - <span style="color:#66d9ef">job_name</span>: <span style="color:#e6db74">&#39;openstack_hypervisors&#39;</span>
    <span style="color:#66d9ef">openstack_sd_configs</span>:
      - <span style="color:#66d9ef">role</span>: <span style="color:#e6db74">&#39;hypervisor&#39;</span>
        <span style="color:#66d9ef">region</span>: <span style="color:#e6db74">&#39;microstack&#39;</span>
        <span style="color:#66d9ef">identity_endpoint</span>: <span style="color:#e6db74">&#39;http://10.20.20.1:5000/v3/&#39;</span>
        <span style="color:#66d9ef">domain_name</span>: <span style="color:#e6db74">&#39;default&#39;</span>
        <span style="color:#66d9ef">userid</span>: <span style="color:#e6db74">&#39;2ef25a15d1ae4d9b9f50e740da61ba6f&#39;</span>
        <span style="color:#66d9ef">application_credential_id</span>: <span style="color:#e6db74">&#39;3f48628678514e909ad3c01d8f9ccf90&#39;</span>
        <span style="color:#66d9ef">application_credential_secret</span>: <span style="color:#e6db74">&#39;### APPLICATION CREDENTIAL SECRET ###&#39;</span>
        <span style="color:#66d9ef">port</span>: <span style="color:#e6db74">&#39;9100&#39;</span>
    <span style="color:#66d9ef">relabel_configs</span>:
      - <span style="color:#66d9ef">action</span>: labelmap
        <span style="color:#66d9ef">regex</span>: __meta_openstack_(.+)
</code></pre></div><p>One point to note is that the <code>hypervisor</code> role requires higher privileges than the <code>instance</code> role. You will need to create another application credential, this time with <code>admin</code> privileges.</p>
<p>At this point, you could use only the Prometheus Node Exporter to monitor the hypervisor. If you need to gather OpenStack-specific metrics, then use one of the following projects: -</p>
<ul>
<li><a href="https://github.com/openstack-exporter/openstack-exporter">openstack-exporter</a></li>
<li><a href="https://github.com/CanonicalLtd/prometheus-openstack-exporter">prometheus-openstack-exporter</a> from Canonical</li>
</ul>
<p>The <code>prometheus-openstack-exporter</code> project requires multiple Python dependencies to install it natively on the hypervisor. There is also the option of deploying it as a Docker container</p>
<p>The <code>openstack-exporter</code> project is provided as a single Go binary.</p>
<h3 id="openstack-exporter">OpenStack Exporter</h3>
<p>Download the binary from the <a href="https://github.com/openstack-exporter/openstack-exporter">openstack-exporter</a> page, extract the contents, and place the <code>openstack-exporter</code> binary in <code>/usr/local/bin</code>.</p>
<p>The <code>openstack-exporter</code> requires the use of a <code>clouds.yaml</code> file. When you create an application credential, you are given the option to download the YAML file: -</p>
<p><img src="/img/prometheus/microstack-cloudsyaml.png" alt="MicroStack clouds.yaml download"></p>
<p>Create a <code>prometheus</code> user, and also create a <code>prometheus</code> directory in the <code>/etc</code> folder of your OpenStack host: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e"># Create the user</span>
$ adduser prometheus

<span style="color:#75715e"># Create the Prometheus directory</span>
$ mkdir -p /etc/prometheus

<span style="color:#75715e"># Make sure the directory is own by the prometheus user</span>
$ chown prometheus:prometheus /etc/prometheus
</code></pre></div><p>Place the <code>clouds.yaml</code> file in the <code>/etc/prometheus</code> directory. Once this is done, we can test the exporter: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo openstack-exporter --os-client-config /etc/prometheus/clouds.yaml openstack
INFO<span style="color:#f92672">[</span>0000<span style="color:#f92672">]</span> Starting openstack exporter version <span style="color:#f92672">(</span>version<span style="color:#f92672">=</span>1.1.0, branch<span style="color:#f92672">=</span>HEAD, revision<span style="color:#f92672">=</span>f60affb67206e1ab66456b527f3ffc3f8e2a74c6<span style="color:#f92672">)</span> <span style="color:#66d9ef">for</span> cloud: openstack  source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;main.go:49&#34;</span>
INFO<span style="color:#f92672">[</span>0000<span style="color:#f92672">]</span> Build context <span style="color:#f92672">(</span>go<span style="color:#f92672">=</span>go1.13.12, user<span style="color:#f92672">=</span>root@15b7eb12a7c5, date<span style="color:#f92672">=</span>20200702-16:24:38<span style="color:#f92672">)</span>  source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;main.go:50&#34;</span>
ERRO<span style="color:#f92672">[</span>0000<span style="color:#f92672">]</span> enabling exporter <span style="color:#66d9ef">for</span> service volume failed: No suitable endpoint could be found in the service catalog.  source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;main.go:63&#34;</span>
ERRO<span style="color:#f92672">[</span>0000<span style="color:#f92672">]</span> enabling exporter <span style="color:#66d9ef">for</span> service object-store failed: No suitable endpoint could be found in the service catalog.  source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;main.go:63&#34;</span>
ERRO<span style="color:#f92672">[</span>0001<span style="color:#f92672">]</span> enabling exporter <span style="color:#66d9ef">for</span> service dns failed: No suitable endpoint could be found in the service catalog.  source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;main.go:63&#34;</span>
ERRO<span style="color:#f92672">[</span>0001<span style="color:#f92672">]</span> enabling exporter <span style="color:#66d9ef">for</span> service identity failed: No suitable endpoint could be found in the service catalog.  source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;main.go:63&#34;</span>
ERRO<span style="color:#f92672">[</span>0001<span style="color:#f92672">]</span> enabling exporter <span style="color:#66d9ef">for</span> service load-balancer failed: No suitable endpoint could be found in the service catalog.  source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;main.go:63&#34;</span>
ERRO<span style="color:#f92672">[</span>0002<span style="color:#f92672">]</span> enabling exporter <span style="color:#66d9ef">for</span> service container-infra failed: No suitable endpoint could be found in the service catalog.  source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;main.go:63&#34;</span>
INFO<span style="color:#f92672">[</span>0002<span style="color:#f92672">]</span> Adding metric: floating_ips to exporter: neutron  source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;exporter.go:165&#34;</span>
INFO<span style="color:#f92672">[</span>0002<span style="color:#f92672">]</span> Adding metric: floating_ips_associated_not_active to exporter: neutron  source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;exporter.go:165&#34;</span>
INFO<span style="color:#f92672">[</span>0002<span style="color:#f92672">]</span> Adding metric: networks to exporter: neutron  source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;exporter.go:165&#34;</span>
INFO<span style="color:#f92672">[</span>0002<span style="color:#f92672">]</span> Adding metric: security_groups to exporter: neutron  source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;exporter.go:165&#34;</span>
INFO<span style="color:#f92672">[</span>0002<span style="color:#f92672">]</span> Adding metric: subnets to exporter: neutron   source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;exporter.go:165&#34;</span>
INFO<span style="color:#f92672">[</span>0002<span style="color:#f92672">]</span> Adding metric: port to exporter: neutron      source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;exporter.go:165&#34;</span>
INFO<span style="color:#f92672">[</span>0002<span style="color:#f92672">]</span> Adding metric: ports to exporter: neutron     source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;exporter.go:165&#34;</span>
<span style="color:#f92672">[</span>...<span style="color:#f92672">]</span>
</code></pre></div><p>If you then want this to run when the hypervisor starts, define a SystemD unit file like the below: -</p>
<p><strong>/etc/systemd/system/openstack-exporter.service</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#f92672">[</span>Unit<span style="color:#f92672">]</span>
Description<span style="color:#f92672">=</span>OpenStack Exporter
After<span style="color:#f92672">=</span>network.target

<span style="color:#f92672">[</span>Service<span style="color:#f92672">]</span>
User<span style="color:#f92672">=</span>prometheus
Group<span style="color:#f92672">=</span>prometheus
Type<span style="color:#f92672">=</span>simple
ExecStart<span style="color:#f92672">=</span>/usr/local/bin/openstack-exporter --os-client-config /etc/prometheus/clouds.yaml openstack

<span style="color:#f92672">[</span>Install<span style="color:#f92672">]</span>
WantedBy<span style="color:#f92672">=</span>multi-user.target
</code></pre></div><p>Enable and start the service: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e"># Enable the service</span>
$ systemctl daemon-reload
$ systemctl enable openstack-exporter

<span style="color:#75715e"># Start the service</span>
$ systemctl start openstack-exporter
</code></pre></div><p>Update the Prometheus job configuration to use port <code>9180</code> (the <code>openstack-exporter</code> port) rather than <code>9100</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  - <span style="color:#66d9ef">job_name</span>: <span style="color:#e6db74">&#39;openstack_hypervisors&#39;</span>
    <span style="color:#66d9ef">openstack_sd_configs</span>:
      - <span style="color:#66d9ef">role</span>: <span style="color:#e6db74">&#39;hypervisor&#39;</span>
        <span style="color:#66d9ef">region</span>: <span style="color:#e6db74">&#39;microstack&#39;</span>
        <span style="color:#66d9ef">identity_endpoint</span>: <span style="color:#e6db74">&#39;http://10.20.20.1:5000/v3/&#39;</span>
        <span style="color:#66d9ef">domain_name</span>: <span style="color:#e6db74">&#39;default&#39;</span>
        <span style="color:#66d9ef">userid</span>: <span style="color:#e6db74">&#39;2ef25a15d1ae4d9b9f50e740da61ba6f&#39;</span>
        <span style="color:#66d9ef">application_credential_id</span>: <span style="color:#e6db74">&#39;3f48628678514e909ad3c01d8f9ccf90&#39;</span>
        <span style="color:#66d9ef">application_credential_secret</span>: <span style="color:#e6db74">&#39;### APPLICATION CREDENTIAL SECRET ###&#39;</span>
        <span style="color:#66d9ef">port</span>: <span style="color:#e6db74">&#39;9180&#39;</span>
    <span style="color:#66d9ef">relabel_configs</span>:
      - <span style="color:#66d9ef">action</span>: labelmap
        <span style="color:#66d9ef">regex</span>: __meta_openstack_(.+)
</code></pre></div><p>We should now be able to see metadata for the discovered hypervisor (in this case the MicroStack instance) and also see that the <code>openstack-exporter</code> is up: -</p>
<p><img src="/img/prometheus/microstack-hv-metadata.png" alt="Microstack Hypervisor Metadata"></p>
<p><img src="/img/prometheus/microstack-hv-target.png" alt="Microstack Hypervisor Target"></p>
<p>Looking good!</p>
<h2 id="grafana">Grafana</h2>
<p>Now that we have discovered the instances, we can use any Node Exporter dashboard in Grafana to view their metrics. The <a href="https://grafana.com/grafana/dashboards/1860">Node Exporter Full</a> is always a good start when using the Node Exporter: -</p>
<p><img src="/img/prometheus/microstack-node-exporter-dash.png" alt="MicroStack Node Exporter Dashboard"></p>
<p>There is also a pre-existing <a href="https://grafana.com/grafana/dashboards/9701">dashboard</a> for the <code>openstack-exporter</code>: -</p>
<p><img src="/img/prometheus/microstack-openstack-dashboard.png" alt="MicroStack Openstack Dashboard"></p>
<p>If you decide to use the Canonical OpenStack Exporter, then you could use <a href="https://grafana.com/grafana/dashboards/7924">this dashboard</a> as a reference too.</p>
<h2 id="summary">Summary</h2>
<p>For those who choose to own their cloud infrastructure, the OpenStack Service Discovery mechanism does provide a very flexible way of monitoring the hypervisors and instances.</p>
<p>Also, if you migrate from a public cloud provider to OpenStack, it is comforting to know you can manage and monitor your own cloud the same way (i.e. Terraform for deployment, Prometheus for monitoring).</p>
]]></content>
        </item>
        
        <item>
            <title>Prometheus - Auto-deploying Consul and Exporters using Saltstack Part 6: MacOS</title>
            <link>https://yetiops.net/posts/prometheus-consul-saltstack-part-6-macos/</link>
            <pubDate>Wed, 08 Jul 2020 18:02:22 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/prometheus-consul-saltstack-part-6-macos/</guid>
            <description>This is the sixth part in my ongoing series on using SaltStack to deploy Consul and Prometheus Exporters, enabling Prometheus to discover machines and services to monitor. You can view the other posts in the series below: -
 Part 1 - Linux Part 2 - Windows Part 3 - OpenBSD Part 4 - FreeBSD Part 5 - illumos  All of the states are available in my Salt Lab repository.</description>
            <content type="html"><![CDATA[<p>This is the sixth part in my ongoing series on using SaltStack to deploy Consul and Prometheus Exporters, enabling Prometheus to discover machines and services to monitor. You can view the other posts in the series below: -</p>
<ul>
<li><a href="/posts/prometheus-consul-saltstack-part-1-linux/">Part 1 - Linux</a></li>
<li><a href="/posts/prometheus-consul-saltstack-part-2-windows/">Part 2 - Windows</a></li>
<li><a href="/posts/prometheus-consul-saltstack-part-3-openbsd/">Part 3 - OpenBSD</a></li>
<li><a href="/posts/prometheus-consul-saltstack-part-4-freebsd/">Part 4 - FreeBSD</a></li>
<li><a href="/posts/prometheus-consul-saltstack-part-5-illumos/">Part 5 - illumos</a></li>
</ul>
<p>All of the states are available in my <a href="https://gitlab.com/stuh84/salt-lab">Salt Lab</a> repository.</p>
<h2 id="why-macos">Why MacOS?</h2>
<p>Compared to the other systems covered so far, MacOS might seem an odd choice,  as it is primarily a desktop operating system. However there are many cases where businesses have fleets of Mac devices requiring centralized management  and monitoring that is scalable: -</p>
<ul>
<li>A render farm for video and/or animation</li>
<li>Continuous Integration pipelines for iOS applications</li>
<li>Building and signing MacOS applications</li>
</ul>
<p>Companies like <a href="https://www.macstadium.com/">MacStadium</a> offer Macs to customers in a cloud-like fashion for these kind of workloads, and there are other places (like in <a href="https://osxdaily.com/2015/07/26/mac-setups-a-high-end-animation-studio/">this</a> article) that use Macs as part of their rendering workflow.</p>
<p>MacOS has its roots in Unix (FreeBSD and NeXTSTEP) so in some ways it is similar managing a BSD or illumos system. There are some differences in service management, but those used to Linux or Unix should not feel entirely out of place on the MacOS command line.</p>
<h2 id="configuring-macos">Configuring MacOS</h2>
<p>MacOS takes you through the steps of setting up your machine during the install process. If you need to set a static IP after installation, go to <strong>Applications</strong> &gt; <strong>System Preferences</strong> &gt; <strong>Network</strong>, and then set the IP under the correct interface: -</p>

    <img src="/img/prometheus/macos-set-ip.png"  alt="MacOS Set Static IP"  class="center"  style="height:400px;"  />


<h2 id="installing-the-salt-minion">Installing the Salt Minion</h2>
<p>To install the Salt Minion in MacOS, download the latest version from <a href="https://repo.saltstack.com/#osx">here</a>, and then install by opening the executable and following the on-screen instructions. Once it is installed, you could configure it using: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo salt-config -i yourminionname -m yoursaltmaster
</code></pre></div><h3 id="configuring-the-salt-minion">Configuring the Salt Minion</h3>
<p>Salt has an included minion configuration file. We replace the contents with the below: -</p>
<p><strong>/etc/salt/minion</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">master: salt-master.yetiops.lab
id: macos-01.yetiops.lab
nodename: macos-01
</code></pre></div><p>You can then restart the Salt Minion using: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo launchctl stop com.saltstack.salt.minion
$ sudo launchctl start com.saltstack.salt.minion
</code></pre></div><p>MacOS&rsquo;s <code>launchctl</code> command does have a <strong>restart</strong> option, hence the need to stop and start the service.</p>
<p>You should now see this host attempt to register with the Salt Master: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ salt-key -L
Accepted Keys:
alpine-01.yetiops.lab
arch-01.yetiops.lab
centos-01.yetiops.lab
freebsd-01.yetiops.lab
omnios.yetiops.lab
openbsd-salt-01.yetiops.lab
salt-master.yetiops.lab
suse-01.yetiops.lab
ubuntu-01.yetiops.lab
void-01.yetiops.lab
win2019-01.yetiops.lab
Denied Keys:
Unaccepted Keys:
macos-01.yetiops.lab
Rejected Keys:
</code></pre></div><p>Accept the host with <code>salt-key -a 'macos-01*'</code>. Once this is done, you should now be able to manage the machine using Salt: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ salt <span style="color:#e6db74">&#39;macos*&#39;</span> test.ping
macos-01.yetiops.lab:
    True

$ salt <span style="color:#e6db74">&#39;macos*&#39;</span>  grains.item kernel
macos-01.yetiops.lab:
    ----------
    kernel:
        Darwin
</code></pre></div><p><a href="https://en.wikipedia.org/wiki/Darwin_(operating_system)">Darwin</a> is the base kernel that MacOS, iOS and Apple&rsquo;s other OSs are built upon.</p>
<h2 id="salt-states">Salt States</h2>
<p>We use two sets of states to deploy to MacOS. The first deploys Consul. The second deploys the Prometheus Node Exporter.</p>
<h3 id="applying-salt-states">Applying Salt States</h3>
<p>Once you have configured the states detailed below, use one of the following options to deploy the changes to the MacOS machine: -</p>
<ul>
<li><code>salt '*' state.highstate</code> from the Salt server (to configure every machine and every state)</li>
<li><code>salt 'macos*' state.highstate</code> from the Salt server (to configure all machines with a name beginning with <code>macos*</code>, applying all states)</li>
<li><code>salt 'macos*' state.apply consul</code> from the Salt server (to configure all machines with a name beginning with <code>macos*</code>, applying only the <code>consul</code> state)</li>
<li><code>salt-call state.highstate</code> from a machine running the Salt agent (to configure just one machine with all states)</li>
<li><code>salt-call state.apply consul</code> from a machine running the Salt agent (to configure just one machine with only the <code>consul</code> state)</li>
</ul>
<p>You can also use the <code>salt -C</code> option to apply based upon grains, pillars or other types of matches. For example, to apply to all machines running an MacOS kernel, you could run <code>salt -C 'G@kernel:Darwin' state.highstate</code>.</p>
<h3 id="consul---deployment">Consul - Deployment</h3>
<p>The following Salt state is used to deploy Consul onto a MacOS host: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul_binary</span>:
  <span style="color:#66d9ef">archive.extracted</span>:
    - <span style="color:#66d9ef">name</span>: /usr/local/bin
    - <span style="color:#66d9ef">source</span>: https://releases.hashicorp.com/consul/<span style="color:#ae81ff">1.7.3</span>/consul_1<span style="color:#ae81ff">.7</span>.3_darwin_amd64.zip
    - <span style="color:#66d9ef">source_hash</span>: sha256=813eab12ae5c1b815c293c0453d1658dc34d123ac40f3b20c4b12258e0b1034c
    - <span style="color:#66d9ef">enforce_toplevel</span>: <span style="color:#66d9ef">false</span>
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: wheel
    - <span style="color:#66d9ef">if_missing</span>: /usr/local/bin/consul

<span style="color:#66d9ef">consul_user</span>:
  <span style="color:#66d9ef">user.present</span>:
    - <span style="color:#66d9ef">name</span>: consul
    - <span style="color:#66d9ef">fullname</span>: Consul
    - <span style="color:#66d9ef">shell</span>: /bin/<span style="color:#66d9ef">false</span>
    - <span style="color:#66d9ef">home</span>: /etc/consul.d

<span style="color:#66d9ef">consul_group</span>:
  <span style="color:#66d9ef">group.present</span>:
    - <span style="color:#66d9ef">name</span>: consul

<span style="color:#66d9ef">/opt/consul</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/etc/consul.d</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/etc/consul.d/consul.hcl</span>:
  <span style="color:#66d9ef">file.managed</span>:
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>] is defined %}
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;server&#39;</span>] is defined %}
    - <span style="color:#66d9ef">source</span>: salt://consul/server/files/consul.hcl.j2
{% else %}
    - <span style="color:#66d9ef">source</span>: salt://consul/client/files/consul.hcl.j2
{% endif %}
{% endif %}
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0640</span>
    - <span style="color:#66d9ef">template</span>: jinja

<span style="color:#66d9ef">/Library/LaunchDaemons/com.consul.consul.plist</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://consul/client/files/consul-launchdaemon
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: wheel
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">644</span>

<span style="color:#66d9ef">consul_launchctl_load</span>:
   <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: launchctl load /Library/LaunchDaemons/com.consul.consul.plist
    - <span style="color:#66d9ef">watch</span>:
       - <span style="color:#66d9ef">file</span>: /Library/LaunchDaemons/com.consul.consul.plist


<span style="color:#66d9ef">consul_service</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: launchctl stop com.consul.consul; launchctl start com.consul.consul
    - <span style="color:#66d9ef">watch</span>:
      - <span style="color:#66d9ef">file</span>: /etc/consul.d/consul.hcl

{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>] is defined %}
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;prometheus_services&#39;</span>] is defined %}
{% for service in pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;prometheus_services&#39;</span>] %}
/etc/consul.d/{{ <span style="color:#66d9ef">service }}.hcl</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://consul/services/files/{{ service }}.hcl
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0640</span>
    - <span style="color:#66d9ef">template</span>: jinja

consul_reload_{{ <span style="color:#66d9ef">service }}</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: /usr/local/bin/consul reload
    - <span style="color:#66d9ef">watch</span>:
      - <span style="color:#66d9ef">file</span>: /etc/consul.d/{{ service }}.hcl
{% endfor %}
{% endif %}
{% endif %}
</code></pre></div><p>This state is similar to the states used for <a href="/posts/prometheus-consul-saltstack-part-1-linux/">Linux</a> or <a href="/posts/prometheus-consul-saltstack-part-5-illumos">illumos</a>.</p>
<p>Hashicorp provide MacOS binaries for Consul, so we can use the <code>archive.extracted</code> state to download it and extract it. After that, the main differences from the other states are how MacOS defines and uses services.</p>
<p>The service is defined in a <code>plist</code> file, which looks like the below: -</p>
<p><strong>/srv/salt/states/consul/client/files/consul-launchdaemon</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml"><span style="color:#75715e">&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt;</span>
<span style="color:#75715e">&lt;!DOCTYPE plist PUBLIC &#34;-//Apple//DTD PLIST 1.0//EN&#34; &#34;http://www.apple.com/DTDs/PropertyList-1.0.dtd&#34;&gt;</span>
<span style="color:#f92672">&lt;plist</span> <span style="color:#a6e22e">version=</span><span style="color:#e6db74">&#34;1.0&#34;</span><span style="color:#f92672">&gt;</span>
  <span style="color:#f92672">&lt;dict&gt;</span>
    <span style="color:#f92672">&lt;key&gt;</span>Label<span style="color:#f92672">&lt;/key&gt;</span>
    <span style="color:#f92672">&lt;string&gt;</span>com.consul.consul<span style="color:#f92672">&lt;/string&gt;</span>
    <span style="color:#f92672">&lt;key&gt;</span>RunAtLoad<span style="color:#f92672">&lt;/key&gt;</span>
    <span style="color:#f92672">&lt;true/&gt;</span>
    <span style="color:#f92672">&lt;key&gt;</span>KeepAlive<span style="color:#f92672">&lt;/key&gt;</span>
    <span style="color:#f92672">&lt;true/&gt;</span>
    <span style="color:#f92672">&lt;key&gt;</span>ProgramArguments<span style="color:#f92672">&lt;/key&gt;</span>
    <span style="color:#f92672">&lt;array&gt;</span>
        <span style="color:#f92672">&lt;string&gt;</span>/usr/local/bin/consul<span style="color:#f92672">&lt;/string&gt;</span>
        <span style="color:#f92672">&lt;string&gt;</span>agent<span style="color:#f92672">&lt;/string&gt;</span>
        <span style="color:#f92672">&lt;string&gt;</span>-config-dir<span style="color:#f92672">&lt;/string&gt;</span>
        <span style="color:#f92672">&lt;string&gt;</span>/etc/consul.d<span style="color:#f92672">&lt;/string&gt;</span>
    <span style="color:#f92672">&lt;/array&gt;</span>
    <span style="color:#f92672">&lt;key&gt;</span>SoftResourceLimits<span style="color:#f92672">&lt;/key&gt;</span>
    <span style="color:#f92672">&lt;dict&gt;</span>
        <span style="color:#f92672">&lt;key&gt;</span>NumberOfFiles<span style="color:#f92672">&lt;/key&gt;</span>
        <span style="color:#f92672">&lt;integer&gt;</span>100000<span style="color:#f92672">&lt;/integer&gt;</span>
    <span style="color:#f92672">&lt;/dict&gt;</span>
    <span style="color:#f92672">&lt;key&gt;</span>HardResourceLimits<span style="color:#f92672">&lt;/key&gt;</span>
    <span style="color:#f92672">&lt;dict&gt;</span>
        <span style="color:#f92672">&lt;key&gt;</span>NumberOfFiles<span style="color:#f92672">&lt;/key&gt;</span>
        <span style="color:#f92672">&lt;integer&gt;</span>100000<span style="color:#f92672">&lt;/integer&gt;</span>
    <span style="color:#f92672">&lt;/dict&gt;</span>
    <span style="color:#75715e">&lt;!-- uncomment the lines below to debug launchd issues --&gt;</span>
    <span style="color:#75715e">&lt;!--
</span><span style="color:#75715e">    &lt;key&gt;StandardOutPath&lt;/key&gt;
</span><span style="color:#75715e">    &lt;string&gt;/tmp/consul.out&lt;/string&gt;
</span><span style="color:#75715e">    &lt;key&gt;StandardErrorPath&lt;/key&gt;
</span><span style="color:#75715e">    &lt;string&gt;/tmp/consul.err&lt;/string&gt;
</span><span style="color:#75715e">    --&gt;</span>
  <span style="color:#f92672">&lt;/dict&gt;</span>
<span style="color:#f92672">&lt;/plist&gt;</span>
</code></pre></div><p>Like illumos/Solaris, MacOS defines services using XML. The relevant fields are: -</p>
<ul>
<li><code>Key: Label</code> - This is the name of the service (<code>com.consul.consul</code>)</li>
<li><code>RunAtLoad: True</code> - This ensures the service starts on boot</li>
<li><code>KeepAlive: True</code> - This makes sure that the service keeps running, rather than being a one time execution</li>
<li><code>ProgramArguments</code> - This contains the parameters we use to run Consul (with each argument on a different line)</li>
</ul>
<p>There are also other parameters like Soft and Hard Resource limits (which control how many files can be open on the system, and how many can be open per process), as well as some debugging options.</p>
<p>The command <code>launchctl load /Library/LaunchDaemons/com.consul.consul.plist</code> imports the <code>plist</code> file. This is only reimported if the contents of the service file change.</p>
<p>After that, the service is stopped and started, to ensure it will pick up any changes made to the service in future.</p>
<p>The final section regarding Consul services is the same as we use in the other parts in this series. This adds all services that Prometheus can then discover.</p>
<p>This state is applied to MacOS machines as such: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">base</span>:
  <span style="color:#66d9ef">&#39;G@init:systemd and G@kernel:Linux&#39;</span>:
    - <span style="color:#66d9ef">match</span>: compound
    - consul
    - exporters.node_exporter.systemd

  <span style="color:#66d9ef">&#39;os:Alpine&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.alpine
    - exporters.node_exporter.alpine

  <span style="color:#66d9ef">&#39;os:Void&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.void
    - exporters.node_exporter.void

  <span style="color:#66d9ef">&#39;kernel:OpenBSD&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.openbsd
    - exporters.node_exporter.bsd

  <span style="color:#66d9ef">&#39;kernel:FreeBSD&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.freebsd
    - exporters.node_exporter.bsd
    - exporters.gstat_exporter.freebsd

  <span style="color:#66d9ef">&#39;kernel:SunOS&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.illumos
    - exporters.node_exporter.illumos

  <span style="color:#66d9ef">&#39;kernel:Darwin&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.macos

  <span style="color:#66d9ef">&#39;kernel:Windows&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.windows
    - exporters.windows_exporter.win_exporter
    - exporters.windows_exporter.windows_exporter
</code></pre></div><p>We match the <code>kernel</code> grain, ensuring the value is <code>Darwin</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ salt <span style="color:#e6db74">&#39;macos*&#39;</span> grains.item kernel
macos-01.yetiops.lab:
    ----------
    kernel:
        Darwin
</code></pre></div><h4 id="pillars">Pillars</h4>
<p>We use the same pillars for MacOS as we do for Linux, OpenBSD and illumos: -</p>
<p><strong>consul.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul</span>:
  <span style="color:#66d9ef">data_dir</span>: /opt/consul
  <span style="color:#66d9ef">prometheus_services</span>:
  - node_exporter
</code></pre></div><p><strong>consul-dc.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul</span>:
  <span style="color:#66d9ef">dc</span>: yetiops
  <span style="color:#66d9ef">enc_key</span>: <span style="color:#75715e">###CONSUL_KEY###</span>
  <span style="color:#66d9ef">servers</span>:
  - salt-master.yetiops.lab
</code></pre></div><p>These pillars reside in <code>/srv/salt/pillars/consul</code>. They are applied as such: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">base</span>:
  <span style="color:#66d9ef">&#39;*&#39;</span>:
    - consul.consul-dc

  <span style="color:#66d9ef">&#39;G@kernel:Linux or G@kernel:OpenBSD or G@kernel:FreeBSD or G@kernel:SunOS or G@kernel:Darwin&#39;</span>:
    - <span style="color:#66d9ef">match</span>: compound
    - consul.consul

  <span style="color:#66d9ef">&#39;kernel:FreeBSD&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.consul-freebsd

  <span style="color:#66d9ef">&#39;kernel:Windows&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.consul-client-win

  <span style="color:#66d9ef">&#39;salt-master*&#39;</span>:
    - consul.consul-server
</code></pre></div><p>To match MacOS, we add the <code>G@kernel:Darwin</code> part to our original match statement (to include the standard <code>consul.consul</code> pillar).</p>
<h3 id="consul---verification">Consul - Verification</h3>
<p>We can verify that Consul is working with the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ consul members
Node                         Address           Status  Type    Build  Protocol  DC       Segment
salt-master                  10.15.31.5:8301   alive   server  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;all&gt;
alpine-01                    10.15.31.27:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
arch-01                      10.15.31.26:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
centos-01.yetiops.lab        10.15.31.24:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
freebsd-01.yetiops.lab       10.15.31.21:8301  alive   client  1.7.2  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
macos-01.local               10.15.31.29:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
omnios-01                    10.15.31.20:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
openbsd-salt-01.yetiops.lab  10.15.31.23:8301  alive   client  1.7.2  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
suse-01                      10.15.31.22:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
ubuntu-01                    10.15.31.33:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
void-01                      10.15.31.31:8301  alive   client  1.7.2  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
win2019-01                   10.15.31.25:8301  alive   client  1.7.2  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;

$ consul catalog nodes -service node_exporter
Node                         ID        Address      DC
alpine-01                    e59eb6fc  10.15.31.27  yetiops
arch-01                      97c67201  10.15.31.26  yetiops
centos-01.yetiops.lab        78ac8405  10.15.31.24  yetiops
freebsd-01.yetiops.lab       3e7b0ce8  10.15.31.21  yetiops
macos-01.local               9a2687e5  10.15.31.29  yetiops
omnios-01                    7c736402  10.15.31.20  yetiops
openbsd-salt-01.yetiops.lab  c87bfa18  10.15.31.23  yetiops
salt-master                  344fb6f2  10.15.31.5   yetiops
suse-01                      d2fdd88a  10.15.31.22  yetiops
ubuntu-01                    4544c7ff  10.15.31.33  yetiops
void-01                      e99c7e3c  10.15.31.31  yetiops
</code></pre></div><h3 id="node-exporter---deployment">Node Exporter - Deployment</h3>
<p>Now that Consul is up and running, we will install the Prometheus Node Exporter.</p>
<h4 id="states">States</h4>
<p>The following Salt state is used to deploy the Prometheus Node Exporter onto a MacOS host: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">{% if not salt[<span style="color:#e6db74">&#39;file.file_exists&#39;</span>](<span style="color:#e6db74">&#39;/usr/local/bin/node_exporter&#39;</span>) %}
<span style="color:#66d9ef">extract_node_exporter</span>:
  <span style="color:#66d9ef">archive.extracted</span>:
    - <span style="color:#66d9ef">name</span>: /usr/local/bin
    - <span style="color:#66d9ef">enforce_toplevel</span>: <span style="color:#66d9ef">false</span>
    - <span style="color:#66d9ef">source</span>: https://github.com/prometheus/node_exporter/releases/download/v1<span style="color:#ae81ff">.0.0</span>/node_exporter<span style="color:#ae81ff">-1.0.0</span>.darwin-amd64.tar.gz
    - <span style="color:#66d9ef">source_hash</span>: sha256=68eec397b0b88767508aab9ec5214070b5877daef33fb94b1377aa245bb90d01
    - <span style="color:#66d9ef">archive_format</span>: tar
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: wheel

<span style="color:#66d9ef">move_node_exporter</span>:
  <span style="color:#66d9ef">file.rename</span>:
    - <span style="color:#66d9ef">name</span>: /usr/local/bin/node_exporter
    - <span style="color:#66d9ef">source</span>: /usr/local/bin/node_exporter<span style="color:#ae81ff">-1.0.0</span>.darwin-amd64/node_exporter

<span style="color:#66d9ef">delete_node_exporter_dir</span>:
  <span style="color:#66d9ef">file.absent</span>:
    - <span style="color:#66d9ef">name</span>: /usr/local/bin/node_exporter<span style="color:#ae81ff">-1.0.0</span>.darwin-amd64/

<span style="color:#66d9ef">delete_node_exporter_files</span>:
  <span style="color:#66d9ef">file.absent</span>:
    - <span style="color:#66d9ef">name</span>: /usr/local/bin/node_exporter<span style="color:#ae81ff">-1.0.0</span>.darwin-amd64.tar.gz
{% endif %}

<span style="color:#66d9ef">node_exporter_user</span>:
  <span style="color:#66d9ef">user.present</span>:
    - <span style="color:#66d9ef">name</span>: node_exporter
    - <span style="color:#66d9ef">fullname</span>: Node Exporter
    - <span style="color:#66d9ef">shell</span>: /bin/<span style="color:#66d9ef">false</span>

<span style="color:#66d9ef">node_exporter_group</span>:
  <span style="color:#66d9ef">group.present</span>:
    - <span style="color:#66d9ef">name</span>: node_exporter

<span style="color:#66d9ef">/opt/prometheus/exporters/dist/textfile</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: node_exporter
    - <span style="color:#66d9ef">group</span>: node_exporter
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/Library/LaunchDaemons/com.prometheus.node_exporter.plist</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://exporters/node_exporter/files/node_exporter-launchdaemon
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: wheel
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">644</span>

<span style="color:#66d9ef">node_exporter_launchctl_load</span>:
   <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: launchctl load /Library/LaunchDaemons/com.prometheus.node_exporter.plist
    - <span style="color:#66d9ef">watch</span>:
       - <span style="color:#66d9ef">file</span>: /Library/LaunchDaemons/com.prometheus.node_exporter.plist

<span style="color:#66d9ef">node_exporter_service</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: launchctl stop com.prometheus.node_exporter; launchctl start com.prometheus.node_exporter
</code></pre></div><p>This state is very similar to the <a href="/posts/prometheus-consul-saltstack-part-1-linux/#node-exporter---systemd">Linux state</a>.</p>
<p>The first part downloads the <code>node_exporter</code> binary from GitHub, and extracts the contents. This extracts the contents to <code>/usr/local/bin/node_exporter-1.0.0.darwin-amd64/</code>. We then move the <code>node_exporter</code> binary into <code>/usr/local/bin</code> directory.</p>
<p>Finally, we clean up the the directory created by the archive, and remove the archive as well. These steps are executed if the <code>node_exporter</code> binary is not already <code>/usr/local/bin</code>.</p>
<p>After that, we create the Node Exporter user, group, textfile directory, and then define the service (using <code>plist</code> files like in the Consul deployment).</p>
<p>The contents of the <code>plist</code> are: -</p>
<p><strong>/srv/salt/states/exporters/node_exporter/files/node_exporter-launchdaemon</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">&lt;?xml version=<span style="color:#e6db74">&#34;1.0&#34;</span> encoding=<span style="color:#e6db74">&#34;UTF-8&#34;</span>?&gt;
&lt;!DOCTYPE plist PUBLIC <span style="color:#e6db74">&#34;-//Apple//DTD PLIST 1.0//EN&#34;</span> <span style="color:#e6db74">&#34;http://www.apple.com/DTDs/PropertyList-1.0.dtd&#34;</span>&gt;
&lt;plist version=<span style="color:#e6db74">&#34;1.0&#34;</span><span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">  &lt;dict&gt;</span>
    &lt;key&gt;Label&lt;/key<span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">    &lt;string&gt;com.prometheus.node_exporter&lt;/string&gt;</span>
    &lt;key&gt;RunAtLoad&lt;/key<span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">    &lt;true/&gt;</span>
    &lt;key&gt;KeepAlive&lt;/key<span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">    &lt;true/&gt;</span>
    &lt;key&gt;ProgramArguments&lt;/key<span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">    &lt;array&gt;</span>
        &lt;string&gt;/usr/local/bin/node_exporter&lt;/string<span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">        &lt;string&gt;--collector.textfile&lt;/string&gt;</span>
        &lt;string&gt;--collector.textfile.directory=/opt/prometheus/exporters/dist/textfile&lt;/string<span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">    &lt;/array&gt;</span>
    &lt;key&gt;SoftResourceLimits&lt;/key<span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">    &lt;dict&gt;</span>
        &lt;key&gt;NumberOfFiles&lt;/key<span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">        &lt;integer&gt;100000&lt;/integer&gt;</span>
    &lt;/dict<span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">    &lt;key&gt;HardResourceLimits&lt;/key&gt;</span>
    &lt;dict<span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">        &lt;key&gt;NumberOfFiles&lt;/key&gt;</span>
        &lt;integer&gt;<span style="color:#ae81ff">100000</span>&lt;/integer<span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">    &lt;/dict&gt;</span>
    &lt;!-- uncomment the lines below to debug launchd issues --<span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">    &lt;!--</span>
    &lt;key&gt;StandardOutPath&lt;/key<span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">    &lt;string&gt;/tmp/node_exporter.out&lt;/string&gt;</span>
    &lt;key&gt;StandardErrorPath&lt;/key<span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">    &lt;string&gt;/tmp/node_exporter.err&lt;/string&gt;</span>
    --<span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">  &lt;/dict&gt;</span>
&lt;/plist&gt;
</code></pre></div><p>This is very similar to the Consul service. The main differences are: -</p>
<ul>
<li>The label is <code>com.prometheus.node_exporter</code> (used when restarting the service)</li>
<li>The ProgramArguments (as it is a different service)</li>
</ul>
<p>We apply the state with the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">base</span>:
  <span style="color:#66d9ef">&#39;G@init:systemd and G@kernel:Linux&#39;</span>:
    - <span style="color:#66d9ef">match</span>: compound
    - consul
    - exporters.node_exporter.systemd

  <span style="color:#66d9ef">&#39;os:Alpine&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.alpine
    - exporters.node_exporter.alpine

  <span style="color:#66d9ef">&#39;os:Void&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.void
    - exporters.node_exporter.void

  <span style="color:#66d9ef">&#39;kernel:OpenBSD&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.openbsd
    - exporters.node_exporter.bsd

  <span style="color:#66d9ef">&#39;kernel:FreeBSD&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.freebsd
    - exporters.node_exporter.bsd
    - exporters.gstat_exporter.freebsd

  <span style="color:#66d9ef">&#39;kernel:SunOS&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.illumos
    - exporters.node_exporter.illumos

  <span style="color:#66d9ef">&#39;kernel:Darwin&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.macos
    - exporters.node_exporter.macos

  <span style="color:#66d9ef">&#39;kernel:Windows&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.windows
    - exporters.windows_exporter.win_exporter
    - exporters.windows_exporter.windows_exporter
</code></pre></div><h4 id="pillars-1">Pillars</h4>
<p>There are no pillars in this lab specific to the Node Exporter.</p>
<h3 id="node-exporter---verification">Node Exporter - Verification</h3>
<p>After this, we should be able to see the <code>node_exporter</code> running and producing metrics: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># Check the service is enabled</span>
$ launchctl list com.prometheus.node_exporter
<span style="color:#f92672">{</span>
	<span style="color:#e6db74">&#34;LimitLoadToSessionType&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;System&#34;</span>;
	<span style="color:#e6db74">&#34;Label&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;com.prometheus.node_exporter&#34;</span>;
	<span style="color:#e6db74">&#34;OnDemand&#34;</span> <span style="color:#f92672">=</span> false;
	<span style="color:#e6db74">&#34;LastExitStatus&#34;</span> <span style="color:#f92672">=</span> 0;
	<span style="color:#e6db74">&#34;PID&#34;</span> <span style="color:#f92672">=</span> 108;
	<span style="color:#e6db74">&#34;Program&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/usr/local/bin/node_exporter&#34;</span>;
	<span style="color:#e6db74">&#34;ProgramArguments&#34;</span> <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>
		<span style="color:#e6db74">&#34;/usr/local/bin/node_exporter&#34;</span>;
		<span style="color:#e6db74">&#34;--collector.textfile&#34;</span>;
		<span style="color:#e6db74">&#34;--collector.textfile.directory=/opt/prometheus/exporters/dist/textfile&#34;</span>;
	<span style="color:#f92672">)</span>;
<span style="color:#f92672">}</span>;

$ launchctl list | grep -iE <span style="color:#e6db74">&#34;pid|node_exporter&#34;</span>
PID	Status	Label
108	0	com.prometheus.node_exporter

<span style="color:#75715e"># Check it is listening</span>
$ netstat -an | grep -i <span style="color:#ae81ff">9100</span>
tcp4       <span style="color:#ae81ff">0</span>      <span style="color:#ae81ff">0</span>  10.15.31.29.9100       10.15.31.254.59210     ESTABLISHED
tcp46      <span style="color:#ae81ff">0</span>      <span style="color:#ae81ff">0</span>  *.9100                 *.*                    LISTEN

<span style="color:#75715e"># Check it responds</span>
$ curl 10.15.31.29:9100/metrics  | grep -i uname
node_uname_info<span style="color:#f92672">{</span>domainname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(none)&#34;</span>,machine<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;x86_64&#34;</span>,nodename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;macos-01&#34;</span>,release<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;19.5.0&#34;</span>,sysname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Darwin&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Darwin Kernel Version 19.5.0: Thu Apr 30 18:25:59 PDT 2020; root:xnu-6153.121.1~7/RELEASE_X86_64&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>
</code></pre></div><p>All looks good!</p>
<h2 id="prometheus-targets">Prometheus Targets</h2>
<p>As Prometheus is already set up (see <a href="/posts/prometheus-consul-saltstack-part-1-linux/#configuring-prometheus">here</a>), and matches on the <code>prometheus</code> tag, we should see this within the Prometheus targets straight away: -</p>
<p><img src="/img/prometheus/macos-prometheus-targets.png" alt="MacOS Prometheus Targets"></p>
<p>The last host in the list is the MacOS machine</p>
<p><img src="/img/prometheus/macos-prometheus-metadata.png" alt="MacOS Prometheus Metadata"></p>
<p>Above is the Metadata we receive from Consul about this host.</p>
<h2 id="grafana">Grafana</h2>
<p>If we use the <a href="https://grafana.com/grafana/dashboards/1860">Node Exporter Full</a> dashboard, most of the metrics are available: -</p>
<p><img src="/img/prometheus/macos-node-exporter-full.png" alt="MacOS Node Exporter Full"></p>
<p>In the above, the graphs for memory are missing. This is where we can use some of the BSD heritage of MacOS, by using the <a href="https://grafana.com/grafana/dashboards/4260">Node Exporter FreeBSD</a> dashboard: -</p>
<p><img src="/img/prometheus/macos-node-exporter-freebsd.png" alt="MacOS Node Exporter FreeBSD"></p>
<p>While we are still missing some metrics, we have more available than in the previous dashboard. With some tweaking to this dashboard, we can see all of the values: -</p>
<p><img src="/img/prometheus/macos-node-exporter-freebsd-tweaked.png" alt="MacOS Node Exporter FreeBSD Tweaked"></p>
<p>This does mean that the dashboard would no longer work for FreeBSDs hosts, so you would need to duplicate it and update the graphs accordingly.</p>
<h2 id="summary">Summary</h2>
<p>If, like me, you have used Macs for a number of years, you have probably used GUI utilities to manage them. However the capabilities are available to manage them like any other server operating system. Using something like SaltStack allows you to integrate MacOS alongside other systems with little effort.</p>
<p>As with all the systems we have covered, using the same tools to configure, manage and monitor your infrastructure is invaluable. If your infrastructure includes the need to build iOS applications, or you are managing a render farm in conjunction with Linux, Windows, BSD or illumos, you can use Prometheus and Grafana as your one pane of glass to monitor everything.</p>
<p>This post is the last in the series on managing and monitoring different operating systems using SaltStack, Consul and Prometheus. I hope those who read this series find it useful, and I am more than willing to include systems not already covered in future posts (suggestions in the comments!).</p>
]]></content>
        </item>
        
        <item>
            <title>Prometheus - Auto-deploying Consul and Exporters using Saltstack Part 5: illumos</title>
            <link>https://yetiops.net/posts/prometheus-consul-saltstack-part-5-illumos/</link>
            <pubDate>Sun, 05 Jul 2020 19:17:57 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/prometheus-consul-saltstack-part-5-illumos/</guid>
            <description>This is the fifth part in my ongoing series on using SaltStack to deploy Consul and Prometheus Exporters, enabling Prometheus to discover machines and services to monitor. You can view the other posts in the series below: -
 Part 1 - Linux Part 2 - Windows Part 3 - OpenBSD Part 4 - FreeBSD Part 6 - MacOS  All of the states (as well as those for future posts, if you want a quick preview) are available in my Salt Lab repository.</description>
            <content type="html"><![CDATA[<p>This is the fifth part in my ongoing series on using SaltStack to deploy Consul and Prometheus Exporters, enabling Prometheus to discover machines and services to monitor. You can view the other posts in the series below: -</p>
<ul>
<li><a href="/posts/prometheus-consul-saltstack-part-1-linux/">Part 1 - Linux</a></li>
<li><a href="/posts/prometheus-consul-saltstack-part-2-windows/">Part 2 - Windows</a></li>
<li><a href="/posts/prometheus-consul-saltstack-part-3-openbsd/">Part 3 - OpenBSD</a></li>
<li><a href="/posts/prometheus-consul-saltstack-part-4-freebsd/">Part 4 - FreeBSD</a></li>
<li><a href="/posts/prometheus-consul-saltstack-part-6-macos/">Part 6 - MacOS</a></li>
</ul>
<p>All of the states (as well as those for future posts, if you want a quick preview) are available in my <a href="https://gitlab.com/stuh84/salt-lab">Salt Lab</a> repository.</p>
<h2 id="why-illumos">Why illumos?</h2>
<p>illumos is a continuation of Sun Microsystem&rsquo;s OpenSolaris. It is technically a fork, starting around the time of the Oracle acquisition of Sun. As part of this acquisition, Oracle moved all development of Solaris (and other products) from open source to a closed source model.</p>
<p>Like Linux, there are multiple distributions of illumos. Notable examples are <a href="https://www.openindiana.org/">OpenIndiana</a> (aimed at being user friendly), <a href="https://www.joyent.com/smartos">SmartOS</a> (for building cloud and hypervisor platforms) and <a href="https://omniosce.org/">OmniOS</a> (a minimal server base).</p>
<p>illumos and Solaris are still used in some businesses, whether through supporting legacy systems or just a preference for the Solaris way of administering systems.</p>
<h3 id="which-distribution">Which distribution?</h3>
<p>To create this post, I decided to use OmniOS. With its minimal base, we can install what we need to show how to monitor an illumos (or even Solaris) system with few resources and extra packages.</p>
<h2 id="configuring-omnios">Configuring OmniOS</h2>
<p>The <a href="https://omniosce.org/setup/freshinstall.html">fresh installation walkthrough</a> for OmniOS is a great resource for installing the system and getting up and running. Also, because a lot of the tools to configure networking and storage are similar (and in some cases) the same as Solaris, most Solaris guides will apply to OmniOS as well.</p>
<p>You can set a static IP in the installer, but if you need to configure it after installation, you can do the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># Get the list of interfaces</span>
$ dladm show-link 
LINK        CLASS     MTU    STATE    BRIDGE     OVER
e1000g0     phys      <span style="color:#ae81ff">1500</span>   up       --         --
e1000g1     phys      <span style="color:#ae81ff">1500</span>   up       --         --

<span style="color:#75715e"># Show the current list of configured IPv4 and IPv6 addresses</span>
$ ipadm
ADDROBJ           TYPE     STATE        ADDR
lo0/v4            static   ok           127.0.0.1/8
e1000g0/dhcp      dhcp     ok           192.168.122.93/24
lo0/v6            static   ok           ::1/128

<span style="color:#75715e"># Add a static IP address to the e1000g1 interface</span>
$ ipadm create-addr -T static -a 10.15.31.20/24 e1000g1/v4

<span style="color:#75715e"># Show that the IP address was added</span>
$ ipadm
ADDROBJ           TYPE     STATE        ADDR
lo0/v4            static   ok           127.0.0.1/8
e1000g0/dhcp      dhcp     ok           192.168.122.93/24
e1000g1/v4        static   ok           10.15.31.20/24
lo0/v6            static   ok           ::1/128
</code></pre></div><h3 id="installing-the-salt-minion">Installing the Salt Minion</h3>
<p>Salt is not included in the OmniOS package archives, so we need to compile it from source. The <a href="https://docs.saltstack.com/en/latest/topics/installation/solaris.html">Solaris instructions</a> in SaltStack&rsquo;s documentation do work, but only after we install a prerequisite library.</p>
<p>The library we need to install is <a href="https://zeromq.org/">ZeroMQ</a>, which is not available in the OmniOS package archives.</p>
<p>To build <strong>ZeroMQ</strong> from source, you can do the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># Install the dependencies first</span>
$ pkg install developer/gcc7 developer/build/gnu-make developer/build/libtool developer/build/autoconf developer/build/automake developer/pkg-config developer/macro/gnu-m4

<span style="color:#75715e"># Clone the repository from GitHub</span>
$ git clone https://github.com/zeromq/libzmq.git
$ cd libzmq

<span style="color:#75715e"># Generate the &#34;configure&#34; script</span> 
$ ./autogen.sh

<span style="color:#75715e"># Configure, make and install</span>
$ MAKE<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;gmake&#34;</span> ./configure
$ gmake
$ sudo gmake install
</code></pre></div><p>The reason for using <code>gmake</code> (i.e. gnu-make) rather than just <code>make</code> is that some of the macros and other functions within the <code>Makefile</code> use GNU-specific terms. This means that the illumos/Solaris <code>make</code> will not be able to build <strong>ZeroMQ</strong>.</p>
<p>After you have installed the above, you should then be able to use the instructions to install Salt: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ git clone https://github.com/saltstack/salt
$ cd salt
$ sudo python setup.py install --force
</code></pre></div><p>After this, Salt should be installed and ready to use: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ which salt-minion                                          
/usr/bin/salt-minion

$ salt-minion --version                                       
salt-minion <span style="color:#ae81ff">3001</span>
</code></pre></div><h3 id="creating-a-service">Creating a service</h3>
<p>As Salt is not packaged for illumos, it does not come with any included service files. We will need to create our own to ensure that the Minion starts on boot and runs in the background.</p>
<p>In Solaris and illumos, we can use service <strong>manifests</strong> to define the methods we can use to execute the service. For the Salt Minion We also have the method file, which is a shell script that the manifest uses to control the Salt Minion binary.</p>
<p><strong>/lib/svc/manifest/system/salt-minion.xml</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml"><span style="color:#75715e">&lt;?xml version=&#39;1.0&#39;?&gt;</span>
<span style="color:#75715e">&lt;!DOCTYPE service_bundle SYSTEM &#39;/usr/share/lib/xml/dtd/service_bundle.dtd.1&#39;&gt;</span>
<span style="color:#75715e">&lt;!--
</span><span style="color:#75715e">Service manifest for salt-minion
</span><span style="color:#75715e">--&gt;</span>

<span style="color:#f92672">&lt;service_bundle</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#39;manifest&#39;</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#39;salt-minion:salt-minion&#39;</span><span style="color:#f92672">&gt;</span>

<span style="color:#f92672">&lt;service</span>
  <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#39;network/salt-minion&#39;</span>
  <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#39;service&#39;</span>
  <span style="color:#a6e22e">version=</span><span style="color:#e6db74">&#39;1&#39;</span><span style="color:#f92672">&gt;</span>
  <span style="color:#f92672">&lt;create_default_instance</span> <span style="color:#a6e22e">enabled=</span><span style="color:#e6db74">&#39;false&#39;</span> <span style="color:#f92672">/&gt;</span>
  <span style="color:#f92672">&lt;single_instance</span> <span style="color:#f92672">/&gt;</span>

  <span style="color:#f92672">&lt;dependency</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#39;fs&#39;</span>
    <span style="color:#a6e22e">grouping=</span><span style="color:#e6db74">&#39;require_all&#39;</span>
    <span style="color:#a6e22e">restart_on=</span><span style="color:#e6db74">&#39;none&#39;</span>
    <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#39;service&#39;</span><span style="color:#f92672">&gt;</span>
    <span style="color:#f92672">&lt;service_fmri</span> <span style="color:#a6e22e">value=</span><span style="color:#e6db74">&#39;svc:/system/filesystem/local&#39;</span> <span style="color:#f92672">/&gt;</span>
  <span style="color:#f92672">&lt;/dependency&gt;</span>

  <span style="color:#f92672">&lt;dependency</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#39;net&#39;</span>
    <span style="color:#a6e22e">grouping=</span><span style="color:#e6db74">&#39;require_all&#39;</span>
    <span style="color:#a6e22e">restart_on=</span><span style="color:#e6db74">&#39;none&#39;</span>
    <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#39;service&#39;</span><span style="color:#f92672">&gt;</span>
    <span style="color:#f92672">&lt;service_fmri</span> <span style="color:#a6e22e">value=</span><span style="color:#e6db74">&#39;svc:/network/loopback&#39;</span> <span style="color:#f92672">/&gt;</span>
  <span style="color:#f92672">&lt;/dependency&gt;</span>

  <span style="color:#f92672">&lt;exec_method</span>
    <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#39;method&#39;</span>
    <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#39;start&#39;</span>
    <span style="color:#a6e22e">exec=</span><span style="color:#e6db74">&#39;/lib/svc/method/svc-salt-minion start&#39;</span>
    <span style="color:#a6e22e">timeout_seconds=</span><span style="color:#e6db74">&#39;120&#39;</span><span style="color:#f92672">&gt;</span>
  <span style="color:#f92672">&lt;/exec_method&gt;</span>

  <span style="color:#f92672">&lt;exec_method</span>
    <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#39;method&#39;</span>
    <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#39;stop&#39;</span>
    <span style="color:#a6e22e">exec=</span><span style="color:#e6db74">&#39;/lib/svc/method/svc-salt-minion stop&#39;</span>
    <span style="color:#a6e22e">timeout_seconds=</span><span style="color:#e6db74">&#39;60&#39;</span><span style="color:#f92672">&gt;</span>
  <span style="color:#f92672">&lt;/exec_method&gt;</span>

  <span style="color:#f92672">&lt;exec_method</span>
    <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#39;method&#39;</span>
    <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#39;restart&#39;</span>
    <span style="color:#a6e22e">exec=</span><span style="color:#e6db74">&#39;/lib/svc/method/svc-salt-minion restart&#39;</span>
    <span style="color:#a6e22e">timeout_seconds=</span><span style="color:#e6db74">&#39;180&#39;</span><span style="color:#f92672">&gt;</span>
  <span style="color:#f92672">&lt;/exec_method&gt;</span>

<span style="color:#f92672">&lt;/service&gt;</span>

<span style="color:#f92672">&lt;/service_bundle&gt;</span>
</code></pre></div><p>This is in XML format. It defines the dependencies (i.e. the file system is available, and the network is up), and that we can start, stop or restart the service.</p>
<p>This is imported using <code>sudo svccfg import /lib/svc/manifest/system/salt-minion.xml</code>.</p>
<p>We also need to create the method file as well: -</p>
<p><strong>/lib/svc/method/svc-salt-minion</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/bin/sh
</span><span style="color:#75715e"></span>#
<span style="color:#75715e">#AUTOENABLE no</span>
#

CONF_DIR<span style="color:#f92672">=</span>/opt/local/etc/salt
PIDFILE<span style="color:#f92672">=</span>/var/run/salt-minion.pid
SALTMINION<span style="color:#f92672">=</span>/usr/bin/salt-minion

<span style="color:#f92672">[</span> ! -d <span style="color:#e6db74">${</span>CONF_DIR<span style="color:#e6db74">}</span> <span style="color:#f92672">]</span> <span style="color:#f92672">&amp;&amp;</span>  exit $CONF_DIR

start_service<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
        /bin/rm -f <span style="color:#e6db74">${</span>PIDFILE<span style="color:#e6db74">}</span>
        $SALTMINION -d -c <span style="color:#e6db74">${</span>CONF_DIR<span style="color:#e6db74">}</span> 2&gt;&amp;<span style="color:#ae81ff">1</span>
<span style="color:#f92672">}</span>

stop_service<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
        <span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> -f <span style="color:#e6db74">&#34;</span>$PIDFILE<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
                /usr/bin/kill -TERM <span style="color:#e6db74">`</span>/usr/bin/cat $PIDFILE<span style="color:#e6db74">`</span>
        <span style="color:#66d9ef">fi</span>
<span style="color:#f92672">}</span>

<span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#34;</span>$1<span style="color:#e6db74">&#34;</span> in
start<span style="color:#f92672">)</span>
        start_service
        ;;
stop<span style="color:#f92672">)</span>
        stop_service
        ;;
restart<span style="color:#f92672">)</span>
        stop_service
        sleep <span style="color:#ae81ff">1</span>
        start_service
        ;;
*<span style="color:#f92672">)</span>
        echo <span style="color:#e6db74">&#34;Usage: </span>$0<span style="color:#e6db74"> {start|stop|restart}&#34;</span>
        exit <span style="color:#ae81ff">1</span>
        ;;
<span style="color:#66d9ef">esac</span>
</code></pre></div><p>This is not too dissimilar from a SysVInit script or an <strong>rc.d</strong> script.</p>
<p>Once this is done, enable the service using <code>svcadm enable salt-minion</code>. You can then check the status with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ svcs salt-minion
STATE          STIME    FMRI
online         15:40:15 svc:/network/salt-minion:default
</code></pre></div><h3 id="configuring-the-salt-minion">Configuring the Salt Minion</h3>
<p>Salt has an included minion configuration file. We replace the contents with the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">master: salt-master.yetiops.lab
id: omnios.yetiops.lab
nodename: omnios
</code></pre></div><p>On illumos, this file resides in <code>/opt/local/etc/salt/minion</code>. Restart the Salt Minion to enable this configuration using <code>svcadm restart salt-minion</code>.</p>
<p>You should now see this host attempt to register with the Salt Master: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ salt-key -L
Accepted Keys:
alpine-01.yetiops.lab
arch-01.yetiops.lab
centos-01.yetiops.lab
freebsd-01.yetiops.lab
openbsd-salt-01.yetiops.lab
salt-master.yetiops.lab
suse-01.yetiops.lab
ubuntu-01.yetiops.lab
void-01.yetiops.lab
win2019-01.yetiops.lab
Denied Keys:
Unaccepted Keys:
omnios.yetiops.lab
Rejected Keys:
</code></pre></div><p>Accept the host with <code>salt-key -a 'omnios-01*'</code>. Once this is done, you should now be able to manage the machine using Salt: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ salt <span style="color:#e6db74">&#39;omnios*&#39;</span> test.ping
omnios.yetiops.lab:
    True

$ salt <span style="color:#e6db74">&#39;omnios*&#39;</span> grains.item kernel
omnios.yetiops.lab:
    ----------
    kernel:
        SunOS
</code></pre></div><p>Interestingly, the kernel reports as <code>SunOS</code> rather than <code>illumos</code> or <code>Solaris</code>. This means that we can use this across both illumos and Solaris-based systems.</p>
<h2 id="salt-states">Salt States</h2>
<p>We use two sets of states to deploy to illumos. The first deploys Consul. The second deploys the Prometheus Node Exporter.</p>
<h3 id="applying-salt-states">Applying Salt States</h3>
<p>Once you have configured the states detailed below, use one of the following options to deploy the changes to the illumos machine: -</p>
<ul>
<li><code>salt '*' state.highstate</code> from the Salt server (to configure every machine and every state)</li>
<li><code>salt 'omnios*' state.highstate</code> from the Salt server (to configure all machines with a name beginning with <code>omnios*</code>, applying all states)</li>
<li><code>salt 'omnios*' state.apply consul</code> from the Salt server (to configure all machines with a name beginning with <code>omnios*</code>, applying only the <code>consul</code> state)</li>
<li><code>salt-call state.highstate</code> from a machine running the Salt agent (to configure just one machine with all states)</li>
<li><code>salt-call state.apply consul</code> from a machine running the Salt agent (to configure just one machine with only the <code>consul</code> state)</li>
</ul>
<p>You can also use the <code>salt -C</code> option to apply based upon grains, pillars or other types of matches. For example, to apply to all machines running a SunOS (i.e. Solaris/illumos) kernel, you could run <code>salt -C 'G@kernel:SunOS' state.highstate</code>.</p>
<h3 id="consul---deployment">Consul - Deployment</h3>
<p>The following Salt state is used to deploy Consul onto an illumos host: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul_binary</span>:
  <span style="color:#66d9ef">archive.extracted</span>:
    - <span style="color:#66d9ef">name</span>: /usr/bin
    - <span style="color:#66d9ef">source</span>: https://releases.hashicorp.com/consul/<span style="color:#ae81ff">1.7.3</span>/consul_1<span style="color:#ae81ff">.7</span>.3_solaris_amd64.zip
    - <span style="color:#66d9ef">source_hash</span>: sha256=af49c5ff0639977d1efc9e5ef30277842c6bab90f53e4758b22b18e224e14bb1
    - <span style="color:#66d9ef">enforce_toplevel</span>: <span style="color:#66d9ef">false</span>
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root
    - <span style="color:#66d9ef">if_missing</span>: /usr/bin/consul

<span style="color:#66d9ef">consul_user</span>:
  <span style="color:#66d9ef">user.present</span>:
    - <span style="color:#66d9ef">name</span>: consul
    - <span style="color:#66d9ef">fullname</span>: Consul
    - <span style="color:#66d9ef">shell</span>: /bin/<span style="color:#66d9ef">false</span>
    - <span style="color:#66d9ef">home</span>: /etc/consul.d

<span style="color:#66d9ef">consul_group</span>:
  <span style="color:#66d9ef">group.present</span>:
    - <span style="color:#66d9ef">name</span>: consul

<span style="color:#66d9ef">/opt/consul</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/opt/local/etc/consul.d</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/lib/svc/manifest/system/consul.xml</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://consul/client/files/consul-svc-manifest
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: sys
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0640</span>

<span style="color:#66d9ef">consul_import_svc</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: svccfg import /lib/svc/manifest/system/consul.xml
    - <span style="color:#66d9ef">watch</span>:
       - <span style="color:#66d9ef">file</span>: /lib/svc/manifest/system/consul.xml

<span style="color:#66d9ef">/opt/local/etc/consul.d/consul.hcl</span>:
  <span style="color:#66d9ef">file.managed</span>:
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>] is defined %}
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;server&#39;</span>] is defined %}
    - <span style="color:#66d9ef">source</span>: salt://consul/server/files/consul.hcl.j2
{% else %}
    - <span style="color:#66d9ef">source</span>: salt://consul/client/files/consul.hcl.j2
{% endif %}
{% endif %}
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0640</span>
    - <span style="color:#66d9ef">template</span>: jinja

<span style="color:#66d9ef">consul_service</span>:
  <span style="color:#66d9ef">service.running</span>:
    - <span style="color:#66d9ef">name</span>: consul
    - <span style="color:#66d9ef">enable</span>: True
    - <span style="color:#66d9ef">reload</span>: True
    - <span style="color:#66d9ef">watch</span>:
      - <span style="color:#66d9ef">file</span>: /opt/local/etc/consul.d/consul.hcl

{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>] is defined %}
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;prometheus_services&#39;</span>] is defined %}
{% for service in pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;prometheus_services&#39;</span>] %}
/opt/local/etc/consul.d/{{ <span style="color:#66d9ef">service }}.hcl</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://consul/services/files/{{ service }}.hcl
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0640</span>
    - <span style="color:#66d9ef">template</span>: jinja

consul_reload_{{ <span style="color:#66d9ef">service }}</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: consul reload
    - <span style="color:#66d9ef">watch</span>:
      - <span style="color:#66d9ef">file</span>: /opt/local/etc/consul.d/{{ service }}.hcl
{% endfor %}
{% endif %}
{% endif %}
</code></pre></div><p>Hashicorp provide Consul binaries for Solaris, meaning we do not need to compile it ourselves. To summarise what we are doing in this state: -</p>
<ul>
<li>Download the Consul binary archive and extract its contents, but only if <code>/usr/bin/consul</code> does not exist already</li>
<li>Create a Consul user and group</li>
<li>Create the <code>/opt/consul</code> directory (for Consul to hold its state and runtime configuration)</li>
<li>Create the <code>/opt/local/etc/consul.d</code> directory (for Consul configuration files)</li>
<li>Adds a Service manifest file</li>
<li>Imports the Service manifest, if the Service manifest has changed
<ul>
<li>There are no Salt states that imports manifest files, so we use <code>cmd.run</code> (i.e. running an ad-hoc command on the host) to import it</li>
</ul>
</li>
<li>Adds the Consul configuration and services</li>
</ul>
<p>Most of this state is similar to how we deploy Consul on <a href="/posts/prometheus-consul-saltstack-part-1-linux/#consul---systemd">Linux</a>. The biggest difference is the use of the the manifest files, rather than SystemD-unit files or otherwise: -</p>
<p><strong>/srv/salt/states/consul/client/files/consul-svc-manifest</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml"><span style="color:#75715e">&lt;?xml version=&#34;1.0&#34;?&gt;</span>
<span style="color:#75715e">&lt;!DOCTYPE service_bundle SYSTEM &#34;/usr/share/lib/xml/dtd/service_bundle.dtd.1&#34;&gt;</span>
<span style="color:#75715e">&lt;!--
</span><span style="color:#75715e">        Created by Manifold
</span><span style="color:#75715e">--&gt;</span>
<span style="color:#f92672">&lt;service_bundle</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;manifest&#34;</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;consul&#34;</span><span style="color:#f92672">&gt;</span>

    <span style="color:#f92672">&lt;service</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;site/consul&#34;</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;service&#34;</span> <span style="color:#a6e22e">version=</span><span style="color:#e6db74">&#34;1&#34;</span><span style="color:#f92672">&gt;</span>

        <span style="color:#f92672">&lt;create_default_instance</span> <span style="color:#a6e22e">enabled=</span><span style="color:#e6db74">&#34;true&#34;</span><span style="color:#f92672">/&gt;</span>

        <span style="color:#f92672">&lt;single_instance/&gt;</span>

        <span style="color:#f92672">&lt;dependency</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;network&#34;</span> <span style="color:#a6e22e">grouping=</span><span style="color:#e6db74">&#34;require_all&#34;</span> <span style="color:#a6e22e">restart_on=</span><span style="color:#e6db74">&#34;error&#34;</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;service&#34;</span><span style="color:#f92672">&gt;</span>
            <span style="color:#f92672">&lt;service_fmri</span> <span style="color:#a6e22e">value=</span><span style="color:#e6db74">&#34;svc:/milestone/network:default&#34;</span><span style="color:#f92672">/&gt;</span>
        <span style="color:#f92672">&lt;/dependency&gt;</span>

        <span style="color:#f92672">&lt;dependency</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;filesystem&#34;</span> <span style="color:#a6e22e">grouping=</span><span style="color:#e6db74">&#34;require_all&#34;</span> <span style="color:#a6e22e">restart_on=</span><span style="color:#e6db74">&#34;error&#34;</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;service&#34;</span><span style="color:#f92672">&gt;</span>
            <span style="color:#f92672">&lt;service_fmri</span> <span style="color:#a6e22e">value=</span><span style="color:#e6db74">&#34;svc:/system/filesystem/local&#34;</span><span style="color:#f92672">/&gt;</span>
        <span style="color:#f92672">&lt;/dependency&gt;</span>

        <span style="color:#f92672">&lt;method_context&gt;</span>
            <span style="color:#f92672">&lt;method_credential</span> <span style="color:#a6e22e">user=</span><span style="color:#e6db74">&#34;consul&#34;</span> <span style="color:#a6e22e">group=</span><span style="color:#e6db74">&#34;consul&#34;</span><span style="color:#f92672">/&gt;</span>
        <span style="color:#f92672">&lt;/method_context&gt;</span>

        <span style="color:#f92672">&lt;exec_method</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;method&#34;</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;start&#34;</span> <span style="color:#a6e22e">exec=</span><span style="color:#e6db74">&#34;/usr/bin/consul agent -config-dir %{config_dir}&#34;</span> <span style="color:#a6e22e">timeout_seconds=</span><span style="color:#e6db74">&#34;60&#34;</span><span style="color:#f92672">/&gt;</span>

        <span style="color:#f92672">&lt;exec_method</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;method&#34;</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;stop&#34;</span> <span style="color:#a6e22e">exec=</span><span style="color:#e6db74">&#34;:kill&#34;</span> <span style="color:#a6e22e">timeout_seconds=</span><span style="color:#e6db74">&#34;60&#34;</span><span style="color:#f92672">/&gt;</span>
<span style="color:#f92672">&lt;exec_method</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;method&#34;</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;refresh&#34;</span> <span style="color:#a6e22e">exec=</span><span style="color:#e6db74">&#34;:kill -HUP&#34;</span> <span style="color:#a6e22e">timeout_seconds=</span><span style="color:#e6db74">&#34;10&#34;</span><span style="color:#f92672">/&gt;</span>
        <span style="color:#f92672">&lt;property_group</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;startd&#34;</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;framework&#34;</span><span style="color:#f92672">&gt;</span>
            <span style="color:#f92672">&lt;propval</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;duration&#34;</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;astring&#34;</span> <span style="color:#a6e22e">value=</span><span style="color:#e6db74">&#34;child&#34;</span><span style="color:#f92672">/&gt;</span>
            <span style="color:#f92672">&lt;propval</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;ignore_error&#34;</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;astring&#34;</span> <span style="color:#a6e22e">value=</span><span style="color:#e6db74">&#34;core,signal&#34;</span><span style="color:#f92672">/&gt;</span>
        <span style="color:#f92672">&lt;/property_group&gt;</span>

        <span style="color:#f92672">&lt;property_group</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;application&#34;</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;application&#34;</span><span style="color:#f92672">&gt;</span>
            <span style="color:#f92672">&lt;propval</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;config_dir&#34;</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;astring&#34;</span> <span style="color:#a6e22e">value=</span><span style="color:#e6db74">&#34;/opt/local/etc/consul.d&#34;</span><span style="color:#f92672">/&gt;</span>
        <span style="color:#f92672">&lt;/property_group&gt;</span>

        <span style="color:#f92672">&lt;stability</span> <span style="color:#a6e22e">value=</span><span style="color:#e6db74">&#34;Evolving&#34;</span><span style="color:#f92672">/&gt;</span>

        <span style="color:#f92672">&lt;template&gt;</span>
            <span style="color:#f92672">&lt;common_name&gt;</span>
                <span style="color:#f92672">&lt;loctext</span> <span style="color:#a6e22e">xml:lang=</span><span style="color:#e6db74">&#34;C&#34;</span><span style="color:#f92672">&gt;</span>
                    Consul service discovery
                <span style="color:#f92672">&lt;/loctext&gt;</span>
            <span style="color:#f92672">&lt;/common_name&gt;</span>
        <span style="color:#f92672">&lt;/template&gt;</span>

    <span style="color:#f92672">&lt;/service&gt;</span>

<span style="color:#f92672">&lt;/service_bundle&gt;</span>
</code></pre></div><p>This file was sourced from <a href="https://blog.jasper.la/consul-with-smf-on-solaris.html">here</a>. Unlike the Salt service manifest, this file calls the Consul binary directly, rather than using a method script.</p>
<p>This state is applied to illumos machine as such: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">base</span>:
  <span style="color:#66d9ef">&#39;G@init:systemd and G@kernel:Linux&#39;</span>:
    - <span style="color:#66d9ef">match</span>: compound
    - consul
    - exporters.node_exporter.systemd

  <span style="color:#66d9ef">&#39;os:Alpine&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.alpine
    - exporters.node_exporter.alpine

  <span style="color:#66d9ef">&#39;os:Void&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.void
    - exporters.node_exporter.void

  <span style="color:#66d9ef">&#39;kernel:OpenBSD&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.openbsd
    - exporters.node_exporter.bsd

  <span style="color:#66d9ef">&#39;kernel:FreeBSD&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.freebsd
    - exporters.node_exporter.bsd
    - exporters.gstat_exporter.freebsd

  <span style="color:#66d9ef">&#39;kernel:SunOS&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.illumos

  <span style="color:#66d9ef">&#39;kernel:Windows&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.windows
    - exporters.windows_exporter.win_exporter
    - exporters.windows_exporter.windows_exporter
</code></pre></div><p>We match the <code>kernel</code> grain, ensuring the value is <code>SunOS</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ salt <span style="color:#e6db74">&#39;omnios*&#39;</span> grains.item kernel
omnios.yetiops.lab:
    ----------
    kernel:
        SunOS
</code></pre></div><h4 id="pillars">Pillars</h4>
<p>We use the <strong>consul.sls</strong> and the <strong>consul-dc.sls</strong> pillars as we do with Linux, OpenBSD and FreeBSD.</p>
<p><strong>consul.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul</span>:
  <span style="color:#66d9ef">data_dir</span>: /opt/consul
  <span style="color:#66d9ef">prometheus_services</span>:
  - node_exporter
</code></pre></div><p><strong>consul-dc.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul</span>:
  <span style="color:#66d9ef">dc</span>: yetiops
  <span style="color:#66d9ef">enc_key</span>: <span style="color:#75715e">###CONSUL_KEY###</span>
  <span style="color:#66d9ef">servers</span>:
  - salt-master.yetiops.lab
</code></pre></div><p>We are not using any additional exporters, so no extra pillars need defining.</p>
<p>These pillars reside in <code>/srv/salt/pillars/consul</code>. They are applied as such: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">base</span>:
  <span style="color:#66d9ef">&#39;*&#39;</span>:
    - consul.consul-dc

  <span style="color:#66d9ef">&#39;G@kernel:Linux or G@kernel:OpenBSD or G@kernel:FreeBSD or G@kernel:SunOS&#39;</span>:
    - <span style="color:#66d9ef">match</span>: compound
    - consul.consul

  <span style="color:#66d9ef">&#39;kernel:FreeBSD&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.consul-freebsd

  <span style="color:#66d9ef">&#39;kernel:Windows&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.consul-client-win

  <span style="color:#66d9ef">&#39;salt-master*&#39;</span>:
    - consul.consul-server
</code></pre></div><p>To match illumos or Solaris, we add the <code>G@kernel:SunOS</code> part to our original match statement (to include the standard <code>consul.consul</code> pillar).</p>
<h3 id="consul---verification">Consul - Verification</h3>
<p>We can verify that Consul is working with the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ consul members
Node                         Address           Status  Type    Build  Protocol  DC       Segment
salt-master                  10.15.31.5:8301   alive   server  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;all&gt;
alpine-01                    10.15.31.27:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
arch-01                      10.15.31.26:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
centos-01.yetiops.lab        10.15.31.24:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
freebsd-01.yetiops.lab       10.15.31.21:8301  alive   client  1.7.2  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
omnios-01                    10.15.31.20:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
openbsd-salt-01.yetiops.lab  10.15.31.23:8301  alive   client  1.7.2  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
suse-01                      10.15.31.22:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
ubuntu-01                    10.15.31.33:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
void-01                      10.15.31.31:8301  alive   client  1.7.2  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
win2019-01                   10.15.31.25:8301  alive   client  1.7.2  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;

$ consul catalog nodes -service node_exporter
Node                         ID        Address      DC
alpine-01                    e59eb6fc  10.15.31.27  yetiops
arch-01                      97c67201  10.15.31.26  yetiops
centos-01.yetiops.lab        78ac8405  10.15.31.24  yetiops
freebsd-01.yetiops.lab       3e7b0ce8  10.15.31.21  yetiops
omnios-01                    7c736402  10.15.31.20  yetiops
openbsd-salt-01.yetiops.lab  c87bfa18  10.15.31.23  yetiops
salt-master                  344fb6f2  10.15.31.5   yetiops
suse-01                      d2fdd88a  10.15.31.22  yetiops
ubuntu-01                    4544c7ff  10.15.31.33  yetiops
void-01                      e99c7e3c  10.15.31.31  yetiops
</code></pre></div><h3 id="node-exporter---building-from-source">Node Exporter - Building from source</h3>
<p>As with SaltStack, there are no released builds for Solaris-like systems, nor do they exist in the OmniOS package manager. Because of this, we now need to build this from source as well.</p>
<p>To do this, first we need to install the Go programming language on the system: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ pkg install ooce/developer/go-114
</code></pre></div><p>After this, we can create a temporary Go environment and build the <code>node_exporter</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ export GOPATH<span style="color:#f92672">=</span>/tmp/go
$ go get github.com/prometheus/node_exporter
$ cd /tmp/go/src/github.com/prometheus/node_exporter
</code></pre></div><p>Rather than using <code>make</code> (or <code>gmake</code> to ensure it works with the GNU-specific macros), use <code>make build</code> (or <code>gmake build</code>), which will generate the correct binaries.</p>
<p>You may encounter an error during this process like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"> make: *** <span style="color:#f92672">[</span>Makefile.common:240: /tmp/go/bin/promu<span style="color:#f92672">]</span> Error <span style="color:#ae81ff">3</span> ***
</code></pre></div><p>If you want to build the full suite of Prometheus applications, this maybe be an issue. However, if you only need the <code>node_exporter</code> binary, then it should have already generated before this error. Go to <code>/tmp/go/bin</code> and you should see the <code>node_exporter</code> binary.</p>
<p><a href="https://elatov.github.io/2020/04/monitoring-other-targets-with-prometheus/">This post</a> is very useful in describing how to compile the <code>node_exporter</code> binary on OmniOS. Unlike the author of this post, I didn&rsquo;t seem to need the <code>promu</code> binary to build for the <code>node_exporter</code> binary to compile.</p>
<p>To make use of this binary, I placed it in <code>/srv/salt/states/exporters/node_exporter/files/node_exporter_illumos</code> so that Salt can deploy it to all illumos/Solaris hosts.</p>
<h3 id="node-exporter---deployment">Node Exporter - Deployment</h3>
<p>Now that the Node Exporter has been compiled and Consul is running, we can run the states to deploy it.</p>
<h4 id="states">States</h4>
<p>The following Salt state is used to deploy the Prometheus Node Exporter onto an illumos host: -</p>
<p><strong>/srv/salt/states/exporters/node_exporter/illumos.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">node_exporter_binary</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">name</span>: /usr/bin/node_exporter
    - <span style="color:#66d9ef">source</span>: salt://exporters/node_exporter/files/node_exporter_illumos
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: bin
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0755</span>

<span style="color:#66d9ef">node_exporter_user</span>:
  <span style="color:#66d9ef">user.present</span>:
    - <span style="color:#66d9ef">name</span>: node_exporter
    - <span style="color:#66d9ef">fullname</span>: Node Exporter
    - <span style="color:#66d9ef">shell</span>: /bin/<span style="color:#66d9ef">false</span>

<span style="color:#66d9ef">node_exporter_group</span>:
  <span style="color:#66d9ef">group.present</span>:
    - <span style="color:#66d9ef">name</span>: node_exporter

<span style="color:#66d9ef">/opt/prometheus/exporters/dist/textfile</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: node_exporter
    - <span style="color:#66d9ef">group</span>: node_exporter
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/lib/svc/manifest/system/node_exporter.xml</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://exporters/node_exporter/files/node_exporter-svc-manifest
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: sys
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0640</span>

<span style="color:#66d9ef">node_exporter_import_svc</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: svccfg import /lib/svc/manifest/system/node_exporter.xml
    - <span style="color:#66d9ef">watch</span>:
       - <span style="color:#66d9ef">file</span>: /lib/svc/manifest/system/node_exporter.xml

<span style="color:#66d9ef">node_exporter_service</span>:
  <span style="color:#66d9ef">service.running</span>:
    - <span style="color:#66d9ef">name</span>: node_exporter
    - <span style="color:#66d9ef">enable</span>: True
    - <span style="color:#66d9ef">reload</span>: True
</code></pre></div><p>As with the previous state, we need to create a Service manifest and deploy it. We also add the <code>node_exporter</code> binary that we compiled in the previous stage.</p>
<p>The Service manifest file looks like the below: -</p>
<p><strong>/srv/salt/states/exporters/node_exporter/files/node_exporter-svc-manifest</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml"><span style="color:#75715e">&lt;?xml version=&#34;1.0&#34;?&gt;</span>
<span style="color:#75715e">&lt;!DOCTYPE service_bundle SYSTEM &#34;/usr/share/lib/xml/dtd/service_bundle.dtd.1&#34;&gt;</span>
<span style="color:#75715e">&lt;!--
</span><span style="color:#75715e">        Created by Manifold
</span><span style="color:#75715e">--&gt;</span>
<span style="color:#f92672">&lt;service_bundle</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;manifest&#34;</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;node_exporter&#34;</span><span style="color:#f92672">&gt;</span>

    <span style="color:#f92672">&lt;service</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;site/node_exporter&#34;</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;service&#34;</span> <span style="color:#a6e22e">version=</span><span style="color:#e6db74">&#34;1&#34;</span><span style="color:#f92672">&gt;</span>

        <span style="color:#f92672">&lt;create_default_instance</span> <span style="color:#a6e22e">enabled=</span><span style="color:#e6db74">&#34;true&#34;</span><span style="color:#f92672">/&gt;</span>

        <span style="color:#f92672">&lt;single_instance/&gt;</span>

        <span style="color:#f92672">&lt;dependency</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;network&#34;</span> <span style="color:#a6e22e">grouping=</span><span style="color:#e6db74">&#34;require_all&#34;</span> <span style="color:#a6e22e">restart_on=</span><span style="color:#e6db74">&#34;error&#34;</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;service&#34;</span><span style="color:#f92672">&gt;</span>
            <span style="color:#f92672">&lt;service_fmri</span> <span style="color:#a6e22e">value=</span><span style="color:#e6db74">&#34;svc:/milestone/network:default&#34;</span><span style="color:#f92672">/&gt;</span>
        <span style="color:#f92672">&lt;/dependency&gt;</span>

        <span style="color:#f92672">&lt;dependency</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;filesystem&#34;</span> <span style="color:#a6e22e">grouping=</span><span style="color:#e6db74">&#34;require_all&#34;</span> <span style="color:#a6e22e">restart_on=</span><span style="color:#e6db74">&#34;error&#34;</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;service&#34;</span><span style="color:#f92672">&gt;</span>
            <span style="color:#f92672">&lt;service_fmri</span> <span style="color:#a6e22e">value=</span><span style="color:#e6db74">&#34;svc:/system/filesystem/local&#34;</span><span style="color:#f92672">/&gt;</span>
        <span style="color:#f92672">&lt;/dependency&gt;</span>

        <span style="color:#f92672">&lt;method_context&gt;</span>
            <span style="color:#f92672">&lt;method_credential</span> <span style="color:#a6e22e">user=</span><span style="color:#e6db74">&#34;node_exporter&#34;</span> <span style="color:#a6e22e">group=</span><span style="color:#e6db74">&#34;node_exporter&#34;</span><span style="color:#f92672">/&gt;</span>
        <span style="color:#f92672">&lt;/method_context&gt;</span>

        <span style="color:#f92672">&lt;exec_method</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;method&#34;</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;start&#34;</span> <span style="color:#a6e22e">exec=</span><span style="color:#e6db74">&#34;/usr/bin/node_exporter --collector.textfile --collector.textfile.directory=%{textfile_dir}&#34;</span> <span style="color:#a6e22e">timeout_seconds=</span><span style="color:#e6db74">&#34;60&#34;</span><span style="color:#f92672">/&gt;</span>

        <span style="color:#f92672">&lt;exec_method</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;method&#34;</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;stop&#34;</span> <span style="color:#a6e22e">exec=</span><span style="color:#e6db74">&#34;:kill&#34;</span> <span style="color:#a6e22e">timeout_seconds=</span><span style="color:#e6db74">&#34;60&#34;</span><span style="color:#f92672">/&gt;</span>
<span style="color:#f92672">&lt;exec_method</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;method&#34;</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;refresh&#34;</span> <span style="color:#a6e22e">exec=</span><span style="color:#e6db74">&#34;:kill -HUP&#34;</span> <span style="color:#a6e22e">timeout_seconds=</span><span style="color:#e6db74">&#34;10&#34;</span><span style="color:#f92672">/&gt;</span>
        <span style="color:#f92672">&lt;property_group</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;startd&#34;</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;framework&#34;</span><span style="color:#f92672">&gt;</span>
            <span style="color:#f92672">&lt;propval</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;duration&#34;</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;astring&#34;</span> <span style="color:#a6e22e">value=</span><span style="color:#e6db74">&#34;child&#34;</span><span style="color:#f92672">/&gt;</span>
            <span style="color:#f92672">&lt;propval</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;ignore_error&#34;</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;astring&#34;</span> <span style="color:#a6e22e">value=</span><span style="color:#e6db74">&#34;core,signal&#34;</span><span style="color:#f92672">/&gt;</span>
        <span style="color:#f92672">&lt;/property_group&gt;</span>

        <span style="color:#f92672">&lt;property_group</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;application&#34;</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;application&#34;</span><span style="color:#f92672">&gt;</span>
            <span style="color:#f92672">&lt;propval</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;textfile_dir&#34;</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;astring&#34;</span> <span style="color:#a6e22e">value=</span><span style="color:#e6db74">&#34;/opt/prometheus/exporters/dist/textfile&#34;</span><span style="color:#f92672">/&gt;</span>
        <span style="color:#f92672">&lt;/property_group&gt;</span>

        <span style="color:#f92672">&lt;stability</span> <span style="color:#a6e22e">value=</span><span style="color:#e6db74">&#34;Evolving&#34;</span><span style="color:#f92672">/&gt;</span>

        <span style="color:#f92672">&lt;template&gt;</span>
            <span style="color:#f92672">&lt;common_name&gt;</span>
                <span style="color:#f92672">&lt;loctext</span> <span style="color:#a6e22e">xml:lang=</span><span style="color:#e6db74">&#34;C&#34;</span><span style="color:#f92672">&gt;</span>
                    Node Exporter
                <span style="color:#f92672">&lt;/loctext&gt;</span>
            <span style="color:#f92672">&lt;/common_name&gt;</span>
        <span style="color:#f92672">&lt;/template&gt;</span>

    <span style="color:#f92672">&lt;/service&gt;</span>

<span style="color:#f92672">&lt;/service_bundle&gt;</span>
</code></pre></div><p>This manifest is very similar to the one we used in the Consul deployment (the Consul service was used as a starting point to create this one). This is imported using the <code>svccfg</code> command in the <code>node_exporter_import_svc</code> section of the state file.</p>
<p>We apply the state with the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">base</span>:
  <span style="color:#66d9ef">&#39;G@init:systemd and G@kernel:Linux&#39;</span>:
    - <span style="color:#66d9ef">match</span>: compound
    - consul
    - exporters.node_exporter.systemd

  <span style="color:#66d9ef">&#39;os:Alpine&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.alpine
    - exporters.node_exporter.alpine

  <span style="color:#66d9ef">&#39;os:Void&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.void
    - exporters.node_exporter.void

  <span style="color:#66d9ef">&#39;kernel:OpenBSD&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.openbsd
    - exporters.node_exporter.bsd

  <span style="color:#66d9ef">&#39;kernel:FreeBSD&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.freebsd
    - exporters.node_exporter.bsd
    - exporters.gstat_exporter.freebsd

  <span style="color:#66d9ef">&#39;kernel:SunOS&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.illumos
    - exporters.node_exporter.illumos

  <span style="color:#66d9ef">&#39;kernel:Windows&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.windows
    - exporters.windows_exporter.win_exporter
    - exporters.windows_exporter.windows_exporter
</code></pre></div><h4 id="pillars-1">Pillars</h4>
<p>There are no pillars in this lab specific to the Node Exporter.</p>
<h3 id="node-exporter---verification">Node Exporter - Verification</h3>
<p>After this, we should be able to see the <code>node_exporter</code> running and producing metrics: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># Check the service is enabled</span>
$ svcs node_exporter
STATE          STIME    FMRI
online         15:38:47 svc:/site/node_exporter:default

<span style="color:#75715e"># Check it is listening</span>
$ netstat -an | grep -Ei <span style="color:#e6db74">&#34;tcp|local|9100|---&#34;</span>
TCP: IPv4
   Local Address        Remote Address    Swind  Send-Q Rwind  Recv-Q    State
-------------------- -------------------- ------ ------ ------ ------ -----------
      *.9100               *.*                 <span style="color:#ae81ff">0</span>      <span style="color:#ae81ff">0</span> <span style="color:#ae81ff">128000</span>      <span style="color:#ae81ff">0</span> LISTEN
10.15.31.20.9100     10.15.31.254.46192    <span style="color:#ae81ff">64128</span>      <span style="color:#ae81ff">0</span> <span style="color:#ae81ff">128872</span>      <span style="color:#ae81ff">0</span> ESTABLISHED

TCP: IPv6
   Local Address                     Remote Address                 Swind  Send-Q Rwind  Recv-Q    State      If
--------------------------------- --------------------------------- ------ ------ ------ ------ ----------- -----
      *.9100                            *.*                              <span style="color:#ae81ff">0</span>      <span style="color:#ae81ff">0</span> <span style="color:#ae81ff">128000</span>      <span style="color:#ae81ff">0</span> LISTEN

<span style="color:#75715e"># Check it responds</span>
curl 10.15.31.20:9100/metrics  | grep -i boot
<span style="color:#75715e"># HELP node_boot_time_seconds Unix time of last boot, including microseconds.</span>
<span style="color:#75715e"># TYPE node_boot_time_seconds gauge</span>
node_boot_time_seconds 1.59387695e+09
</code></pre></div><p>There is no <code>uname</code> section exposed in the <code>node_exporter</code>, hence using the <code>node_boot_time_seconds</code> metric instead.</p>
<h2 id="prometheus-targets">Prometheus Targets</h2>
<p>As Prometheus is already set up (see <a href="/posts/prometheus-consul-saltstack-part-1-linux/#configuring-prometheus">here</a>), and matches on the <code>prometheus</code> tag, we should see this within the Prometheus targets straight away: -</p>
<p><img src="/img/prometheus/illumos-consul-target.png" alt="illumos Prometheus Consul Targets"></p>
<p>The second target here is the illumos host.</p>
<p><img src="/img/prometheus/illumos-consul-metadata.png" alt="illumos Prometheus Consul Metadata"></p>
<p>Above is the Metadata we receive from Consul about this host.</p>
<h2 id="grafana-dashboards">Grafana Dashboards</h2>
<p>Looking at the <code>node_exporter</code> <a href="https://github.com/prometheus/node_exporter">GitHub page</a>, the following metrics are supported on Solaris/illumos: -</p>
<ul>
<li><code>boottime</code></li>
<li><code>cpu</code></li>
<li><code>cpufreq</code></li>
<li><code>loadavg</code></li>
<li><code>zfs</code></li>
</ul>
<p>Because of this, you will find missing values on most dashboards (e.g. memory or network bandwidth utilization)</p>
<p>When using the existing Node Exporter dashboards with illumos, you&rsquo;ll also notice that either your host is not listed, or that the results are confusing. For example, if we look at the <a href="https://grafana.com/grafana/dashboards/1860">Node Exporter Full</a> dashboard, the host doesn&rsquo;t appear: -</p>
<p><img src="/img/prometheus/illumos-node-exporter-full.png" alt="Prometheus Node Exporter Full with illumos"></p>
<p>This is because the <code>node_exporter</code> binary on illumos/Solaris does not expose any <code>uname</code> metrics. The above dashboard relies upon <code>uname</code> metrics to differentiate between hosts: -</p>
<p><img src="/img/prometheus/illumos-node-exporter-full-vars.png" alt="Prometheus Node Exporter Full illumos variables"></p>
<p>Looking at either the <a href="https://grafana.com/grafana/dashboards/11640">OpenBSD Server Overview</a> dashboard or the <a href="https://grafana.com/grafana/dashboards/4260">Node Exporter FreeBSD</a> dashboard, you will see results. However, they display graphs that are rather intriguing: -</p>
<p><img src="/img/prometheus/illumos-node-exporter-freebsd.png" alt="Prometheus Node Exporter FreeBSD illumos"></p>
<p><img src="/img/prometheus/illumos-openbsd-server-overview.png" alt="Prometheus OpenBSD Server Overview illumos"></p>
<p>In both, we see CPU usage either in the negative thousands of percent (which is impossible) or the CPU being multiple of thousands of percent busy (which is very improbable). When compared to the load averages in both dashboards, it is safe to assume that the way these graphs are displaying the metrics does not match how a Solaris or illumos host would present them.</p>
<p>In the below, we can see what the CPU metric is returning across all of our systems: -</p>
<p><code>node_cpu_seconds_total{mode=~&quot;user&quot;}</code>
<img src="/img/prometheus/illumos-node-cpu-seconds-total.png" alt="Prometheus Node CPU Seconds Total"></p>
<p>As you can see, the value for the illumos host (<code>10.15.31.20</code>) is significantly higher than the others (nearly <strong>20000</strong>, when the others are between <strong>10</strong> and <strong>40</strong>). If you use the following query instead, this gives something closer to reality: -</p>
<p><code>100 - (irate(node_cpu_seconds_total{mode=&quot;idle&quot;, instance=&quot;10.15.31.20:9100&quot;}[5m]) / 10)</code>
<img src="/img/prometheus/illumos-idle-cpu-query.png" alt="Grafana Idle CPU illumos"></p>
<p>With this being the case, you will need to create your own dashboards if you choose to monitor illumos or Solaris hosts.</p>
<h2 id="summary">Summary</h2>
<p>As I found when writing this post, the support for Solaris and illumos-derived operating systems is not on the same level as it is with Linux, Windows or even BSD. This does seem to be a product of the industry as a whole, as it is becoming rarer to see a machine or host running Solaris or illumos in a company&rsquo;s infrastructure.</p>
<p>However for those who either still support Solaris, or those who use either Joyent&rsquo;s <a href="https://www.joyent.com/triton/compute">Triton Compute</a> (which Prometheus does provide <a href="https://prometheus.io/docs/prometheus/latest/configuration/configuration/#triton_sd_config">Service Discovery</a> for) or <a href="https://www.joyent.com/smartos">SmartOS</a>, using SaltStack to manage them is still very viable, as well as deploying the monitoring agents too.</p>
<p>In the next post in this series, we will cover how you deploy SaltStack on a MacOS host, which will then deploy Consul and the Prometheus Node Exporter.</p>
]]></content>
        </item>
        
        <item>
            <title>Prometheus - Auto-deploying Consul and Exporters using Saltstack Part 4: FreeBSD</title>
            <link>https://yetiops.net/posts/prometheus-consul-saltstack-part-4-freebsd/</link>
            <pubDate>Tue, 30 Jun 2020 20:05:28 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/prometheus-consul-saltstack-part-4-freebsd/</guid>
            <description>This is the fourth part in my ongoing series on using SaltStack to deploy Consul and Prometheus Exporters, enabling Prometheus to discover machines and services to monitor. You can view the other posts in the series below: -
 Part 1 - Linux Part 2 - Windows Part 3 - OpenBSD Part 5 - illumos Part 6 - MacOS  All of the states (as well as those for future posts, if you want a quick preview) are available in my Salt Lab repository.</description>
            <content type="html"><![CDATA[<p>This is the fourth part in my ongoing series on using SaltStack to deploy Consul and Prometheus Exporters, enabling Prometheus to discover machines and services to monitor. You can view the other posts in the series below: -</p>
<ul>
<li><a href="/posts/prometheus-consul-saltstack-part-1-linux/">Part 1 - Linux</a></li>
<li><a href="/posts/prometheus-consul-saltstack-part-2-windows/">Part 2 - Windows</a></li>
<li><a href="/posts/prometheus-consul-saltstack-part-3-openbsd/">Part 3 - OpenBSD</a></li>
<li><a href="/posts/prometheus-consul-saltstack-part-5-illumos/">Part 5 - illumos</a></li>
<li><a href="/posts/prometheus-consul-saltstack-part-6-macos/">Part 6 - MacOS</a></li>
</ul>
<p>All of the states (as well as those for future posts, if you want a quick preview) are available in my <a href="https://gitlab.com/stuh84/salt-lab">Salt Lab</a> repository.</p>
<h2 id="why-freebsd">Why FreeBSD?</h2>
<p>FreeBSD is the most popular BSD-based operating system. Whereas OpenBSD&rsquo;s primary goal is security, FreeBSD&rsquo;s is on wider usability and better compatibility with newer hardware. This does mean that sometimes FreeBSD will include non-free/libre hardware modules for greater compatibility.</p>
<p>As FreeBSD (and other BSDs) is permissively licensed (using the BSD licenses rather than GPLv3 or similar), it is often used as a base for commercial operating systems that do not make their source code available. Examples of this are the Playstation 3 and 4 OS, Juniper&rsquo;s JunOS, and even MacOS contains FreeBSD code (as part of the <a href="https://en.wikipedia.org/wiki/Darwin_(operating_system)">Darwin</a> project).</p>
<p>A number of commercial companies contribute to the FreeBSD codebase, including <a href="https://www.ixsystems.com/">iXsystems</a> and a small, relatively unknown video streaming company called <a href="https://papers.freebsd.org/2019/fosdem/looney-netflix_and_freebsd/">Netflix</a> (tongue firmly in cheek 😝).</p>
<h2 id="configuring-freebsd">Configuring FreeBSD</h2>
<p>The <a href="https://www.freebsd.org/doc/en_US.ISO8859-1/books/faq/">FreeBSD FAQ</a> is a good place to start when discovering how to install and configure FreeBSD.</p>
<p>You can set a static IP for FreeBSD within the installer. To set it after install,edit <code>/etc/rc.conf</code> and add a line like the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ifconfig_vtnet1<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;inet 10.15.31.21 netmask 255.255.255.0&#34;</span>
</code></pre></div><p>Bring up the additional interface using <code>service netif restart</code>.</p>
<h2 id="installing-the-salt-minion">Installing the Salt Minion</h2>
<p>To install the Salt Minion in FreeBSD, you can use the <code>pkg</code> utility.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># Ensure the package exists</span>
$ pkg search salt
p5-Crypt-Salt-0.01_1           Perl extension to generate a salt to be fed into crypt
p5-Crypt-SaltedHash-0.09       Perl extension to work with salted hashes
py27-salt-3001_1               Distributed remote execution and configuration management system
py37-salt-3001_1               Distributed remote execution and configuration management system
rubygem-hammer_cli_foreman_salt-0.0.5 SaltStack integration commands <span style="color:#66d9ef">for</span> Hammer CLI
rubygem-smart_proxy_salt-2.1.9 SaltStack Plug-In <span style="color:#66d9ef">for</span> Foreman<span style="color:#960050;background-color:#1e0010">&#39;</span>s Smart Proxy

<span style="color:#75715e"># Install the package</span>
$ pkg install py37-salt-3001_1 
</code></pre></div><p>After this, the Salt Minion will now be installed.</p>
<h3 id="configuring-the-salt-minion">Configuring the Salt Minion</h3>
<p>Salt has an included minion configuration file. We replace the contents with the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">master: salt-master.yetiops.lab
id: freebsd-01.yetiops.lab
nodename: freebsd-01 
</code></pre></div><p>On most systems, this is in <code>/etc/salt</code>. On FreeBSD though, it is in <code>/usr/local/etc/salt</code>. This is because most user-installed utilities and applications in FreeBSD go into <code>/usr/local</code>, to separate them from the included applications and configuration.</p>
<p>Enable the Salt Minion and restart it using the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># Enable the service</span>
$ sysrc salt_minion_enable<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;YES&#34;</span>
salt_minion_enable: NO -&gt; YES

<span style="color:#75715e"># Restart the service</span>
$ service salt_minion restart
Stopping salt_minion.
Waiting <span style="color:#66d9ef">for</span> PIDS: 866.
Starting salt_minion.
</code></pre></div><p>You should now see this host attempt to register with the Salt Master: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo salt-key -L
Accepted Keys:
alpine-01.yetiops.lab
arch-01.yetiops.lab
centos-01.yetiops.lab
freebsd-01.yetiops.lab
openbsd-salt-01.yetiops.lab
salt-master.yetiops.lab
suse-01.yetiops.lab
ubuntu-01.yetiops.lab
void-01.yetiops.lab
win2019-01.yetiops.lab
Denied Keys:
Unaccepted Keys:
freebsd-01.yetiops.lab
Rejected Keys:
</code></pre></div><p>Accept the host with <code>salt-key -a 'freebsd-01*'</code>. Once this is done, you should now be able to manage the machine using Salt: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ salt <span style="color:#e6db74">&#39;freebsd*&#39;</span> test.ping
freebsd-01.yetiops.lab:
    True

$ salt <span style="color:#e6db74">&#39;freebsd*&#39;</span> grains.item os
freebsd-01.yetiops.lab:
    ----------
    os:
        FreeBSD
</code></pre></div><h2 id="salt-states">Salt States</h2>
<p>We use three sets of states to deploy to FreeBSD. The first deploys Consul. The second deploys the Prometheus Node Exporter. The third installs the <code>gstat_exporter</code> which is used to query <a href="https://en.wikipedia.org/wiki/GEOM">GEOM</a> devices (primarily storage) on a FreeBSD host. This exporter is covered by the <a href="https://twitter.com/tykling">author</a> <a href="https://blog.tyk.nu/blog/all-in-a-days-work-prometheus-gstat_exporter-and-grafana-dashboard/">here</a> (including a provided dashboard).</p>
<h3 id="applying-salt-states">Applying Salt States</h3>
<p>Once you have configured the states detailed below, use one of the following options to deploy the changes to the FreeBSD machine: -</p>
<ul>
<li><code>salt '*' state.highstate</code> from the Salt server (to configure every machine and every state)</li>
<li><code>salt 'freebsd*' state.highstate</code> from the Salt server (to configure all machines with a name beginning with <code>freebsd*</code>, applying all states)</li>
<li><code>salt 'freebsd*' state.apply consul</code> from the Salt server (to configure all machines with a name beginning with <code>freebsd*</code>, applying only the <code>consul</code> state)</li>
<li><code>salt-call state.highstate</code> from a machine running the Salt agent (to configure just one machine with all states)</li>
<li><code>salt-call state.apply consul</code> from a machine running the Salt agent (to configure just one machine with only the <code>consul</code> state)</li>
</ul>
<p>You can also use the <code>salt -C</code> option to apply based upon grains, pillars or other types of matches. For example, to apply to all machines running an FreeBSD kernel, you could run <code>salt -C 'G@kernel:FreeBSD' state.highstate</code>.</p>
<h3 id="consul---deployment">Consul - Deployment</h3>
<p>The following Salt state is used to deploy Consul onto a FreeBSD host: -</p>
<p><strong>/srv/salt/states/consul/freebsd.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul_package</span>:
  <span style="color:#66d9ef">pkg.installed</span>:
  - <span style="color:#66d9ef">pkgs</span>:
    - consul

<span style="color:#66d9ef">/opt/consul</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/usr/local/etc/consul.d</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/usr/local/etc/consul.d/consul.hcl</span>:
  <span style="color:#66d9ef">file.managed</span>:
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>] is defined %}
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;server&#39;</span>] is defined %}
    - <span style="color:#66d9ef">source</span>: salt://consul/server/files/consul.hcl.j2
{% else %}
    - <span style="color:#66d9ef">source</span>: salt://consul/client/files/consul.hcl.j2
{% endif %}
{% endif %}
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0640</span>
    - <span style="color:#66d9ef">template</span>: jinja

<span style="color:#66d9ef">consul_service</span>:
  <span style="color:#66d9ef">service.running</span>:
  - <span style="color:#66d9ef">name</span>: consul
  - <span style="color:#66d9ef">enable</span>: True
  - <span style="color:#66d9ef">reload</span>: True
  - <span style="color:#66d9ef">watch</span>:
    - <span style="color:#66d9ef">file</span>: /usr/local/etc/consul.d/consul.hcl

{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>] is defined %}
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;prometheus_services&#39;</span>] is defined %}
{% for service in pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;prometheus_services&#39;</span>] %}
/usr/local/etc/consul.d/{{ <span style="color:#66d9ef">service }}.hcl</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://consul/services/files/{{ service }}.hcl
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0640</span>
    - <span style="color:#66d9ef">template</span>: jinja

consul_reload_{{ <span style="color:#66d9ef">service }}</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: consul reload
    - <span style="color:#66d9ef">watch</span>:
      - <span style="color:#66d9ef">file</span>: /usr/local/etc/consul.d/{{ service }}.hcl
{% endfor %}
{% endif %}
{% endif %}
</code></pre></div><p>If you compare this to the <a href="/posts/prometheus-consul-saltstack-part-3-openbsd/#consul---deployment">OpenBSD Consul Deployment</a>, they are almost identical. The only differences are: -</p>
<ul>
<li>FreeBSD does not prefix system users and groups with an underscore (e.g. <code>_consul</code> rather than <code>consul</code>)</li>
<li>The configuration paths are all prefixed with <code>/usr/local</code> (e.g. <code>/usr/local/etc/consul</code> rather than <code>/etc/consul</code>)</li>
</ul>
<p>This state is applied to FreeBSD machines as such: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">base</span>:
  <span style="color:#66d9ef">&#39;G@init:systemd and G@kernel:Linux&#39;</span>:
    - <span style="color:#66d9ef">match</span>: compound
    - consul
    - exporters.node_exporter.systemd

  <span style="color:#66d9ef">&#39;os:Alpine&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.alpine
    - exporters.node_exporter.alpine

  <span style="color:#66d9ef">&#39;os:Void&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.void
    - exporters.node_exporter.void

  <span style="color:#66d9ef">&#39;kernel:OpenBSD&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.openbsd
    - exporters.node_exporter.bsd

  <span style="color:#66d9ef">&#39;kernel:FreeBSD&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.freebsd

  <span style="color:#66d9ef">&#39;kernel:Windows&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.windows
    - exporters.windows_exporter.win_exporter
    - exporters.windows_exporter.windows_exporter
</code></pre></div><p>We match the <code>kernel</code> grain, ensuring the value is <code>FreeBSD</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ salt <span style="color:#e6db74">&#39;freebsd*&#39;</span> grains.item kernel
freebsd-01.yetiops.lab:
    ----------
    kernel:
        FreeBSD
</code></pre></div><h4 id="pillars">Pillars</h4>
<p>We use the <strong>consul.sls</strong> and the <strong>consul-dc.sls</strong> pillars as we do with Linux and OpenBSD.</p>
<p><strong>consul.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul</span>:
  <span style="color:#66d9ef">data_dir</span>: /opt/consul
  <span style="color:#66d9ef">prometheus_services</span>:
  - node_exporter
</code></pre></div><p><strong>consul-dc.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul</span>:
  <span style="color:#66d9ef">dc</span>: yetiops
  <span style="color:#66d9ef">enc_key</span>: <span style="color:#75715e">###CONSUL_KEY###</span>
  <span style="color:#66d9ef">servers</span>:
  - salt-master.yetiops.lab
</code></pre></div><p>We also specify an additional pillar, to add the additional exporter: -</p>
<p><strong>consul-freebsd.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul</span>:
  <span style="color:#66d9ef">prometheus_services</span>:
    - gstat_exporter
    - node_exporter
</code></pre></div><p>We have specified the <code>node_exporter</code> twice. This is because the <code>prometheus_services</code> list in this pillar does not merge with the list in the <strong>consul.sls</strong> file, taking precedence over it instead. The Consul service file for the <code>gstat_exporter</code> looks like the below: -</p>
<p><strong>/srv/states/consul/services/files/gstat_exporter.hcl</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">{<span style="color:#e6db74">&#34;service&#34;</span>:
  {<span style="color:#66d9ef">&#34;name&#34;: </span><span style="color:#e6db74">&#34;gstat_exporter&#34;</span>,
   <span style="color:#66d9ef">&#34;tags&#34;: </span>[<span style="color:#e6db74">&#34;gstat_exporter&#34;</span>, <span style="color:#e6db74">&#34;prometheus&#34;</span>],
   <span style="color:#66d9ef">&#34;port&#34;: </span><span style="color:#ae81ff">9248</span>
  }
}
</code></pre></div><p>We use the <code>prometheus</code> tag as before (to ensure that it is discovered by Prometheus) and also the <code>gstat_exporter</code> tag. If we want to use a different Prometheus job (to apply different scrape intervals or relabelling), we could use this tag to match against.</p>
<p>These pillars reside in <code>/srv/salt/pillars/consul</code>. They are applied as such: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">base</span>:
  <span style="color:#66d9ef">&#39;*&#39;</span>:
    - consul.consul-dc

  <span style="color:#66d9ef">&#39;G@kernel:Linux or G@kernel:OpenBSD or G@kernel:FreeBSD&#39;</span>:
    - <span style="color:#66d9ef">match</span>: compound
    - consul.consul

  <span style="color:#66d9ef">&#39;kernel:FreeBSD&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.consul-freebsd

  <span style="color:#66d9ef">&#39;kernel:Windows&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.consul-client-win

  <span style="color:#66d9ef">&#39;salt-master*&#39;</span>:
    - consul.consul-server
</code></pre></div><p>To match FreeBSD, we add the <code>G@kernel:FreeBSD</code> part to our original match statement (to include the standard <code>consul.consul</code> pillar) as well an additional section to add the FreeBSD-specific exporters.</p>
<h3 id="consul---verification">Consul - Verification</h3>
<p>We can verify that Consul is working with the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ consul members
Node                         Address           Status  Type    Build  Protocol  DC       Segment
salt-master                  10.15.31.5:8301   alive   server  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;all&gt;
alpine-01                    10.15.31.27:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
arch-01                      10.15.31.26:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
centos-01.yetiops.lab        10.15.31.24:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
freebsd-01.yetiops.lab       10.15.31.21:8301  alive   client  1.7.2  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
openbsd-salt-01.yetiops.lab  10.15.31.23:8301  alive   client  1.7.2  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
suse-01                      10.15.31.22:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
ubuntu-01                    10.15.31.33:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
void-01                      10.15.31.31:8301  alive   client  1.7.2  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
win2019-01                   10.15.31.25:8301  alive   client  1.7.2  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;

$ consul catalog nodes -service node_exporter
Node                         ID        Address      DC
alpine-01                    e59eb6fc  10.15.31.27  yetiops
arch-01                      97c67201  10.15.31.26  yetiops
centos-01.yetiops.lab        78ac8405  10.15.31.24  yetiops
freebsd-01.yetiops.lab       3e7b0ce8  10.15.31.21  yetiops
openbsd-salt-01.yetiops.lab  c87bfa18  10.15.31.23  yetiops
salt-master                  344fb6f2  10.15.31.5   yetiops
suse-01                      d2fdd88a  10.15.31.22  yetiops
ubuntu-01                    4544c7ff  10.15.31.33  yetiops
void-01                      e99c7e3c  10.15.31.31  yetiops
</code></pre></div><h3 id="node-exporter---deployment">Node Exporter - Deployment</h3>
<p>Now that Consul is up and running, we will install the Prometheus Node Exporter.</p>
<h4 id="states">States</h4>
<p>The following Salt state is used to deploy the Prometheus Node Exporter onto a FreeBSD host: -</p>
<p><strong>/srv/salt/states/exporters/node_exporter/bsd.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">node_exporter_package</span>:
  <span style="color:#66d9ef">pkg.installed</span>:
    - <span style="color:#66d9ef">pkgs</span>:
      - node_exporter

<span style="color:#66d9ef">node_exporter_service</span>:
  <span style="color:#66d9ef">service.running</span>:
    - <span style="color:#66d9ef">name</span>: node_exporter
    - <span style="color:#66d9ef">enable</span>: <span style="color:#66d9ef">true</span>
</code></pre></div><p>This state is the same as we use for OpenBSD. While the hosts themselves use a different package manager and init systems, Salt serves as an abstraction to this, meaning we do not need to configure states per package manager in use.</p>
<p>We apply the state with the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">base</span>:
  <span style="color:#66d9ef">&#39;G@init:systemd and G@kernel:Linux&#39;</span>:
    - <span style="color:#66d9ef">match</span>: compound
    - consul
    - exporters.node_exporter.systemd

  <span style="color:#66d9ef">&#39;os:Alpine&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.alpine
    - exporters.node_exporter.alpine

  <span style="color:#66d9ef">&#39;os:Void&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.void
    - exporters.node_exporter.void

  <span style="color:#66d9ef">&#39;kernel:OpenBSD&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.openbsd
    - exporters.node_exporter.bsd

  <span style="color:#66d9ef">&#39;kernel:FreeBSD&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.freebsd
    - exporters.node_exporter.bsd

  <span style="color:#66d9ef">&#39;kernel:Windows&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.windows
    - exporters.windows_exporter.win_exporter
    - exporters.windows_exporter.windows_exporter
</code></pre></div><h4 id="pillars-1">Pillars</h4>
<p>There are no pillars in this lab specific to the Node Exporter.</p>
<h3 id="node-exporter---verification">Node Exporter - Verification</h3>
<p>After this, we should be able to see the <code>node_exporter</code> running and producing metrics: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># Check the service is enabled</span>
$ service node_exporter status
node_exporter is running as pid 873.
$

<span style="color:#75715e"># Check it is listening</span>
$ netstat -an | grep -i <span style="color:#ae81ff">9100</span>
tcp4       <span style="color:#ae81ff">0</span>      <span style="color:#ae81ff">0</span> 10.15.31.21.9100       10.15.31.254.45882     ESTABLISHED
tcp46      <span style="color:#ae81ff">0</span>      <span style="color:#ae81ff">0</span> *.9100                 *.*                    LISTEN

<span style="color:#75715e"># Check it responds</span>
$ curl 10.15.31.21:9100/metrics | grep -i uname
node_uname_info<span style="color:#f92672">{</span>domainname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;yetiops.lab&#34;</span>,machine<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;amd64&#34;</span>,nodename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;freebsd-01&#34;</span>,release<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;12.1-RELEASE&#34;</span>,sysname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;FreeBSD&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;FreeBSD 12.1-RELEASE r354233 GENERIC &#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>
</code></pre></div><p>All looks good!</p>
<h3 id="gstat-exporter---deployment">Gstat Exporter - Deployment</h3>
<p>As part of this, we are also adding an additional exporter. The creator of the Gstat Exporter created <a href="https://github.com/tykling/ansible-roles/blob/master/prometheus_monitored_server/tasks/gstat_exporter.yml">Ansible Playbooks</a> to deploy Gstat Exporter, which are quite straightforward to adapt to Salt state files.</p>
<p>The only change I have made is creating an <strong>rc.d</strong> script rather than using <a href="http://supervisord.org/">supervisor</a>. This cuts down on required dependencies.</p>
<h4 id="states-1">States</h4>
<p>The following Salt state is used to deploy the Gstat Exporter on a FreeBSD host: -</p>
<p><strong>/srv/salt/states/exporters/gstat_exporter/freebsd.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">gstat_exporter_deps</span>:
  <span style="color:#66d9ef">pkg.installed</span>:
    - <span style="color:#66d9ef">pkgs</span>:
      - py37-pip
      - py37-setuptools
      - py37-virtualenv
      - git

<span style="color:#66d9ef">gstat_git_repo</span>:
  <span style="color:#66d9ef">git.cloned</span>:
    - <span style="color:#66d9ef">name</span>: https://github.com/tykling/gstat_exporter
    - <span style="color:#66d9ef">target</span>: /usr/local/gstat_exporter

<span style="color:#66d9ef">gstat_requirements</span>:
  <span style="color:#66d9ef">virtualenv.managed</span>:
    - <span style="color:#66d9ef">name</span>: /usr/local/gstat_exporter/venv
    - <span style="color:#66d9ef">requirements</span>: /usr/local/gstat_exporter/requirements.txt

<span style="color:#66d9ef">/usr/local/etc/rc.d/gstat_exporter</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://exporters/gstat_exporter/files/gstat_exporter-rcd
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0755</span>

<span style="color:#66d9ef">gstat_exporter</span>:
  <span style="color:#66d9ef">service.running</span>:
    - <span style="color:#66d9ef">enable</span>: <span style="color:#66d9ef">true</span>
</code></pre></div><p>First, we install our dependencies (Python3&rsquo;s PIP package manager, Python3 setuptools, Python3 Virtualenvs and Git).</p>
<p>After this, we clone the exporter&rsquo;s GitHub repository into <code>/usr/local/gstat_exporter</code>.</p>
<p>Next, we use the <code>virtualenv.managed</code> state to create a Python <a href="https://www.pythonforbeginners.com/basics/how-to-use-python-virtualenv/">virtualenv</a>. A <strong>virtualenv</strong> (or virtual environment) is a localised installation of Python, with its own packages and requirements installed. This means that you do not create conflicts with system packages, or version conflicts with other Python applications on your system. This also installs the Python3 modules that are specified in the repository&rsquo;s <code>requirements.txt</code> file inside the virtual environment.</p>
<p>We then add an <code>rc.d</code> configuration file, which is used to start and enable the <code>gstat_exporter</code> at runtime. The contents of this file are: -</p>
<p><strong>/srv/salt/states/exporters/gstat_exporter/files/gstat_exporter-rcd</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/bin/sh
</span><span style="color:#75715e"></span>#
<span style="color:#75715e"># PROVIDE: gstat_exporter</span>
<span style="color:#75715e"># REQUIRE: LOGIN NETWORKING</span>

. /etc/rc.subr

name<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;gstat_exporter&#34;</span>
rcvar<span style="color:#f92672">=</span><span style="color:#e6db74">${</span>name<span style="color:#e6db74">}</span>_enable

: <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>gstat_exporter_enable:=<span style="color:#e6db74">&#34;NO&#34;</span><span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>

command_interpreter<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/usr/local/gstat_exporter/venv/bin/python&#34;</span>
command<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/usr/local/gstat_exporter/gstat_exporter.py&#34;</span>
start_cmd<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/usr/sbin/daemon </span>$command_interpreter<span style="color:#e6db74"> </span>$command<span style="color:#e6db74">&#34;</span>
pidfile<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/var/run</span><span style="color:#e6db74">${</span>name<span style="color:#e6db74">}</span><span style="color:#e6db74">.pid&#34;</span>

load_rc_config <span style="color:#e6db74">${</span>name<span style="color:#e6db74">}</span>
run_rc_command <span style="color:#e6db74">&#34;</span>$1<span style="color:#e6db74">&#34;</span>
</code></pre></div><p>This file uses our <code>virtualenv</code>-installed Python binary to run the <code>gstat_exporter.py</code> application. We use the <code>/usr/sbin/daemon</code> command to ensure that it runs continuously in the background.</p>
<p>For more information on creating <code>rc.d</code> configuration scripts, see <a href="https://www.freebsd.org/doc/en/articles/rc-scripting/">here</a>.</p>
<p>Finally, we enable the <code>gstat_exporter</code> service, and run it.</p>
<p>This state is applied with the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">base</span>:
  <span style="color:#66d9ef">&#39;G@init:systemd and G@kernel:Linux&#39;</span>:
    - <span style="color:#66d9ef">match</span>: compound
    - consul
    - exporters.node_exporter.systemd

  <span style="color:#66d9ef">&#39;os:Alpine&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.alpine
    - exporters.node_exporter.alpine

  <span style="color:#66d9ef">&#39;os:Void&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.void
    - exporters.node_exporter.void

  <span style="color:#66d9ef">&#39;kernel:OpenBSD&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.openbsd
    - exporters.node_exporter.bsd

  <span style="color:#66d9ef">&#39;kernel:FreeBSD&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.freebsd
    - exporters.node_exporter.bsd
    - exporters.gstat_exporter.freebsd

  <span style="color:#66d9ef">&#39;kernel:Windows&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.windows
    - exporters.windows_exporter.win_exporter
    - exporters.windows_exporter.windows_exporter
</code></pre></div><h4 id="pillars-2">Pillars</h4>
<p>There are no pillars in this lab specific to the Gstat Exporter.</p>
<h3 id="gstat-exporter---verification">Gstat Exporter - Verification</h3>
<p>After this, we should be able to see the <code>gstat_exporter</code> running and producing metrics: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ps aux | grep -i gstat
root    <span style="color:#ae81ff">880</span>   0.0  2.1  <span style="color:#ae81ff">33076</span> <span style="color:#ae81ff">21280</span>  -  Ss   18:34    0:01.74 /usr/local/gstat_exporter/venv/bin/python /usr/local/gstat_exporter/gstat_exporter.py <span style="color:#f92672">(</span>python3.7<span style="color:#f92672">)</span>

$ netstat -an | grep -i <span style="color:#ae81ff">9248</span>
tcp4       <span style="color:#ae81ff">0</span>      <span style="color:#ae81ff">0</span> 10.15.31.21.9248       10.15.31.254.42722     TIME_WAIT
tcp4       <span style="color:#ae81ff">0</span>      <span style="color:#ae81ff">0</span> 10.15.31.21.9248       10.15.31.254.42718     TIME_WAIT
tcp4       <span style="color:#ae81ff">0</span>      <span style="color:#ae81ff">0</span> 10.15.31.21.9248       10.15.31.254.42708     TIME_WAIT
tcp4       <span style="color:#ae81ff">0</span>      <span style="color:#ae81ff">0</span> *.9248                 *.*                    LISTEN

$ curl 10.15.31.21:9248/metrics | grep -i up
gstat_up 1.0
</code></pre></div><p>All looks good!</p>
<h2 id="prometheus-targets">Prometheus Targets</h2>
<p>As Prometheus is already set up (see <a href="/posts/prometheus-consul-saltstack-part-1-linux/#configuring-prometheus">here</a>), and matches on the <code>prometheus</code> tag, we should see this within the Prometheus targets straight away: -</p>
<p><img src="/img/prometheus/freebsd-consul-targets.png" alt="FreeBSD Prometheus Consul Targets"></p>
<p>As you can see, both exporters (<code>node_exporter</code> and <code>gstat_exporter</code>) appear here.</p>
<p><img src="/img/prometheus/freebsd-consul-metadata.png" alt="FreeBSD Prometheus Consul Metadata"></p>
<p>Above is the Metadata we receive from Consul about this host. It appears twice, because we have two services, and therefore two sets of unique metadata (despite many duplicate labels).</p>
<h2 id="grafana">Grafana</h2>
<p>As with OpenBSD, load in FreeBSD is a measure of CPU usage only. Linux&rsquo;s load calculation is based upon many other factors, as noted in Brendan Gregg&rsquo;s <a href="http://www.brendangregg.com/blog/2017-08-08/linux-load-averages.html">Linux Load Averages</a> article.</p>
<p>With this being the case, using a standard Node Exporter dashboard would not be accurate. As mentioned in the OpenBSD post though, there is already a <a href="https://grafana.com/grafana/dashboards/4260">Node Exporter dashboard</a> for FreeBSD: -</p>
<p><img src="/img/prometheus/freebsd-node-exporter.png" alt="FreeBSD Node Exporter Dashboard"></p>
<p>The creator of the Gstat Exporter also made a Grafana <a href="https://grafana.com/grafana/dashboards/11223">dashboard</a> to go with it: -</p>
<p><img src="/img/prometheus/freebsd-gstat-exporter-dashboard.png" alt="FreeBSD Gstat Exporter Dashbaord"></p>
<h2 id="summary">Summary</h2>
<p>In this, we showed that FreeBSD can be managed and monitored using the same tools as Linux, Windows and OpenBSD. We also deployed another exporter without making any fundamental changes to our states.</p>
<p>Again, if you are running infrastructure across many different systems, automatically monitoring your systems and managing them consistently removes a lot of the barriers to adopting the right system for the right purpose. Choose OpenBSD for the security, FreeBSD for ZFS and jails, Windows for Active Directory and Linux for Docker containers, gives you a very capable infrastructure without the additional overhead of managing each separately.</p>
<p>In the next post in this series, we will cover how you deploy SaltStack on an illumos-based host (running OmniOS), which will then deploy Consul and the Prometheus Node Exporter.</p>
]]></content>
        </item>
        
        <item>
            <title>Prometheus - Using Service Discovery with Digital Ocean</title>
            <link>https://yetiops.net/posts/prometheus-service-discovery-digitalocean/</link>
            <pubDate>Mon, 29 Jun 2020 07:49:37 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/prometheus-service-discovery-digitalocean/</guid>
            <description>Recently I put together a post on using Prometheus to discover services within AWS, Azure and the Google Cloud Platform. Not long after publishing this post, I saw that service discovery for Digital Ocean is now available within Prometheus as well.
This feature is not in the published version as of yet (2.19.2 at the time of writing), so you will need to do one of the following: -
 If you are using Docker, use the :master tag (e.</description>
            <content type="html"><![CDATA[<p>Recently I put together a <a href="/posts/prometheus-service-discovery-aws-gcp-azure">post</a> on using Prometheus to discover services within AWS, Azure and the Google Cloud Platform. Not long after publishing this post, I saw that  <a href="https://github.com/prometheus/prometheus/tree/master/discovery/digitalocean">service discovery for Digital Ocean</a> is now available within Prometheus as well.</p>
<p>This feature is not in the published version as of yet (2.19.2 at the time of writing), so you will need to do one of the following: -</p>
<ul>
<li>If you are using Docker, use the <code>:master</code> tag (e.g. <code>docker pull prom/prometheus:master</code>)</li>
<li>Build it from source</li>
</ul>
<p>Because this feature is not in a general released version, the service discovery mechanism may change at a later date. If it does, I will update this post to reflect that.</p>
<h2 id="building-from-source">Building from source</h2>
<p>To build Prometheus from source, you will need to make sure you have a working Golang (v1.13 or above) environment, as well as installing NodeJS and Yarn. To prepare an Ubuntu 20.04 instance for this, do the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># Install Golang</span>
$ apt install golang

<span style="color:#75715e"># Install NodeJS and NPM (Node Package Manager)</span>
$ apt install nodejs npm

<span style="color:#75715e"># Install Yarn</span>
$ npm install -g npm

<span style="color:#75715e"># Set your GOPATH</span>
$ mkdir ~/go
$ export GOPATH<span style="color:#f92672">=</span>~/go
</code></pre></div><p>After this, you can follow the instructions provided on the <a href="https://github.com/prometheus/prometheus">Prometheus GitHub README</a>. These are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ mkdir -p $GOPATH/src/github.com/prometheus
$ cd $GOPATH/src/github.com/prometheus
$ git clone https://github.com/prometheus/prometheus.git
$ cd prometheus
$ make build
</code></pre></div><p>This process generates all the web assets (using NodeJS and Yarn), as well as injecting them into the Golang build process. This makes the binary portable (i.e. the web assets are part of the binary, rather than in a static path). The output of the <code>make build</code> command is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">cd web/ui/react-app <span style="color:#f92672">&amp;&amp;</span> yarn --frozen-lockfile
yarn install v1.22.4
<span style="color:#f92672">[</span>1/4<span style="color:#f92672">]</span> Resolving packages...
<span style="color:#f92672">[</span>2/4<span style="color:#f92672">]</span> Fetching packages...
info fsevents@2.1.2: The platform <span style="color:#e6db74">&#34;linux&#34;</span> is incompatible with this module.
info <span style="color:#e6db74">&#34;fsevents@2.1.2&#34;</span> is an optional dependency and failed compatibility check. Excluding it from installation.
info fsevents@1.2.12: The platform <span style="color:#e6db74">&#34;linux&#34;</span> is incompatible with this module.
info <span style="color:#e6db74">&#34;fsevents@1.2.12&#34;</span> is an optional dependency and failed compatibility check. Excluding it from installation.
<span style="color:#f92672">[</span>3/4<span style="color:#f92672">]</span> Linking dependencies...
warning <span style="color:#e6db74">&#34; &gt; eslint-config-react-app@5.2.1&#34;</span> has unmet peer dependency <span style="color:#e6db74">&#34;babel-eslint@10.x&#34;</span>.
<span style="color:#f92672">[</span>4/4<span style="color:#f92672">]</span> Building fresh packages...
Done in 47.65s.
&gt;&gt; building React app
building React app
yarn run v1.22.4
$ react-scripts build
Creating an optimized production build...
Compiled successfully.

File sizes after gzip:

  260.61 KB  build/static/js/2.dfe05a07.chunk.js
  29.67 KB   build/static/js/main.3085a125.chunk.js
  23.27 KB   build/static/css/2.df42c974.chunk.css
  1.5 KB     build/static/css/main.0b010d50.chunk.css
  <span style="color:#ae81ff">770</span> B      build/static/js/runtime-main.5db206b5.js

The project was built assuming it is hosted at ./.
You can control this with the homepage field in your package.json.

The build folder is ready to be deployed.

Find out more about deployment here:

  bit.ly/CRA-deploy

Done in 70.80s.
&gt;&gt; writing assets
<span style="color:#75715e"># Un-setting GOOS and GOARCH here because the generated Go code is always the same,</span>
<span style="color:#75715e"># but the cached object code is incompatible between architectures and OSes (which</span>
<span style="color:#75715e"># breaks cross-building for different combinations on CI in the same container).</span>
cd web/ui <span style="color:#f92672">&amp;&amp;</span> GO111MODULE<span style="color:#f92672">=</span>on GOOS<span style="color:#f92672">=</span> GOARCH<span style="color:#f92672">=</span> go generate -x -v  -mod<span style="color:#f92672">=</span>vendor
doc.go
go run -mod<span style="color:#f92672">=</span>vendor assets_generate.go
writing assets_vfsdata.go
ui.go
curl -s -L https://github.com/prometheus/promu/releases/download/v0.5.0/promu-0.5.0.linux-amd64.tar.gz | tar -xvzf - -C /tmp/tmp.Zr9ksLsvhC
promu-0.5.0.linux-amd64/
promu-0.5.0.linux-amd64/promu
promu-0.5.0.linux-amd64/NOTICE
promu-0.5.0.linux-amd64/LICENSE
mkdir -p /home/$USER/go/bin
cp /tmp/tmp.Zr9ksLsvhC/promu-0.5.0.linux-amd64/promu /home/$USER/go/bin/promu
rm -r /tmp/tmp.Zr9ksLsvhC
&gt;&gt; building binaries
GO111MODULE<span style="color:#f92672">=</span>on /home/$USER/go/bin/promu build --prefix /home/$USER/go/src/github.com/prometheus/prometheus
 &gt;   prometheus
 &gt;   promtool
 &gt;   tsdb
</code></pre></div><p>This will generate the <code>prometheus</code> binary, as well as <code>promtool</code>. You can use this <code>prometheus</code> binary in place of your existing one. For me, this would be <code>/usr/local/bin/prometheus</code>.</p>
<p>Do note that as this is an unreleased version, you may encounter bugs that are not in the official published versions.</p>
<h2 id="terraform">Terraform</h2>
<p>Now that we have a Prometheus binary that can discover services in DigitalOcean, we can define our Digital Ocean resources in Terraform. For information on how to install Terraform and the project structure, see <a href="/posts/prometheus-service-discovery-aws-gcp-azure/#configure-terraform">here</a>.</p>
<h2 id="digital-ocean">Digital Ocean</h2>
<p>Digital Ocean offer a variety of services, from cloud instances (virtual machines), databases, object storage, load balancers and managed Kubernetes. While they the size of AWS, Azure or Google Cloud Platform, they still have a significant user base (myself being one of them!)</p>
<p>Cloud Instances within Digital Ocean are known as Droplets. The smallest Droplet comes with 1 vCPU and 1G of memory, with the largest at 32 vCPUs and 192G of memory.</p>
<p>You can sign up for a DigitalOcean account <a href="https://cloud.digitalocean.com/registrations/new">here</a>. As a point of note, if you listen to podcasts in the Linux and Open Source community, you&rsquo;ll probably hear offers to get $100 of credit for 60 days, so be sure to use one of these if you can.</p>
<p>Once you have signed up, you&rsquo;ll be asked to create a project. This is a container for your resources, allowing you to group them together as you see fit: -</p>
<p><img src="/img/prometheus/do-new-project.png" alt="Digital Ocean Project"></p>
<h3 id="create-an-api-key">Create an API key</h3>
<p>Terraform uses Digital Ocean API keys to authenticate and provision resources. You can generate a key in the <strong>API</strong> section of the <a href="https://cloud.digitalocean.com/account/api/">Digital Ocean Cloud Console</a>: -</p>
<p><img src="/img/prometheus/do-api-token.png" alt="Digital Ocean API Key"></p>
<p>You can choose to either: -</p>
<ul>
<li>Configure the API token as a variable in Terraform</li>
<li>Expose it as an environment variable</li>
</ul>
<p>To configure it as a variable in Terraform, do something like the following (taken from the Terraform <a href="https://www.terraform.io/docs/providers/do/index.html">Digital Ocean Provider</a> page): -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#75715e"># Set the variable value in *.tfvars file
</span><span style="color:#75715e"># or using -var=&#34;do_token=...&#34; CLI option
</span><span style="color:#75715e"></span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;do_token&#34;</span> {
  type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;string&#34;</span>
  default <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;$TOKEN_GOES_HERE&#34;</span>
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e"># Configure the DigitalOcean Provider
</span><span style="color:#75715e"></span><span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;digitalocean&#34;</span> {
  token <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">do_token</span>
}
</code></pre></div><p>Alternatively, Terraform will use the environment variables <code>$DIGITALOCEAN_TOKEN</code> or <code>$DIGITALOCEAN_ACCESS_TOKEN</code>. You can set this with <code>export DIGITALOCEAN_TOKEN=&quot;###API-KEY###</code> or you can place them in your <code>.bashrc</code> or <code>.zshrc</code> to be loaded when you open a terminal.</p>
<h3 id="configure-terraform---droplets">Configure Terraform - Droplets</h3>
<p>Now that we have an API key created and available, we can use Terraform with Digital Ocean.</p>
<p>In your chosen directory to define your infrastructure (I am using <code>~/terraform/basic-vms</code> for this), create a <code>providers.tf</code> file that contains the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#75715e"># Digital Ocean Provider
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;digitalocean&#34;</span> {
}
</code></pre></div><p>The above discovers the Digital Ocean API Key from our environment variables (<code>$DIGITALOCEAN_TOKEN</code> in my case). No other details are required at this stage.</p>
<p>After this, run <code>terraform init</code>. This downloads the Digital Ocean Terraform provider binary: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">Initializing the backend...

Initializing provider plugins...
- Checking <span style="color:#66d9ef">for</span> available provider plugins...
- Downloading plugin <span style="color:#66d9ef">for</span> provider <span style="color:#e6db74">&#34;digitalocean&#34;</span> <span style="color:#f92672">(</span>terraform-providers/digitalocean<span style="color:#f92672">)</span> 1.20.0...
- Downloading plugin <span style="color:#66d9ef">for</span> provider <span style="color:#e6db74">&#34;template&#34;</span> <span style="color:#f92672">(</span>hashicorp/template<span style="color:#f92672">)</span> 2.1.2...

The following providers <span style="color:#66d9ef">do</span> not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;...&#34;</span> constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* provider.digitalocean: version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;~&gt; 1.20&#34;</span>
* provider.template: version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;~&gt; 2.1&#34;</span>

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running <span style="color:#e6db74">&#34;terraform plan&#34;</span> to see
any changes that are required <span style="color:#66d9ef">for</span> your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration <span style="color:#66d9ef">for</span> Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to <span style="color:#66d9ef">do</span> so <span style="color:#66d9ef">if</span> necessary.
</code></pre></div><p>The template provider is because we use it for <code>user-data</code> (i.e. first-time boot configuration). This is covered in the <a href="/posts/prometheus-service-discovery-aws-gcp-azure/#configure-terraform---ec2s">AWS EC2s section of the previous post on Cloud service discovery</a>.</p>
<h3 id="define-the-infrastructure---droplets">Define the infrastructure - Droplets</h3>
<p>You can now create the configuration files for your first Droplet. The below is from the file <code>do.tf</code> in the <code>terraform/basic-vms</code> directory: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;digitalocean_droplet&#34; &#34;yetiops-prom-vm&#34;</span> {
  image              <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ubuntu-20-04-x64&#34;</span>
  name               <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-prom-vm&#34;</span>
  region             <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;fra1&#34;</span>
  size               <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;s-1vcpu-1gb&#34;</span>
  ssh_keys           <span style="color:#f92672">=</span> [<span style="color:#66d9ef">digitalocean_ssh_key</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">ssh</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">key</span>.<span style="color:#66d9ef">fingerprint</span>]

  tags <span style="color:#f92672">=</span> [
    <span style="color:#66d9ef">digitalocean_tag</span>.<span style="color:#66d9ef">prometheus</span>.<span style="color:#66d9ef">id</span>,
    <span style="color:#66d9ef">digitalocean_tag</span>.<span style="color:#66d9ef">node_exporter</span>.<span style="color:#66d9ef">id</span>
  ]

  user_data          <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">template_file</span>.<span style="color:#66d9ef">ubuntu</span>.<span style="color:#66d9ef">template</span>

}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;digitalocean_tag&#34; &#34;prometheus&#34;</span> {
  name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;prometheus&#34;</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;digitalocean_tag&#34; &#34;node_exporter&#34;</span> {
  name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;node_exporter&#34;</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;digitalocean_ssh_key&#34; &#34;yetiops-ssh-key&#34;</span> {
  name       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;SSH Key&#34;</span>
  public_key <span style="color:#f92672">=</span> <span style="color:#66d9ef">file</span>(<span style="color:#e6db74">&#34;~/.ssh/id_ed25519.pub&#34;</span>)
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;digitalocean_firewall&#34; &#34;yetiops-prom-vm&#34;</span> {
  name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-prom-vm&#34;</span>

  droplet_ids <span style="color:#f92672">=</span> [<span style="color:#66d9ef">digitalocean_droplet</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">vm</span>.<span style="color:#66d9ef">id</span>]

  <span style="color:#66d9ef">inbound_rule</span> {
    protocol         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
    port_range       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;22&#34;</span>
    source_addresses <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;$MY_PUBLIC_IP/32&#34;</span>]
  }

  <span style="color:#66d9ef">inbound_rule</span> {
    protocol         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
    port_range       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;9100&#34;</span>
    source_addresses <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;$MY_PUBLIC_IP/32&#34;</span>]
  }

  <span style="color:#66d9ef">inbound_rule</span> {
    protocol         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;icmp&#34;</span>
    source_addresses <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;0.0.0.0/0&#34;, &#34;::/0&#34;</span>]
  }

  <span style="color:#66d9ef">outbound_rule</span> {
    protocol              <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;icmp&#34;</span>
    destination_addresses <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;0.0.0.0/0&#34;, &#34;::/0&#34;</span>]
  }

  <span style="color:#66d9ef">outbound_rule</span> {
    protocol              <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
    port_range            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;1-65535&#34;</span>
    destination_addresses <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;0.0.0.0/0&#34;, &#34;::/0&#34;</span>]
  }

  <span style="color:#66d9ef">outbound_rule</span> {
    protocol              <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;udp&#34;</span>
    port_range            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;1-65535&#34;</span>
    destination_addresses <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;0.0.0.0/0&#34;, &#34;::/0&#34;</span>]
  }
}
</code></pre></div><p>To summarize what we are doing here, we are: -</p>
<ul>
<li>Creating a Digital Ocean Droplet, running Ubuntu 20.04, of size <code>s-1vcpu-1gb</code> (1 vCPU, 1G of memory)</li>
<li>Using the Ubuntu template file for <code>user-data</code> (which is a <strong>cloud-config</strong> file that installs the Prometheus Node Exporter and nothing more)</li>
<li>Creating two tags (<code>prometheus</code> and <code>node_exporter</code>) and attaching them to the Droplet</li>
<li>Adding a local SSH key to Digital Ocean so that we can SSH into the instance once it is provisioned</li>
<li>Creating a firewall that allows SSH and the Node Exporter port inbound from my public IP, any ICMP (i.e. Ping) traffic, and all outbound traffic</li>
</ul>
<p>Unlike tags in AWS and Azure, and labels in Google Cloud Platform, Digital Ocean tags are not <code>$KEY:$VALUE</code> based (i.e. <code>prometheus: true</code>). This is something to be aware of when configuring Prometheus later.</p>
<h3 id="build-the-infrastructure---droplets">Build the infrastructure - Droplets</h3>
<p>We can now apply our configuration, and see if it builds a Digital Ocean Droplet: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ terraform apply
data.template_file.ubuntu: Refreshing state...
data.template_cloudinit_config.ubuntu: Refreshing state...

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  <span style="color:#75715e"># digitalocean_droplet.yetiops-prom-vm will be created</span>
  + resource <span style="color:#e6db74">&#34;digitalocean_droplet&#34;</span> <span style="color:#e6db74">&#34;yetiops-prom-vm&#34;</span> <span style="color:#f92672">{</span>
      + backups              <span style="color:#f92672">=</span> false
      + created_at           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + disk                 <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id                   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + image                <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ubuntu-20-04-x64&#34;</span>
      + ipv4_address         <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + ipv4_address_private <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + ipv6                 <span style="color:#f92672">=</span> false
      + ipv6_address         <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + ipv6_address_private <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + locked               <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + memory               <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + monitoring           <span style="color:#f92672">=</span> false
      + name                 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-prom-vm&#34;</span>
      + price_hourly         <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + price_monthly        <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + private_networking   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + region               <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;fra1&#34;</span>
      + resize_disk          <span style="color:#f92672">=</span> true
      + size                 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;s-1vcpu-1gb&#34;</span>
      + ssh_keys             <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + status               <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + tags                 <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + urn                  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + user_data            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;2169d8a3e100623d34bf1a7b2f6bd924a8997bfb&#34;</span>
      + vcpus                <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + volume_ids           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + vpc_uuid             <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># digitalocean_firewall.yetiops-prom-vm will be created</span>
  + resource <span style="color:#e6db74">&#34;digitalocean_firewall&#34;</span> <span style="color:#e6db74">&#34;yetiops-prom-vm&#34;</span> <span style="color:#f92672">{</span>
      + created_at      <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + droplet_ids     <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id              <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + name            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-prom-vm&#34;</span>
      + pending_changes <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + status          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>

      + inbound_rule <span style="color:#f92672">{</span>
          + protocol                  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;icmp&#34;</span>
          + source_addresses          <span style="color:#f92672">=</span> <span style="color:#f92672">[</span>
              + <span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>,
              + <span style="color:#e6db74">&#34;::/0&#34;</span>,
            <span style="color:#f92672">]</span>
          + source_droplet_ids        <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
          + source_load_balancer_uids <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
          + source_tags               <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
        <span style="color:#f92672">}</span>
      + inbound_rule <span style="color:#f92672">{</span>
          + port_range                <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;22&#34;</span>
          + protocol                  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
          + source_addresses          <span style="color:#f92672">=</span> <span style="color:#f92672">[</span>
              + <span style="color:#e6db74">&#34;</span>$MY_PUBLIC_IP<span style="color:#e6db74">/32&#34;</span>,
            <span style="color:#f92672">]</span>
          + source_droplet_ids        <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
          + source_load_balancer_uids <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
          + source_tags               <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
        <span style="color:#f92672">}</span>
      + inbound_rule <span style="color:#f92672">{</span>
          + port_range                <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;9100&#34;</span>
          + protocol                  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
          + source_addresses          <span style="color:#f92672">=</span> <span style="color:#f92672">[</span>
              + <span style="color:#e6db74">&#34;</span>$MY_PUBLIC_IP<span style="color:#e6db74">/32&#34;</span>,
            <span style="color:#f92672">]</span>
          + source_droplet_ids        <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
          + source_load_balancer_uids <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
          + source_tags               <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
        <span style="color:#f92672">}</span>

      + outbound_rule <span style="color:#f92672">{</span>
          + destination_addresses          <span style="color:#f92672">=</span> <span style="color:#f92672">[</span>
              + <span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>,
              + <span style="color:#e6db74">&#34;::/0&#34;</span>,
            <span style="color:#f92672">]</span>
          + destination_droplet_ids        <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
          + destination_load_balancer_uids <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
          + destination_tags               <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
          + protocol                       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;icmp&#34;</span>
        <span style="color:#f92672">}</span>
      + outbound_rule <span style="color:#f92672">{</span>
          + destination_addresses          <span style="color:#f92672">=</span> <span style="color:#f92672">[</span>
              + <span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>,
              + <span style="color:#e6db74">&#34;::/0&#34;</span>,
            <span style="color:#f92672">]</span>
          + destination_droplet_ids        <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
          + destination_load_balancer_uids <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
          + destination_tags               <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
          + port_range                     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;1-65535&#34;</span>
          + protocol                       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
        <span style="color:#f92672">}</span>
      + outbound_rule <span style="color:#f92672">{</span>
          + destination_addresses          <span style="color:#f92672">=</span> <span style="color:#f92672">[</span>
              + <span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>,
              + <span style="color:#e6db74">&#34;::/0&#34;</span>,
            <span style="color:#f92672">]</span>
          + destination_droplet_ids        <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
          + destination_load_balancer_uids <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
          + destination_tags               <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
          + port_range                     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;1-65535&#34;</span>
          + protocol                       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;udp&#34;</span>
        <span style="color:#f92672">}</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># digitalocean_ssh_key.yetiops-ssh-key will be created</span>
  + resource <span style="color:#e6db74">&#34;digitalocean_ssh_key&#34;</span> <span style="color:#e6db74">&#34;yetiops-ssh-key&#34;</span> <span style="color:#f92672">{</span>
      + fingerprint <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + name        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;SSH Key&#34;</span>
      + public_key  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;</span>$SSH_PUBLIC_KEY_CONTENTS<span style="color:#e6db74">&#34;</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># digitalocean_tag.node_exporter will be created</span>
  + resource <span style="color:#e6db74">&#34;digitalocean_tag&#34;</span> <span style="color:#e6db74">&#34;node_exporter&#34;</span> <span style="color:#f92672">{</span>
      + databases_count        <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + droplets_count         <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id                     <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + images_count           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + name                   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;node_exporter&#34;</span>
      + total_resource_count   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + volume_snapshots_count <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + volumes_count          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># digitalocean_tag.prometheus will be created</span>
  + resource <span style="color:#e6db74">&#34;digitalocean_tag&#34;</span> <span style="color:#e6db74">&#34;prometheus&#34;</span> <span style="color:#f92672">{</span>
      + databases_count        <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + droplets_count         <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id                     <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + images_count           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + name                   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;prometheus&#34;</span>
      + total_resource_count   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + volume_snapshots_count <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + volumes_count          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
    <span style="color:#f92672">}</span>

Plan: <span style="color:#ae81ff">5</span> to add, <span style="color:#ae81ff">0</span> to change, <span style="color:#ae81ff">0</span> to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only <span style="color:#e6db74">&#39;yes&#39;</span> will be accepted to approve.

  Enter a value: yes

digitalocean_tag.prometheus: Creating...
digitalocean_tag.node_exporter: Creating...
digitalocean_ssh_key.yetiops-ssh-key: Creating...
digitalocean_tag.prometheus: Creation complete after 1s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>prometheus<span style="color:#f92672">]</span>
digitalocean_ssh_key.yetiops-ssh-key: Creation complete after 1s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>27810953<span style="color:#f92672">]</span>
digitalocean_tag.node_exporter: Creation complete after 1s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>node_exporter<span style="color:#f92672">]</span>
digitalocean_droplet.yetiops-prom-vm: Creating...
digitalocean_droplet.yetiops-prom-vm: Still creating... <span style="color:#f92672">[</span>10s elapsed<span style="color:#f92672">]</span>
digitalocean_droplet.yetiops-prom-vm: Still creating... <span style="color:#f92672">[</span>20s elapsed<span style="color:#f92672">]</span>
digitalocean_droplet.yetiops-prom-vm: Still creating... <span style="color:#f92672">[</span>30s elapsed<span style="color:#f92672">]</span>
digitalocean_droplet.yetiops-prom-vm: Creation complete after 35s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>197944957<span style="color:#f92672">]</span>
digitalocean_firewall.yetiops-prom-vm: Creating...
digitalocean_firewall.yetiops-prom-vm: Creation complete after 0s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>180b6bd2-ee0c-4649-bae5-9cb2d3db6473<span style="color:#f92672">]</span>

Apply complete! Resources: <span style="color:#ae81ff">5</span> added, <span style="color:#ae81ff">0</span> changed, <span style="color:#ae81ff">0</span> destroyed.
</code></pre></div><p>We can double check that Terraform is managing these resources now with <code>terraform state list</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ terraform state list
data.template_cloudinit_config.ubuntu
data.template_file.ubuntu
digitalocean_droplet.yetiops-prom-vm
digitalocean_firewall.yetiops-prom-vm
digitalocean_ssh_key.yetiops-ssh-key
digitalocean_tag.node_exporter
digitalocean_tag.prometheus
</code></pre></div><p>We can check to see if the instance is in the Digital Ocean Console: -</p>
<p><img src="/img/prometheus/do-droplet.png" alt="Digital Ocean Droplet"></p>
<p>Now lets try SSH: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ssh root@$DROPLET_PUBLIC_IP
Welcome to Ubuntu 20.04 LTS <span style="color:#f92672">(</span>GNU/Linux 5.4.0-29-generic x86_64<span style="color:#f92672">)</span>

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Mon Jun <span style="color:#ae81ff">29</span> 06:48:28 UTC <span style="color:#ae81ff">2020</span>

  System load:  0.13              Processes:             <span style="color:#ae81ff">103</span>
  Usage of /:   5.6% of 24.06GB   Users logged in:       <span style="color:#ae81ff">0</span>
  Memory usage: 19%               IPv4 address <span style="color:#66d9ef">for</span> eth0: $PUBLIC_IP 
  Swap usage:   0%                IPv4 address <span style="color:#66d9ef">for</span> eth0: 10.19.0.5

<span style="color:#ae81ff">65</span> updates can be installed immediately.
<span style="color:#ae81ff">29</span> of these updates are security updates.
To see these additional updates run: apt list --upgradable



The programs included with the Ubuntu system are free software;
the exact distribution terms <span style="color:#66d9ef">for</span> each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

root@yetiops-prom-vm:~# ps aux | grep -i node
prometh+    <span style="color:#ae81ff">2266</span>  0.0  1.0 <span style="color:#ae81ff">336924</span> <span style="color:#ae81ff">10468</span> ?        Ssl  06:44   0:00 /usr/bin/prometheus-node-exporter
root       <span style="color:#ae81ff">14312</span>  0.0  0.0   <span style="color:#ae81ff">8156</span>   <span style="color:#ae81ff">672</span> pts/0    S+   06:48   0:00 grep --color<span style="color:#f92672">=</span>auto -i node
</code></pre></div><h3 id="configure-terraform---prometheus-user">Configure Terraform - Prometheus User</h3>
<p>Digital Ocean have the option of creating applications with limited permissions, but it appears this is for Oauth2-based applications. Standard access tokens do not get the same level of fine grained permissions. With this being the case, we do not create a Prometheus user in Terraform. Instead, follow the steps above for creating an API key for Prometheus.</p>
<h2 id="prometheus">Prometheus</h2>
<p>Now that we have our Droplet in Digital Ocean, we can configure our Prometheus instance. I am using an Ubuntu 20.04 virtual machine in my lab for this.</p>
<h3 id="digital-ocean-service-discovery">Digital Ocean Service Discovery</h3>
<p>To allow Prometheus to discover instances in Digital Ocean, use configuration like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">global</span>:
  <span style="color:#66d9ef">scrape_interval</span>:     15s
  <span style="color:#66d9ef">evaluation_interval</span>: 15s

<span style="color:#66d9ef">scrape_configs</span>:
  - <span style="color:#66d9ef">job_name</span>: <span style="color:#e6db74">&#39;prometheus&#39;</span>
    <span style="color:#66d9ef">static_configs</span>:
    - <span style="color:#66d9ef">targets</span>:
      - <span style="color:#e6db74">&#39;localhost:9090&#39;</span>
  - <span style="color:#66d9ef">job_name</span>: <span style="color:#e6db74">&#39;do-nodes&#39;</span>
    <span style="color:#66d9ef">digitalocean_sd_configs</span>:
      - <span style="color:#66d9ef">bearer_token</span>: <span style="color:#e6db74">&#39;$DIGITAL_OCEAN_PROMETHEUS_API_TOKEN&#39;</span>
        <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">9100</span>
    <span style="color:#66d9ef">relabel_configs</span>:
      - <span style="color:#66d9ef">source_labels</span>: [__meta_digitalocean_tags]
        <span style="color:#66d9ef">regex</span>: .<span style="color:#75715e">*,prometheus,.*</span>
        <span style="color:#66d9ef">action</span>: keep
      - <span style="color:#66d9ef">source_labels</span>: [__meta_digitalocean_tags]
        <span style="color:#66d9ef">regex</span>: .<span style="color:#75715e">*,node_exporter,.*</span>
        <span style="color:#66d9ef">action</span>: keep
</code></pre></div><p>Replace <code>$DIGITAL_OCEAN_PROMETHEUS_API_TOKEN</code> with the token created <a href="/prometheus-service-discovery-digitalocean/#configure-terraform---prometheus-user">above</a>.</p>
<p>Notice that we have a <code>digitalocean_tags</code> metadata field, rather than each tag having its own field. Here is the AWS EC2 relabelling configuration for comparison: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">    <span style="color:#66d9ef">relabel_configs</span>:
      - <span style="color:#66d9ef">source_labels</span>: [__meta_ec2_tag_prometheus]
        <span style="color:#66d9ef">regex</span>: <span style="color:#66d9ef">true</span>.*
        <span style="color:#66d9ef">action</span>: keep
      - <span style="color:#66d9ef">source_labels</span>: [__meta_ec2_tag_node_exporter]
        <span style="color:#66d9ef">regex</span>: <span style="color:#66d9ef">true</span>.*
        <span style="color:#66d9ef">action</span>: keep
</code></pre></div><p>Because of this, rather than looking for the value <code>true</code>, we are using a regular expression to filter the contents of the <code>tags</code> field itself (looking for <code>prometheus</code> and <code>node_exporter</code>). If you run another exporter, like the <a href="https://github.com/prometheus/haproxy_exporter">HAProxy Exporter</a>, you could use something like the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl">  <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">job_name</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">do</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">nodes</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">haproxy</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
    <span style="color:#66d9ef">digitalocean_sd_configs</span><span style="color:#960050;background-color:#1e0010">:</span>
      <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">bearer_token</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;$</span><span style="color:#66d9ef">DIGITAL_OCEAN_PROMETHEUS_API_TOKEN</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
        <span style="color:#66d9ef">port</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#ae81ff">9101</span>
    <span style="color:#66d9ef">relabel_configs</span><span style="color:#960050;background-color:#1e0010">:</span>
      <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#66d9ef">__meta_digitalocean_tags</span>]
        <span style="color:#66d9ef">regex</span><span style="color:#960050;background-color:#1e0010">:</span> .<span style="color:#960050;background-color:#1e0010">*</span>,<span style="color:#66d9ef">prometheus</span>,.<span style="color:#960050;background-color:#1e0010">*</span>
        <span style="color:#66d9ef">action</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">keep</span>
      <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#66d9ef">__meta_digitalocean_tags</span>]
        <span style="color:#66d9ef">regex</span><span style="color:#960050;background-color:#1e0010">:</span> .<span style="color:#960050;background-color:#1e0010">*</span>,<span style="color:#66d9ef">haproxy_exporter</span>,.<span style="color:#960050;background-color:#1e0010">*</span>
        <span style="color:#66d9ef">action</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">keep</span>
</code></pre></div><p>The Digital Ocean Service Discovery uses the Public IPv4 address of the Digital Ocean Droplet by default to target (unlike AWS, Azure or Google Cloud Platform, which uses the private IPv4). Because of this, we do not need to do any relabelling to reach it from an external source. Digital Ocean do have VPCs, so if you decide to run your Prometheus instance within Digital Ocean, you will want to relabel the target address to use the Private IPv4 address instead.</p>
<p>We can now look at the labels that the Digital Ocean Service Discovery generates: -</p>
<p><img src="/img/prometheus/do-droplet-sd.png" alt="Digital Ocean Droplet Service Discovery"></p>
<p>Notice the tags field which contains all of the tags we have assigned (and hence what we match upon).</p>
<p>Can we reach the <code>node_exporter</code> on the Droplet?</p>
<p><img src="/img/prometheus/do-droplet-target.png" alt="Digital Ocean Droplet Target"></p>
<p>Yes we can, brilliant!</p>
<h2 id="more-efficient-service-discovery">More efficient service discovery</h2>
<p><a href="https://twitter.com/roidelapluie">Julien Pivotto</a> (one of the contributors to Prometheus) made me aware that there is a more efficient way of using Prometheus Service Discovery with multiple targets</p>
<p>The approach in this post looks something like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl">  <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">job_name</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">do</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">nodes</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
    <span style="color:#66d9ef">digitalocean_sd_configs</span><span style="color:#960050;background-color:#1e0010">:</span>
      <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">bearer_token</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;$</span><span style="color:#66d9ef">DIGITAL_OCEAN_PROMETHEUS_API_TOKEN</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
        <span style="color:#66d9ef">port</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#ae81ff">9100</span>
    <span style="color:#66d9ef">relabel_configs</span><span style="color:#960050;background-color:#1e0010">:</span>
      <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#66d9ef">__meta_digitalocean_tags</span>]
        <span style="color:#66d9ef">regex</span><span style="color:#960050;background-color:#1e0010">:</span> .<span style="color:#960050;background-color:#1e0010">*</span>,<span style="color:#66d9ef">prometheus</span>,.<span style="color:#960050;background-color:#1e0010">*</span>
        <span style="color:#66d9ef">action</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">keep</span>
      <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#66d9ef">__meta_digitalocean_tags</span>]
        <span style="color:#66d9ef">regex</span><span style="color:#960050;background-color:#1e0010">:</span> .<span style="color:#960050;background-color:#1e0010">*</span>,<span style="color:#66d9ef">node_exporter</span>,.<span style="color:#960050;background-color:#1e0010">*</span>
        <span style="color:#66d9ef">action</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">keep</span>
  <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">job_name</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">do</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">nodes</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">haproxy</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
    <span style="color:#66d9ef">digitalocean_sd_configs</span><span style="color:#960050;background-color:#1e0010">:</span>
      <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">bearer_token</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;$</span><span style="color:#66d9ef">DIGITAL_OCEAN_PROMETHEUS_API_TOKEN</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
        <span style="color:#66d9ef">port</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#ae81ff">9101</span>
    <span style="color:#66d9ef">relabel_configs</span><span style="color:#960050;background-color:#1e0010">:</span>
      <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#66d9ef">__meta_digitalocean_tags</span>]
        <span style="color:#66d9ef">regex</span><span style="color:#960050;background-color:#1e0010">:</span> .<span style="color:#960050;background-color:#1e0010">*</span>,<span style="color:#66d9ef">prometheus</span>,.<span style="color:#960050;background-color:#1e0010">*</span>
        <span style="color:#66d9ef">action</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">keep</span>
      <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#66d9ef">__meta_digitalocean_tags</span>]
        <span style="color:#66d9ef">regex</span><span style="color:#960050;background-color:#1e0010">:</span> .<span style="color:#960050;background-color:#1e0010">*</span>,<span style="color:#66d9ef">haproxy_exporter</span>,.<span style="color:#960050;background-color:#1e0010">*</span>
        <span style="color:#66d9ef">action</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">keep</span>
</code></pre></div><p>In the first job we make the relevant API calls to the Digital Ocean API to retrieve the configured Droplets. When we get to the second job, we make the same API calls again to retrieve the configured Droplets. The more jobs you have, the more API calls Prometheus would make.</p>
<p>Instead, you can use something like: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl">  <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">job_name</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">do</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">nodes</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
    <span style="color:#66d9ef">digitalocean_sd_configs</span><span style="color:#960050;background-color:#1e0010">:</span>
      <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">bearer_token</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;$</span><span style="color:#66d9ef">DIGITAL_OCEAN_PROMETHEUS_API_TOKEN</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
    <span style="color:#66d9ef">relabel_configs</span><span style="color:#960050;background-color:#1e0010">:</span>
      <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#66d9ef">__meta_digitalocean_tags</span>]
        <span style="color:#66d9ef">regex</span><span style="color:#960050;background-color:#1e0010">:</span> .<span style="color:#960050;background-color:#1e0010">*</span>,<span style="color:#66d9ef">prometheus</span>,.<span style="color:#960050;background-color:#1e0010">*</span>
        <span style="color:#66d9ef">action</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">keep</span>
      <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#66d9ef">__meta_digitalocean_tags</span>]
        <span style="color:#66d9ef">regex</span><span style="color:#960050;background-color:#1e0010">:</span> .<span style="color:#960050;background-color:#1e0010">*</span>,<span style="color:#66d9ef">node_exporter</span>,.<span style="color:#960050;background-color:#1e0010">*</span>
        <span style="color:#66d9ef">action</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">keep</span>
      <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#66d9ef">__meta_digitalocean_public_ipv4</span>]
        <span style="color:#66d9ef">target_label</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">__address__</span>
        <span style="color:#66d9ef">replacement</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;$</span><span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">:</span><span style="color:#ae81ff">9100</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
  <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">job_name</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">do</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">nodes</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">haproxy</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
    <span style="color:#66d9ef">digitalocean_sd_configs</span><span style="color:#960050;background-color:#1e0010">:</span>
      <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">bearer_token</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;$</span><span style="color:#66d9ef">DIGITAL_OCEAN_PROMETHEUS_API_TOKEN</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
    <span style="color:#66d9ef">relabel_configs</span><span style="color:#960050;background-color:#1e0010">:</span>
      <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#66d9ef">__meta_digitalocean_tags</span>]
        <span style="color:#66d9ef">regex</span><span style="color:#960050;background-color:#1e0010">:</span> .<span style="color:#960050;background-color:#1e0010">*</span>,<span style="color:#66d9ef">prometheus</span>,.<span style="color:#960050;background-color:#1e0010">*</span>
        <span style="color:#66d9ef">action</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">keep</span>
      <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#66d9ef">__meta_digitalocean_tags</span>]
        <span style="color:#66d9ef">regex</span><span style="color:#960050;background-color:#1e0010">:</span> .<span style="color:#960050;background-color:#1e0010">*</span>,<span style="color:#66d9ef">haproxy_exporter</span>,.<span style="color:#960050;background-color:#1e0010">*</span>
        <span style="color:#66d9ef">action</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">keep</span>
      <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#66d9ef">__meta_digitalocean_public_ipv4</span>]
        <span style="color:#66d9ef">target_label</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">__address__</span>
        <span style="color:#66d9ef">replacement</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;$</span><span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">:</span><span style="color:#ae81ff">9101</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
</code></pre></div><p>Because the jobs are using relabelling for the <code>__address__</code> label (i.e. the Prometheus target address), they use the same Service Discovery API calls. All relabelling actions are performed locally, so they do not necessitate more API calls.</p>
<p>In the near future I will put a post together to compare the two approaches, so we can see the difference it makes.</p>
<h2 id="grafana">Grafana</h2>
<p>With the above, we can use any Node Exporter dashboard in Grafana to view all of the discovered instances. The <a href="https://grafana.com/grafana/dashboards/1860">Node Exporter Full</a> is always a good start when using the Node Exporter: -</p>
<p><img src="/img/prometheus/do-grafana.png" alt="Digital Ocean Grafana"></p>
<p>If we add more Droplets, they will also appear in this dashboard too.</p>
<h2 id="summary">Summary</h2>
<p>Prometheus is constantly being improved, with new features that make it more compelling with every release.</p>
<p>The inclusion of the Digital Ocean service discovery mechanism means that those who either do not require the complexity of AWS, Azure or Google Cloud Platform, or just prefer them as a provider, no longer need to run other mechanisms (like Consul or other methods) to automatically discover the Droplets and services.</p>
]]></content>
        </item>
        
        <item>
            <title>Prometheus - Auto-deploying Consul and Exporters using Saltstack Part 3: OpenBSD</title>
            <link>https://yetiops.net/posts/prometheus-consul-saltstack-part-3-openbsd/</link>
            <pubDate>Thu, 25 Jun 2020 00:10:22 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/prometheus-consul-saltstack-part-3-openbsd/</guid>
            <description>This is the third part in my ongoing series on using SaltStack to deploy Consul and Prometheus Exporters, enabling Prometheus to discover machines and services to monitor. You can view the other posts in the series below: -
 Part 1 - Linux Part 2 - Windows Part 4 - FreeBSD Part 5 - illumos Part 6 - MacOS  All of the states (as well as those for future posts, if you want a quick preview) are available in my Salt Lab repository.</description>
            <content type="html"><![CDATA[<p>This is the third part in my ongoing series on using SaltStack to deploy Consul and Prometheus Exporters, enabling Prometheus to discover machines and services to monitor. You can view the other posts in the series below: -</p>
<ul>
<li><a href="/posts/prometheus-consul-saltstack-part-1-linux/">Part 1 - Linux</a></li>
<li><a href="/posts/prometheus-consul-saltstack-part-2-windows/">Part 2 - Windows</a></li>
<li><a href="/posts/prometheus-consul-saltstack-part-4-freebsd/">Part 4 - FreeBSD</a></li>
<li><a href="/posts/prometheus-consul-saltstack-part-5-illumos/">Part 5 - illumos</a></li>
<li><a href="/posts/prometheus-consul-saltstack-part-6-macos/">Part 6 - MacOS</a></li>
</ul>
<p>All of the states (as well as those for future posts, if you want a quick preview) are available in my <a href="https://gitlab.com/stuh84/salt-lab">Salt Lab</a> repository.</p>
<h2 id="why-openbsd">Why OpenBSD?</h2>
<p>If you have visited this site before, you may know that I am a fan of OpenBSD. OpenBSD is built with security as its primary focus, with a base of applications than cover everything from <a href="/posts/openbsd-firewall-ha/">high availability firewalling</a>, network routers, SMTP servers and much more.</p>
<p>The developers also consider a lack of, or hard to understand documentation a bug. This means that the <code>man</code> pages, as well as example configuration files (in <code>/etc/examples</code>) are some of the most comprehensive and clear documentation you&rsquo;ll find on just about any operating system out there.</p>
<p>It is possible to configure an OpenBSD machine as a network router using only the documentation provided with the system itself. This has the advantage that if you are in a remote location with no access to the internet, there is enough information and examples in the base install to get it up and running.</p>
<h2 id="configuring-openbsd">Configuring OpenBSD</h2>
<p>The OpenBSD <a href="https://www.openbsd.org/faq/faq4.html">FAQ</a> will cover most of what you need to install OpenBSD and basic configuration tasks.</p>
<p>To configure a static IP in our management subnet, all we need to do is create a file called <code>/etc/hostname.$INTERFACE_NAME</code> (<code>vio1</code> in our case) and add the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">inet 10.15.31.23 255.255.255.0
</code></pre></div><p>You can then bring up the interface using <code>doas sh /etc/netstart vio1</code> (<code>doas(1)</code> being the privilege escalation utility in OpenBSD).</p>
<h2 id="installing-the-salt-minion">Installing the Salt Minion</h2>
<p>To install the Salt Minion in OpenBSD, you can use the <code>pkg_add(1)</code> utility. <code>pkg_add(1)</code> is OpenBSDs package manager, installing packages from their <a href="https://man.openbsd.org/ports.7">ports(7)</a> tree.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># Ensure the package exists</span>
$ pkg_info -Q salt
salt-2018.3.3p4

<span style="color:#75715e"># Install the package</span>
$ doas pkg_add salt
</code></pre></div><p>After this, the Salt Minion will now be installed.</p>
<h3 id="configuring-the-salt-minion">Configuring the Salt Minion</h3>
<p>As with Linux and Windows, Salt has an included minion configuration file. We replace the contents with the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">master: salt-master.yetiops.lab
id: openbsd-salt-01.yetiops.lab
nodename: openbsd-salt-01
</code></pre></div><p>The reason I have used the hostname <code>openbsd-salt-01</code> rather than just <code>openbsd-01</code> is because I already have a host with that name in my lab (as part of my <a href="/posts/ansible-for-networking-series/">Ansible for Networking series</a>).</p>
<p>Enable and restart the Salt Minion with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ doas rcctl enable salt_minion
$ doas rcctl start salt_minion
salt_minion<span style="color:#f92672">(</span>ok<span style="color:#f92672">)</span>
</code></pre></div><p>You should now see this host attempt to register with the Salt Master: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo salt-key -L
Accepted Keys:
alpine-01.yetiops.lab
arch-01.yetiops.lab
centos-01.yetiops.lab
salt-master.yetiops.lab
suse-01.yetiops.lab
ubuntu-01.yetiops.lab
void-01.yetiops.lab
win2019-01.yetiops.lab
Denied Keys:
Unaccepted Keys:
openbsd-salt-01.yetiops.lab
Rejected Keys:
</code></pre></div><p>Accept the key with <code>salt-key -a 'openbsd-salt-01*'</code>. Once this is done, you should now be able to manage the machine using Salt: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo salt <span style="color:#e6db74">&#39;openbsd*&#39;</span> test.ping
openbsd-salt-01.yetiops.lab:
    True

$ sudo salt <span style="color:#e6db74">&#39;openbsd*&#39;</span> grains.item os
openbsd-salt-01.yetiops.lab:
    ----------
    os:
        OpenBSD
</code></pre></div><h2 id="salt-states">Salt States</h2>
<p>We use two sets of states to deploy to OpenBSD. The first deploys Consul, the other deploys the Prometheus Node Exporter.</p>
<h3 id="applying-salt-states">Applying Salt States</h3>
<p>Once you have configured the states detailed below, use one of the following options to deploy the changes to the OpenBSD machine: -</p>
<ul>
<li><code>salt '*' state.highstate</code> from the Salt server (to configure every machine and every state)</li>
<li><code>salt 'openbsd*' state.highstate</code> from the Salt server (to configure all machines with a name beginning with <code>openbsd*</code>, applying all states)</li>
<li><code>salt 'openbsd*' state.apply consul</code> from the Salt server (to configure all machines with a name beginning with <code>openbsd*</code>, applying only the <code>consul</code> state)</li>
<li><code>salt-call state.highstate</code> from a machine running the Salt agent (to configure just one machine with all states)</li>
<li><code>salt-call state.apply consul</code> from a machine running the Salt agent (to configure just one machine with only the <code>consul</code> state)</li>
</ul>
<p>You can also use the <code>salt -C</code> option to apply based upon grains, pillars or other types of matches. For example, to apply to all machines running an OpenBSD kernel, you could run <code>salt -C 'G@kernel:OpenBSD' state.highstate</code>.</p>
<h3 id="consul---deployment">Consul - Deployment</h3>
<p>The following Salt state is used to deploy Consul onto an OpenBSD host: -</p>
<p><strong>/srv/salt/states/consul/openbsd.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul_package</span>:
  <span style="color:#66d9ef">pkg.installed</span>:
  - <span style="color:#66d9ef">pkgs</span>:
    - consul

<span style="color:#66d9ef">/opt/consul</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: _consul
    - <span style="color:#66d9ef">group</span>: _consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/etc/consul.d</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: _consul
    - <span style="color:#66d9ef">group</span>: _consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/etc/consul.d/consul.hcl</span>:
  <span style="color:#66d9ef">file.managed</span>:
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>] is defined %}
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;server&#39;</span>] is defined %}
    - <span style="color:#66d9ef">source</span>: salt://consul/server/files/consul.hcl.j2
{% else %}
    - <span style="color:#66d9ef">source</span>: salt://consul/client/files/consul.hcl.j2
{% endif %}
{% endif %}
    - <span style="color:#66d9ef">user</span>: _consul
    - <span style="color:#66d9ef">group</span>: _consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0640</span>
    - <span style="color:#66d9ef">template</span>: jinja

<span style="color:#66d9ef">consul_service</span>:
  <span style="color:#66d9ef">service.running</span>:
  - <span style="color:#66d9ef">name</span>: consul
  - <span style="color:#66d9ef">enable</span>: True
  - <span style="color:#66d9ef">reload</span>: True
  - <span style="color:#66d9ef">watch</span>:
    - <span style="color:#66d9ef">file</span>: /etc/consul.d/consul.hcl

{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>] is defined %}
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;prometheus_services&#39;</span>] is defined %}
{% for service in pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;prometheus_services&#39;</span>] %}
/etc/consul.d/{{ <span style="color:#66d9ef">service }}.hcl</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://consul/services/files/{{ service }}.hcl
    - <span style="color:#66d9ef">user</span>: _consul
    - <span style="color:#66d9ef">group</span>: _consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0640</span>
    - <span style="color:#66d9ef">template</span>: jinja

consul_reload_{{ <span style="color:#66d9ef">service }}</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: consul reload
    - <span style="color:#66d9ef">watch</span>:
      - <span style="color:#66d9ef">file</span>: /etc/consul.d/{{ service }}.hcl
{% endfor %}
{% endif %}
{% endif %}
</code></pre></div><p>This state is very similar to the Linux states. The main difference is that because Hashicorp do not provide a Consul binary for OpenBSD, we install it from the OpenBSD package repositories (which uses <code>pkg_add(1)</code>).</p>
<p>You may notice that the users and groups are prefixed with an underscore (<code>_</code>). This is a naming convention that means the user is <strong>unprivileged</strong>, meaning they should not be able to perform the same kind of tasks that a normal user could.</p>
<p>This state is applied to OpenBSD machines as such: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">base</span>:
  <span style="color:#66d9ef">&#39;G@init:systemd and G@kernel:Linux&#39;</span>:
    - <span style="color:#66d9ef">match</span>: compound
    - consul
    - exporters.node_exporter.systemd

  <span style="color:#66d9ef">&#39;os:Alpine&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.alpine
    - exporters.node_exporter.alpine

  <span style="color:#66d9ef">&#39;os:Void&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.void
    - exporters.node_exporter.void

  <span style="color:#66d9ef">&#39;kernel:OpenBSD&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.openbsd

  <span style="color:#66d9ef">&#39;kernel:Windows&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.windows
    - exporters.windows_exporter.win_exporter
    - exporters.windows_exporter.windows_exporter
</code></pre></div><p>We match the <code>kernel</code> grain, ensuring the value is <code>OpenBSD</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ salt <span style="color:#e6db74">&#39;openbsd*&#39;</span> grains.item kernel
openbsd-salt-01.yetiops.lab:
    ----------
    kernel:
        OpenBSD
</code></pre></div><h4 id="pillars">Pillars</h4>
<p>We use the same pillars for OpenBSD as we do for Linux: -</p>
<p><strong>consul.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul</span>:
  <span style="color:#66d9ef">data_dir</span>: /opt/consul
  <span style="color:#66d9ef">prometheus_services</span>:
  - node_exporter
</code></pre></div><p><strong>consul-dc.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul</span>:
  <span style="color:#66d9ef">dc</span>: yetiops
  <span style="color:#66d9ef">enc_key</span>: <span style="color:#75715e">###CONSUL_KEY### </span>
  <span style="color:#66d9ef">servers</span>:
  - salt-master.yetiops.lab
</code></pre></div><p>These pillars reside in <code>/srv/salt/pillars/consul</code>. They are applied as such: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">base</span>:
  <span style="color:#66d9ef">&#39;*&#39;</span>:
    - consul.consul-dc

  <span style="color:#66d9ef">&#39;G@kernel:Linux or G@kernel:OpenBSD&#39;</span>:
    - <span style="color:#66d9ef">match</span>: compound
    - consul.consul

  <span style="color:#66d9ef">&#39;kernel:Windows&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.consul-client-win

  <span style="color:#66d9ef">&#39;salt-master*&#39;</span>:
    - consul.consul-server
</code></pre></div><p>In the first part of this series, I mentioned: -</p>
<blockquote>
<p>The consul.consul pillar (i.e. /srv/salt/pillars/consul/consul.sls) is applied to all of our Linux hosts, as they all have the same data directory and the same prometheus_services.</p>
<p>Currently there is only one grain to match against, so we could have used match: grain. However in later posts in this series, we will match against multiple grains (requiring the compound match).</p>
</blockquote>
<p>As we can see above, we are now matching against multiple grains. We now can use the <code>compound</code> match to apply the pillars, rather than repeating the configuration for each different kernel.</p>
<h3 id="consul---verification">Consul - Verification</h3>
<p>We can verify that Consul is working with the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ consul members
Node                         Address           Status  Type    Build  Protocol  DC       Segment
salt-master                  10.15.31.5:8301   alive   server  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;all&gt;
alpine-01                    10.15.31.27:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
arch-01                      10.15.31.26:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
centos-01.yetiops.lab        10.15.31.24:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
openbsd-salt-01.yetiops.lab  10.15.31.23:8301  alive   client  1.7.2  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
suse-01                      10.15.31.22:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
ubuntu-01                    10.15.31.33:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
void-01                      10.15.31.31:8301  alive   client  1.7.2  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
win2019-01                   10.15.31.25:8301  alive   client  1.7.2  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;

$ consul catalog nodes -service<span style="color:#f92672">=</span>node_exporter
Node                         ID        Address      DC
alpine-01                    e59eb6fc  10.15.31.27  yetiops
arch-01                      97c67201  10.15.31.26  yetiops
centos-01.yetiops.lab        78ac8405  10.15.31.24  yetiops
openbsd-salt-01.yetiops.lab  c87bfa18  10.15.31.23  yetiops
salt-master                  344fb6f2  10.15.31.5   yetiops
suse-01                      d2fdd88a  10.15.31.22  yetiops
ubuntu-01                    4544c7ff  10.15.31.33  yetiops
void-01                      e99c7e3c  10.15.31.31  yetiops
</code></pre></div><h3 id="node-exporter---deployment">Node Exporter - Deployment</h3>
<p>Now that Consul is up and running, we will install the Prometheus Node Exporter.</p>
<h4 id="states">States</h4>
<p>The following Salt state is used to deploy the Prometheus Node Exporter onto an OpenBSD host: -</p>
<p><strong>/srv/salt/states/exporters/node_exporter/bsd.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">node_exporter_package</span>:
  <span style="color:#66d9ef">pkg.installed</span>:
    - <span style="color:#66d9ef">pkgs</span>:
      - node_exporter

<span style="color:#66d9ef">node_exporter_service</span>:
  <span style="color:#66d9ef">service.running</span>:
    - <span style="color:#66d9ef">name</span>: node_exporter
    - <span style="color:#66d9ef">enable</span>: <span style="color:#66d9ef">true</span>
</code></pre></div><p>As you can see, we only have two parts to this. First, we install the <code>node_exporter</code> using OpenBSDs package manager (<code>pkg_add(1)</code>). Afterwards, we enable the <code>node_exporter</code> service. Again, binaries are not released for OpenBSD by the Prometheus project, so we use what is in the <code>ports(7)</code> tree.</p>
<p>This state is applied with the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">base</span>:
  <span style="color:#66d9ef">&#39;G@init:systemd and G@kernel:Linux&#39;</span>:
    - <span style="color:#66d9ef">match</span>: compound
    - consul
    - exporters.node_exporter.systemd

  <span style="color:#66d9ef">&#39;os:Alpine&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.alpine
    - exporters.node_exporter.alpine

  <span style="color:#66d9ef">&#39;os:Void&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.void
    - exporters.node_exporter.void

  <span style="color:#66d9ef">&#39;kernel:OpenBSD&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.openbsd
    - exporters.node_exporter.bsd

  <span style="color:#66d9ef">&#39;kernel:Windows&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.windows
    - exporters.windows_exporter.win_exporter
    - exporters.windows_exporter.windows_exporter
</code></pre></div><h4 id="pillars-1">Pillars</h4>
<p>There are no pillars in this lab specific to the Node Exporter.</p>
<h3 id="node-exporter---verification">Node Exporter - Verification</h3>
<p>After this, we should be able to see the <code>node_exporter</code> running and producing metrics: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># Check the service is enabled</span>
$ rcctl ls on  
check_quotas
consul
cron
library_aslr
node_exporter
ntpd
pf
pflogd
salt_minion
slaacd
smtpd
sndiod
sshd
syslogd

<span style="color:#75715e"># Check it is listening</span>
$ netstat -an | grep -i <span style="color:#ae81ff">9100</span>  
tcp          <span style="color:#ae81ff">0</span>      <span style="color:#ae81ff">0</span>  10.15.31.23.9100       10.15.31.254.40456     ESTABLISHED
tcp          <span style="color:#ae81ff">0</span>      <span style="color:#ae81ff">0</span>  *.9100                 *.*                    LISTEN

<span style="color:#75715e"># Check it responds</span>
$ curl 10.15.31.23:9100/metrics | grep -i uname
node_uname_info<span style="color:#f92672">{</span>domainname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;yetiops.lab&#34;</span>,machine<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;amd64&#34;</span>,nodename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;openbsd-salt-01&#34;</span>,release<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;6.7&#34;</span>,sysname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;OpenBSD&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;OpenBSD 6.7 (GENERIC) #179: Thu May  7 11:02:37 MDT 2020     deraadt@amd64.openbsd.org:/usr/src/sys/arch/amd64/compile/GENERIC &#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>
</code></pre></div><p>All looks good!</p>
<h2 id="prometheus-targets">Prometheus Targets</h2>
<p>As Prometheus is already set up (see <a href="/posts/prometheus-consul-saltstack-part-1-linux/#configuring-prometheus">here</a>), and matches on the <code>prometheus</code> tag, we should see this within the Prometheus targets straight away: -</p>
<p><img src="/img/prometheus/openbsd-consul-targets.png" alt="OpenBSD Consul Prometheus Target"></p>
<p>The second target here is the OpenBSD machine (<code>10.15.31.23</code>).</p>
<p><img src="/img/prometheus/openbsd-consul-metadata.png" alt="OpenBSD Consul Prometheus MetaData"></p>
<p>Above is the Metadata we receive from Consul about this host</p>
<h2 id="grafana-dashboards">Grafana Dashboards</h2>
<p>When I <a href="/posts/prometheus-monitoring-other-os/">previously covered</a> using the Prometheus Node Exporter with OpenBSD (and FreeBSD), I mentioned that <strong>load</strong> is calculated differently on BSD-based systems as it is on Linux. In BSD-based systems, <strong>load</strong> is a measure of CPU usage only, whereas in Linux it also takes into account disk/storage wait times and more.</p>
<p>Because of this, different metrics are exposed by the Node Exporter on OpenBSD. This means that you may find that standard Node Exporter dashboards give unexpected results when targeting OpenBSD.</p>
<p>Instead, you could use the <a href="https://grafana.com/grafana/dashboards/11640">OpenBSD Server Overview</a> dashboard. This dashboard does assume that all hosts that it discovers with the <code>node_exporter</code> are OpenBSD. If you run this in an environment with Linux and other systems, it would require updating it to use variables, or matching labels that indicate that a discovered host is running OpenBSD.</p>
<p>An alternative is the <a href="https://grafana.com/grafana/dashboards/4260">Node Exporter FreeBSD</a> dashboard. While the name indicates it is solely for FreeBSD, because FreeBSD and OpenBSD calculate CPU and memory in similar ways, this dashboard will work for both.</p>
<p><strong>OpenBSD Server Overview</strong>
<img src="/img/prometheus/openbsd-server-dashboard.png" alt="OpenBSD Server Overview Dashboard"></p>
<p><strong>Node Exporter FreeBSD</strong>
<img src="/img/prometheus/openbsd-node-exporter-freebsd.png" alt="OpenBSD Node Exporter FreeBSD"></p>
<h2 id="summary">Summary</h2>
<p>Being able to manage OpenBSD in the same way you can manage Linux and Windows ensures consistency across your environment, no matter what role they serve in your infrastructure.</p>
<p>For example, you could use Linux as your primary server operating system, Windows to provide Active Directory, and OpenBSD for your <a href="/posts/openbsd-firewall-ha/">security perimeter</a>. Using something like SaltStack to deploy your configuration across all platforms ensures that your infrastructure policies (domain names, proxies, monitoring thresholds) are consistent, as they all originate from the same source of truth.</p>
<p>Even if you are in an OpenBSD-only environment, you can still benefit from the central management and automatically monitoring new hosts, with the added bonus of needing less state files and pillars to manage!</p>
<p>In the next post in this series, we will cover how you deploy SaltStack on a FreeBSD host, which will then deploy Consul and the Prometheus Node Exporter.</p>
]]></content>
        </item>
        
        <item>
            <title>Prometheus - Using Service Discovery with AWS, Azure and the Google Cloud Platform</title>
            <link>https://yetiops.net/posts/prometheus-service-discovery-aws-gcp-azure/</link>
            <pubDate>Tue, 23 Jun 2020 13:56:29 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/prometheus-service-discovery-aws-gcp-azure/</guid>
            <description>Prometheus has multiple methods to discover services to monitor. You can use static configuration (i.e. specifying the IP and ports of services to monitor), discovery via DNS SRV records, files with targets listed in them, and Hashicorp&amp;rsquo;s Consul. You can leverage these to monitor resources on premise or in the cloud.
Prometheus can also use the APIs of some cloud providers to discover services. This means less operational overhead (e.g. no need for Consul clusters or updating DNS records), while still being able to discovering hosts and services dynamically.</description>
            <content type="html"><![CDATA[<p>Prometheus has multiple methods to discover services to monitor. You can use static configuration (i.e. specifying the IP and ports of services to monitor), <a href="/posts/prometheus-srv-discovery/">discovery via DNS SRV records</a>, files with targets listed in them, and <a href="/posts/prometheus-consul-node_exporter/">Hashicorp&rsquo;s Consul</a>. You can leverage these to monitor resources on premise or in the cloud.</p>
<p>Prometheus can also use the APIs of some cloud providers to discover services. This means less operational overhead (e.g. no need for Consul clusters or updating DNS records), while still being able to discovering hosts and services dynamically.</p>
<h2 id="outline">Outline</h2>
<p>In this post we will cover building a cloud instance in AWS (EC2), Azure (Virtual Machine) and the Google Cloud Platform (Compute Instance) using Terraform.</p>
<p>We will install the Prometheus Node Exporter on each instance, and then create a user that has enough API access for instance and service discovery.</p>
<p>Finally, we will configure a Prometheus instance with access to each platform, allowing it to discover and monitor the cloud instances.</p>
<h2 id="terraform">Terraform</h2>
<p><a href="https://www.terraform.io">Terraform</a> is an Infrastructure-as-Code tool, allowing you to define virtual machines, cloud applications and more. The configuration files can also be committed to a version control system. This means that if you need to rebuild your environment and create similar/identical environments you can take the Terraform files and recreate/update them for your needs. You can also look at changes in your infrastructure by looking through the commit history in your version control system (e.g. Git)</p>
<p>You can find more information about Terraform <a href="https://www.terraform.io/intro/index.html">here</a>.</p>
<p>The <a href="https://www.terraform.io/downloads.html">Terraform binaries</a> (i.e. the application which will turn your Terraform configuration files into infrastructure) are available for Linux, macOS, Windows, FreeBSD, OpenBSD and Solaris.</p>
<h3 id="configure-terraform">Configure Terraform</h3>
<p>Once the Terraform binary is installed, you can configure your Terraform environment.</p>
<p>First, I create a Terraform directory and turn it into a Git repository: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># Create the directory</span>
$ mkdir terraform

<span style="color:#75715e"># Go into the directory</span>
$ cd terraform

<span style="color:#75715e"># Initialize git</span>
$ git init

<span style="color:#75715e"># Create a directory for creating virtual machines</span>
$ mkdir basic-vms

<span style="color:#75715e"># Create a directory for the Prometheus users</span>
$ mkdir prometheus-access
</code></pre></div><p>You can choose to layout your Terraform directory however you want. You could have all files in one directory, directories per cloud provider, directories per environment, or directories by resource type. For this, I have chosen to have a directory for the cloud instances (for all providers) and another for the Prometheus user/role access.</p>
<h2 id="aws">AWS</h2>
<p>Cloud Instances within AWS are known as EC2s, ranging from small virtual machines with 512M of memory with single vCPUs, all the way bare metal with hundreds of gigabytes of memory and tens of CPU cores.</p>
<p>You can sign up for an AWS account <a href="https://portal.aws.amazon.com/billing/signup#/start">here</a>. This comes with a years access to their <a href="https://aws.amazon.com/free/">free tier</a>, which includes their <code>t2.micro</code> (1 vCPU and 1G of memory) and <code>t3.micro</code> (the same specs as the <code>t2.micro</code>, but on a newer generation of hardware) EC2 instances.</p>
<p>Once you have signed up, you&rsquo;ll need to install the AWS CLI tool. Terraform uses the credentials and configuration files that the AWS CLI generates.</p>
<h3 id="install-the-aws-cli">Install the AWS CLI</h3>
<p>The AWS CLI can be installed via your platforms package manager (e.g. <code>apt install awscli</code> or <code>brew install awscli</code>).</p>
<p>Alternatively you can use Python&rsquo;s PIP command. PIP will get you the latest stable version, rather than what is packaged in your operating systems repository. You can install with PIP by using <code>pip install awscli</code> (or <code>pip3</code> for the Python 3 version).</p>
<h3 id="configure-your-credentials">Configure your credentials</h3>
<p>To configure your credentials, use the <code>aws configure</code> command. This will give the following options: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ aws configure
AWS Access Key ID <span style="color:#f92672">[</span>None<span style="color:#f92672">]</span>: <span style="color:#75715e">###ACCESS_KEY###</span> 
AWS Secret Access Key <span style="color:#f92672">[</span>None<span style="color:#f92672">]</span>: <span style="color:#75715e">###SECRET_KEY###</span>
Default region name <span style="color:#f92672">[</span>None<span style="color:#f92672">]</span>: eu-west-2 <span style="color:#75715e">### Replace this with whatever region you prefer (eg us-east-1, ap-southeast-1)</span>
Default output format <span style="color:#f92672">[</span>None<span style="color:#f92672">]</span>:
</code></pre></div><h4 id="access-key-and-secret-key">Access Key and Secret Key</h4>
<p>The Access Key and Secret Key can be obtained from the AWS Console. This can either be for your AWS account&rsquo;s root user, or an IAM (Identity &amp; Access Management) User (i.e. a user created within the account). Typically you should use an IAM user, so that permissions can be restricted to only what is necessary. However for the purposes of this article, I am going to use the root user.</p>
<p>Log into the AWS Console, go to the right hand corner and click on your username: -</p>
<p><img src="/img/prometheus/aws-account-menu.png" alt="AWS Account Menu"></p>
<p>Click on <strong>My Security Credentials</strong>, then <strong>Access Keys (access key ID and secret access key)</strong> and then <strong>Create New Access Key</strong>. You&rsquo;ll be presented with a pop-up that has the Access Key and Secret Access Key. Make sure you save these, as the Secret Key is not shown again.</p>
<h3 id="configure-terraform---ec2s">Configure Terraform - EC2s</h3>
<p>Now that you have generated the correct configuration with the AWS CLI, you can use Terraform with AWS.</p>
<p>Navigate to the Terraform directory we created earlier, and then the <code>basic-vms</code> directory (<code>cd terraform/basic-vms</code>).</p>
<p>In here, create a <code>providers.tf</code> file. This tells Terraform that we are going to configure resources in AWS, and what our default region is: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#75715e"># AWS Provider
</span><span style="color:#75715e"></span><span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;aws&#34;</span> {
  region  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;eu-west-2&#34;</span>
}
</code></pre></div><p>After this, run <code>terraform init</code>. This downloads the AWS Terraform provider binary: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ terraform init

Initializing the backend...

Initializing provider plugins...
- Checking <span style="color:#66d9ef">for</span> available provider plugins...
- Downloading plugin <span style="color:#66d9ef">for</span> provider <span style="color:#e6db74">&#34;aws&#34;</span> <span style="color:#f92672">(</span>hashicorp/aws<span style="color:#f92672">)</span> 2.67.0...

The following providers <span style="color:#66d9ef">do</span> not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;...&#34;</span> constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* provider.aws: version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;~&gt; 2.67&#34;</span>

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running <span style="color:#e6db74">&#34;terraform plan&#34;</span> to see
any changes that are required <span style="color:#66d9ef">for</span> your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration <span style="color:#66d9ef">for</span> Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to <span style="color:#66d9ef">do</span> so <span style="color:#66d9ef">if</span> necessary.
</code></pre></div><h3 id="define-the-infrastructure---ec2s">Define the infrastructure - EC2s</h3>
<p>You can create the configuration files for your first EC2 instance. The below is from the file <code>aws.tf</code> in the <code>terraform/basic-vms</code> directory: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;aws_ami&#34; &#34;ubuntu&#34;</span> {
  most_recent <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>

  <span style="color:#66d9ef">filter</span> {
    name   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;name&#34;</span>
    values <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-*&#34;</span>]
  }

  <span style="color:#66d9ef">filter</span> {
    name   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;virtualization-type&#34;</span>
    values <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;hvm&#34;</span>]
  }

  owners <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;099720109477&#34;</span>]<span style="color:#75715e"> # Canonical
</span><span style="color:#75715e"></span>}

<span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;aws_vpc&#34; &#34;default&#34;</span> {
  default <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_key_pair&#34; &#34;yetiops-aws-prom&#34;</span> {
  key_name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span>
  public_key <span style="color:#f92672">=</span> <span style="color:#66d9ef">file</span>(<span style="color:#e6db74">&#34;~/.ssh/id_rsa.pub&#34;</span>)
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_instance&#34; &#34;yetiops-aws-prom&#34;</span> {
  ami           <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">aws_ami</span>.<span style="color:#66d9ef">ubuntu</span>.<span style="color:#66d9ef">id</span>
  instance_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;t2.micro&#34;</span>
  user_data     <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">template_file</span>.<span style="color:#66d9ef">ubuntu</span>.<span style="color:#66d9ef">template</span>

  key_name <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_key_pair</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">aws</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">key_name</span>

  vpc_security_group_ids <span style="color:#f92672">=</span> [
    <span style="color:#66d9ef">aws_security_group</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">aws</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">id</span>
  ]

  tags <span style="color:#f92672">=</span> {
    Name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span>
    prometheus <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
    node_exporter <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
  }
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_security_group&#34; &#34;yetiops-aws-prom&#34;</span> {
  name        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span>
  description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;AWS Security Group for yetiops-aws-prom&#34;</span>
  vpc_id      <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">aws_vpc</span>.<span style="color:#66d9ef">default</span>.<span style="color:#66d9ef">id</span>

  tags <span style="color:#f92672">=</span> {
    Name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span>
  }
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_security_group_rule&#34; &#34;ingress_ssh_in&#34;</span> {
  type              <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ingress&#34;</span>
  to_port           <span style="color:#f92672">=</span> <span style="color:#ae81ff">22</span>
  protocol          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
  from_port         <span style="color:#f92672">=</span> <span style="color:#ae81ff">22</span>
  cidr_blocks       <span style="color:#f92672">=</span> [
    <span style="color:#e6db74">&#34;$MY_PUBLIC_IP/32&#34;</span>
  ]
  security_group_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_security_group</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">aws</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">id</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_security_group_rule&#34; &#34;ingress_node_exporter_in&#34;</span> {
  type              <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ingress&#34;</span>
  to_port           <span style="color:#f92672">=</span> <span style="color:#ae81ff">9100</span>
  protocol          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
  from_port         <span style="color:#f92672">=</span> <span style="color:#ae81ff">9100</span>
  cidr_blocks       <span style="color:#f92672">=</span> [
    <span style="color:#e6db74">&#34;$MY_PUBLIC_IP/32&#34;</span>
  ]
  security_group_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_security_group</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">aws</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">id</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_security_group_rule&#34; &#34;egress_allow_all&#34;</span> {
  type              <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;egress&#34;</span>
  to_port           <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
  protocol          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;-1&#34;</span>
  from_port         <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
  cidr_blocks       <span style="color:#f92672">=</span> [
    <span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>
  ]
  security_group_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_security_group</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">aws</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">id</span>
}
</code></pre></div><p>To summarize what we are doing here, we are: -</p>
<ul>
<li>Using a Terraform <strong>data</strong> source (i.e. a read only view) to retrieve the latest Amazon Machine Image (i.e. a pre-defined virtual machine image) for Ubuntu 20.04
<ul>
<li>The owner ID of <code>099720109477</code> is the AWS account of Canonical (i.e. the makers of Ubuntu)</li>
</ul>
</li>
<li>Using another <strong>data</strong> source, we discover the default Virtual Public Cloud (i.e. the private Amazon network for our account)</li>
<li>Create an SSH keypair in AWS, based upon one we have generated on our machine (you could generate one in AWS instead if you wish)</li>
<li>Creating an Amazon EC2 of type <code>t2.micro</code>, with a <strong>Security Group</strong> attached, specifying <strong>user-data</strong> (i.e. first boot configuration) and applying some <strong>tags</strong> (key-value pairs) to the instance</li>
<li>Creating a <strong>Security Group</strong> (a network firewall), residing in our VPC</li>
<li>Adding rules to the security group to allow SSH and TCP port 9100 (the Prometheus Node Exporter) port from my public IP address</li>
<li>Allowing all outbound traffic from the virtual machine</li>
</ul>
<p>The <code>user_data</code> is specified in a different file, called <code>user_data.tf</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;template_file&#34; &#34;ubuntu&#34;</span> {
  template <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${file(&#34;${path.module}/files/ubuntu.tpl&#34;)}&#34;</span>
}

<span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;template_cloudinit_config&#34; &#34;ubuntu&#34;</span> {
  gzip          <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>
  base64_encode <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>

  <span style="color:#66d9ef">part</span> {
    filename     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;init.cfg&#34;</span>
    content_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;text/cloud-config&#34;</span>
    content      <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">template_file</span>.<span style="color:#66d9ef">ubuntu</span>.<span style="color:#66d9ef">rendered</span>
  }
}
</code></pre></div><p>The above file serves two purposes. First, it sources the template from a file. In this case, the file is <code>terraform/basic-vms/files/ubuntu.tpl</code> (<code>${path.module}</code> refers to the path relative to where you define your resources). Secondly, it produces a <code>template_cloudinit_config</code> resource as well from the same template. For AWS this is not strictly required, whereas we will need to use this for Azure and Google Cloud Platform.</p>
<p>The contents of the file are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">#cloud-config
packages:
 - prometheus-node-exporter
</code></pre></div><p>The <code>user-data</code> can be shell scripts (Bash, Powershell or otherwise), or you can make use of <a href="https://cloudinit.readthedocs.io/en/latest/topics/examples.html">cloud-config</a>. <strong>cloud-config</strong> is intended to be system agnostic, meaning we do not need to tell it which package manager to use (useful if you are installing across different operating systems).</p>
<h3 id="build-the-infrastructure---ec2s">Build the infrastructure - EC2s</h3>
<p>We can now apply our configuration, and see if it builds an Amazon EC2 machine. First, run <code>terraform plan</code> (in the same directory as your <code>.tf</code> files) to see what infrastructure will be provisioned: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

data.template_file.ubuntu: Refreshing state...
data.template_cloudinit_config.ubuntu: Refreshing state...
data.aws_vpc.default: Refreshing state...
data.aws_ami.ubuntu: Refreshing state...

------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  <span style="color:#75715e"># aws_instance.yetiops-aws-prom will be created</span>
  + resource <span style="color:#e6db74">&#34;aws_instance&#34;</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span> <span style="color:#f92672">{</span>
      + ami                          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ami-00f6a0c18edb19300&#34;</span>
      + arn                          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + associate_public_ip_address  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + availability_zone            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + cpu_core_count               <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + cpu_threads_per_core         <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + get_password_data            <span style="color:#f92672">=</span> false
      + host_id                      <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id                           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + instance_state               <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + instance_type                <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;t2.micro&#34;</span>
      + ipv6_address_count           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + ipv6_addresses               <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + key_name                     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span>
      + network_interface_id         <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + outpost_arn                  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + password_data                <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + placement_group              <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + primary_network_interface_id <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + private_dns                  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + private_ip                   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + public_dns                   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + public_ip                    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + security_groups              <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + source_dest_check            <span style="color:#f92672">=</span> true
      + subnet_id                    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + tags                         <span style="color:#f92672">=</span> <span style="color:#f92672">{</span>
          + <span style="color:#e6db74">&#34;Name&#34;</span>          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span>
          + <span style="color:#e6db74">&#34;node_exporter&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
          + <span style="color:#e6db74">&#34;prometheus&#34;</span>    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
        <span style="color:#f92672">}</span>
      + tenancy                      <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + user_data                    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;2169d8a3e100623d34bf1a7b2f6bd924a8997bfb&#34;</span>
      + volume_tags                  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + vpc_security_group_ids       <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>

      + ebs_block_device <span style="color:#f92672">{</span>
          + delete_on_termination <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + device_name           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + encrypted             <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + iops                  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + kms_key_id            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + snapshot_id           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + volume_id             <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + volume_size           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + volume_type           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
        <span style="color:#f92672">}</span>

      + ephemeral_block_device <span style="color:#f92672">{</span>
          + device_name  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + no_device    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + virtual_name <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
        <span style="color:#f92672">}</span>

      + metadata_options <span style="color:#f92672">{</span>
          + http_endpoint               <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + http_put_response_hop_limit <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + http_tokens                 <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
        <span style="color:#f92672">}</span>

      + network_interface <span style="color:#f92672">{</span>
          + delete_on_termination <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + device_index          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + network_interface_id  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
        <span style="color:#f92672">}</span>

      + root_block_device <span style="color:#f92672">{</span>
          + delete_on_termination <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + device_name           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + encrypted             <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + iops                  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + kms_key_id            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + volume_id             <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + volume_size           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + volume_type           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
        <span style="color:#f92672">}</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># aws_key_pair.yetiops-aws-prom will be created</span>
  + resource <span style="color:#e6db74">&#34;aws_key_pair&#34;</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span> <span style="color:#f92672">{</span>
      + arn         <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + fingerprint <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + key_name    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span>
      + key_pair_id <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + public_key  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;</span>$MY_PUBLIC_KEY<span style="color:#e6db74">&#34;</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># aws_security_group.yetiops-aws-prom will be created</span>
  + resource <span style="color:#e6db74">&#34;aws_security_group&#34;</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span> <span style="color:#f92672">{</span>
      + arn                    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + description            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;AWS Security Group for yetiops-aws-prom&#34;</span>
      + egress                 <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id                     <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + ingress                <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + name                   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span>
      + owner_id               <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + revoke_rules_on_delete <span style="color:#f92672">=</span> false
      + tags                   <span style="color:#f92672">=</span> <span style="color:#f92672">{</span>
          + <span style="color:#e6db74">&#34;Name&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span>
        <span style="color:#f92672">}</span>
      + vpc_id                 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vpc-a62062ce&#34;</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># aws_security_group_rule.egress_allow_all will be created</span>
  + resource <span style="color:#e6db74">&#34;aws_security_group_rule&#34;</span> <span style="color:#e6db74">&#34;egress_allow_all&#34;</span> <span style="color:#f92672">{</span>
      + cidr_blocks              <span style="color:#f92672">=</span> <span style="color:#f92672">[</span>
          + <span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>,
        <span style="color:#f92672">]</span>
      + from_port                <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
      + id                       <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + protocol                 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;-1&#34;</span>
      + security_group_id        <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + self                     <span style="color:#f92672">=</span> false
      + source_security_group_id <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + to_port                  <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
      + type                     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;egress&#34;</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># aws_security_group_rule.ingress_node_exporter_in will be created</span>
  + resource <span style="color:#e6db74">&#34;aws_security_group_rule&#34;</span> <span style="color:#e6db74">&#34;ingress_node_exporter_in&#34;</span> <span style="color:#f92672">{</span>
      + cidr_blocks              <span style="color:#f92672">=</span> <span style="color:#f92672">[</span>
          + <span style="color:#e6db74">&#34;</span>$MY_PUBLIC_IP<span style="color:#e6db74">/32&#34;</span>,
        <span style="color:#f92672">]</span>
      + from_port                <span style="color:#f92672">=</span> <span style="color:#ae81ff">9100</span>
      + id                       <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + protocol                 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
      + security_group_id        <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + self                     <span style="color:#f92672">=</span> false
      + source_security_group_id <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + to_port                  <span style="color:#f92672">=</span> <span style="color:#ae81ff">9100</span>
      + type                     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ingress&#34;</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># aws_security_group_rule.ingress_ssh_in will be created</span>
  + resource <span style="color:#e6db74">&#34;aws_security_group_rule&#34;</span> <span style="color:#e6db74">&#34;ingress_ssh_in&#34;</span> <span style="color:#f92672">{</span>
      + cidr_blocks              <span style="color:#f92672">=</span> <span style="color:#f92672">[</span>
          + <span style="color:#e6db74">&#34;</span>$MY_PUBLIC_IP<span style="color:#e6db74">/32&#34;</span>,
        <span style="color:#f92672">]</span>
      + from_port                <span style="color:#f92672">=</span> <span style="color:#ae81ff">22</span>
      + id                       <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + protocol                 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
      + security_group_id        <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + self                     <span style="color:#f92672">=</span> false
      + source_security_group_id <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + to_port                  <span style="color:#f92672">=</span> <span style="color:#ae81ff">22</span>
      + type                     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ingress&#34;</span>
    <span style="color:#f92672">}</span>

Plan: <span style="color:#ae81ff">6</span> to add, <span style="color:#ae81ff">0</span> to change, <span style="color:#ae81ff">0</span> to destroy.

------------------------------------------------------------------------

Note: You didn<span style="color:#e6db74">&#39;t specify an &#34;-out&#34; parameter to save this plan, so Terraform
</span><span style="color:#e6db74">can&#39;</span>t guarantee that exactly these actions will be performed <span style="color:#66d9ef">if</span>
<span style="color:#e6db74">&#34;terraform apply&#34;</span> is subsequently run.
</code></pre></div><p>This all looks correct, so now it&rsquo;s time to apply the configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ terraform apply
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

data.template_file.ubuntu: Refreshing state...
data.template_cloudinit_config.ubuntu: Refreshing state...
data.aws_vpc.default: Refreshing state...
data.aws_ami.ubuntu: Refreshing state...

------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  <span style="color:#75715e"># aws_instance.yetiops-aws-prom will be created</span>
  + resource <span style="color:#e6db74">&#34;aws_instance&#34;</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span> <span style="color:#f92672">{</span>
      + ami                          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ami-00f6a0c18edb19300&#34;</span>
      + arn                          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + associate_public_ip_address  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + availability_zone            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + cpu_core_count               <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + cpu_threads_per_core         <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + get_password_data            <span style="color:#f92672">=</span> false
      + host_id                      <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id                           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + instance_state               <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + instance_type                <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;t2.micro&#34;</span>
      + ipv6_address_count           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + ipv6_addresses               <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + key_name                     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span>
      + network_interface_id         <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + outpost_arn                  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + password_data                <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + placement_group              <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + primary_network_interface_id <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + private_dns                  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + private_ip                   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + public_dns                   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + public_ip                    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + security_groups              <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + source_dest_check            <span style="color:#f92672">=</span> true
      + subnet_id                    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + tags                         <span style="color:#f92672">=</span> <span style="color:#f92672">{</span>
          + <span style="color:#e6db74">&#34;Name&#34;</span>          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span>
          + <span style="color:#e6db74">&#34;node_exporter&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
          + <span style="color:#e6db74">&#34;prometheus&#34;</span>    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
        <span style="color:#f92672">}</span>
      + tenancy                      <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + user_data                    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;2169d8a3e100623d34bf1a7b2f6bd924a8997bfb&#34;</span>
      + volume_tags                  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + vpc_security_group_ids       <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>

      + ebs_block_device <span style="color:#f92672">{</span>
          + delete_on_termination <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + device_name           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + encrypted             <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + iops                  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + kms_key_id            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + snapshot_id           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + volume_id             <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + volume_size           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + volume_type           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
        <span style="color:#f92672">}</span>

      + ephemeral_block_device <span style="color:#f92672">{</span>
          + device_name  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + no_device    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + virtual_name <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
        <span style="color:#f92672">}</span>

      + metadata_options <span style="color:#f92672">{</span>
          + http_endpoint               <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + http_put_response_hop_limit <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + http_tokens                 <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
        <span style="color:#f92672">}</span>

      + network_interface <span style="color:#f92672">{</span>
          + delete_on_termination <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + device_index          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + network_interface_id  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
        <span style="color:#f92672">}</span>

      + root_block_device <span style="color:#f92672">{</span>
          + delete_on_termination <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + device_name           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + encrypted             <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + iops                  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + kms_key_id            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + volume_id             <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + volume_size           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + volume_type           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
        <span style="color:#f92672">}</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># aws_key_pair.yetiops-aws-prom will be created</span>
  + resource <span style="color:#e6db74">&#34;aws_key_pair&#34;</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span> <span style="color:#f92672">{</span>
      + arn         <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + fingerprint <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + key_name    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span>
      + key_pair_id <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + public_key  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;</span>$MY_PUBLIC_KEY<span style="color:#e6db74">&#34;</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># aws_security_group.yetiops-aws-prom will be created</span>
  + resource <span style="color:#e6db74">&#34;aws_security_group&#34;</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span> <span style="color:#f92672">{</span>
      + arn                    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + description            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;AWS Security Group for yetiops-aws-prom&#34;</span>
      + egress                 <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id                     <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + ingress                <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + name                   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span>
      + owner_id               <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + revoke_rules_on_delete <span style="color:#f92672">=</span> false
      + tags                   <span style="color:#f92672">=</span> <span style="color:#f92672">{</span>
          + <span style="color:#e6db74">&#34;Name&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span>
        <span style="color:#f92672">}</span>
      + vpc_id                 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vpc-a62062ce&#34;</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># aws_security_group_rule.egress_allow_all will be created</span>
  + resource <span style="color:#e6db74">&#34;aws_security_group_rule&#34;</span> <span style="color:#e6db74">&#34;egress_allow_all&#34;</span> <span style="color:#f92672">{</span>
      + cidr_blocks              <span style="color:#f92672">=</span> <span style="color:#f92672">[</span>
          + <span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>,
        <span style="color:#f92672">]</span>
      + from_port                <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
      + id                       <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + protocol                 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;-1&#34;</span>
      + security_group_id        <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + self                     <span style="color:#f92672">=</span> false
      + source_security_group_id <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + to_port                  <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
      + type                     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;egress&#34;</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># aws_security_group_rule.ingress_node_exporter_in will be created</span>
  + resource <span style="color:#e6db74">&#34;aws_security_group_rule&#34;</span> <span style="color:#e6db74">&#34;ingress_node_exporter_in&#34;</span> <span style="color:#f92672">{</span>
      + cidr_blocks              <span style="color:#f92672">=</span> <span style="color:#f92672">[</span>
          + <span style="color:#e6db74">&#34;</span>$MY_PUBLIC_IP<span style="color:#e6db74">/32&#34;</span>,
        <span style="color:#f92672">]</span>
      + from_port                <span style="color:#f92672">=</span> <span style="color:#ae81ff">9100</span>
      + id                       <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + protocol                 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
      + security_group_id        <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + self                     <span style="color:#f92672">=</span> false
      + source_security_group_id <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + to_port                  <span style="color:#f92672">=</span> <span style="color:#ae81ff">9100</span>
      + type                     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ingress&#34;</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># aws_security_group_rule.ingress_ssh_in will be created</span>
  + resource <span style="color:#e6db74">&#34;aws_security_group_rule&#34;</span> <span style="color:#e6db74">&#34;ingress_ssh_in&#34;</span> <span style="color:#f92672">{</span>
      + cidr_blocks              <span style="color:#f92672">=</span> <span style="color:#f92672">[</span>
          + <span style="color:#e6db74">&#34;</span>$MY_PUBLIC_IP<span style="color:#e6db74">/32&#34;</span>,
        <span style="color:#f92672">]</span>
      + from_port                <span style="color:#f92672">=</span> <span style="color:#ae81ff">22</span>
      + id                       <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + protocol                 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
      + security_group_id        <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + self                     <span style="color:#f92672">=</span> false
      + source_security_group_id <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + to_port                  <span style="color:#f92672">=</span> <span style="color:#ae81ff">22</span>
      + type                     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ingress&#34;</span>
    <span style="color:#f92672">}</span>

Plan: <span style="color:#ae81ff">6</span> to add, <span style="color:#ae81ff">0</span> to change, <span style="color:#ae81ff">0</span> to destroy.

------------------------------------------------------------------------

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only <span style="color:#e6db74">&#39;yes&#39;</span> will be accepted to approve.

  Enter a value: yes

aws_key_pair.yetiops-aws-prom: Creating...
aws_security_group.yetiops-aws-prom: Creating...
aws_key_pair.yetiops-aws-prom: Creation complete after 0s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>yetiops-aws-prom<span style="color:#f92672">]</span>
aws_security_group.yetiops-aws-prom: Creation complete after 1s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>sg-04e928f187b556e09<span style="color:#f92672">]</span>
aws_security_group_rule.ingress_ssh_in: Creating...
aws_security_group_rule.egress_allow_all: Creating...
aws_security_group_rule.ingress_node_exporter_in: Creating...
aws_instance.yetiops-aws-prom: Creating...
aws_security_group_rule.ingress_ssh_in: Creation complete after 1s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>sgrule-703042014<span style="color:#f92672">]</span>
aws_security_group_rule.egress_allow_all: Creation complete after 1s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>sgrule-2079816963<span style="color:#f92672">]</span>
aws_security_group_rule.ingress_node_exporter_in: Creation complete after 2s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>sgrule-147676407<span style="color:#f92672">]</span>
aws_instance.yetiops-aws-prom: Still creating... <span style="color:#f92672">[</span>10s elapsed<span style="color:#f92672">]</span>
aws_instance.yetiops-aws-prom: Still creating... <span style="color:#f92672">[</span>20s elapsed<span style="color:#f92672">]</span>
aws_instance.yetiops-aws-prom: Creation complete after 22s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>i-04cb1d016718cef07<span style="color:#f92672">]</span>

Apply complete! Resources: <span style="color:#ae81ff">6</span> added, <span style="color:#ae81ff">0</span> changed, <span style="color:#ae81ff">0</span> destroyed.
</code></pre></div><p>We can double check that Terraform is managing these resources now with <code>terraform state list</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ terraform state list
data.aws_ami.ubuntu
data.aws_vpc.default
data.template_cloudinit_config.ubuntu
data.template_file.ubuntu
aws_instance.yetiops-aws-prom
aws_key_pair.yetiops-aws-prom
aws_security_group.yetiops-aws-prom
aws_security_group_rule.egress_allow_all
aws_security_group_rule.ingress_node_exporter_in
aws_security_group_rule.ingress_ssh_in
</code></pre></div><p>We can check to see if the instance is in the AWS Console: -</p>
<p><img src="/img/prometheus/aws-instance.png" alt="AWS Instance"></p>
<p>Finally, we can try to SSH to it: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ssh ubuntu@$EC2-PUBLIC-IP
Welcome to Ubuntu 18.04.4 LTS <span style="color:#f92672">(</span>GNU/Linux 5.3.0-1023-aws x86_64<span style="color:#f92672">)</span>

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Sat Jun <span style="color:#ae81ff">20</span> 13:36:44 UTC <span style="color:#ae81ff">2020</span>

  System load:  0.08              Processes:           <span style="color:#ae81ff">91</span>
  Usage of /:   16.1% of 7.69GB   Users logged in:     <span style="color:#ae81ff">0</span>
  Memory usage: 17%               IP address <span style="color:#66d9ef">for</span> eth0: 172.31.45.249
  Swap usage:   0%

<span style="color:#ae81ff">9</span> packages can be updated.
<span style="color:#ae81ff">2</span> updates are security updates.



The programs included with the Ubuntu system are free software;
the exact distribution terms <span style="color:#66d9ef">for</span> each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

To run a command as administrator <span style="color:#f92672">(</span>user <span style="color:#e6db74">&#34;root&#34;</span><span style="color:#f92672">)</span>, use <span style="color:#e6db74">&#34;sudo &lt;command&gt;&#34;</span>.
See <span style="color:#e6db74">&#34;man sudo_root&#34;</span> <span style="color:#66d9ef">for</span> details.

ubuntu@ip-172-31-45-249:~$ ps aux | grep -i node
prometh+  <span style="color:#ae81ff">1933</span>  0.0  1.5 <span style="color:#ae81ff">245600</span> <span style="color:#ae81ff">15368</span> ?        Ssl  13:31   0:00 /usr/bin/prometheus-node-exporter --collector.diskstats.ignored-devices<span style="color:#f92672">=</span>^<span style="color:#f92672">(</span>ram|loop|fd|<span style="color:#f92672">(</span>h|s|v|xv<span style="color:#f92672">)</span>d<span style="color:#f92672">[</span>a-z<span style="color:#f92672">]</span>|nvmed+nd+p<span style="color:#f92672">)</span>d+$ --collector.filesystem.ignored-mount-points<span style="color:#f92672">=</span>^/<span style="color:#f92672">(</span>sys|proc|dev|run<span style="color:#f92672">)(</span>$|/<span style="color:#f92672">)</span> --collector.netdev.ignored-devices<span style="color:#f92672">=</span>^lo$ --collector.textfile.directory<span style="color:#f92672">=</span>/var/lib/prometheus/node-exporter
ubuntu    <span style="color:#ae81ff">2220</span>  0.0  0.0  <span style="color:#ae81ff">14852</span>  <span style="color:#ae81ff">1000</span> pts/0    S+   13:37   0:00 grep --color<span style="color:#f92672">=</span>auto -i node
</code></pre></div><p>As we can see, we have the <code>node_exporter</code> running already, due to the <code>user_data</code> configuration.</p>
<p>It is worth double checking the tags are configured, as we will need them later when Prometheus wants to discover instances: -</p>
<p><img src="/img/prometheus/aws-instance-tags.png" alt="AWS Instance Tags"></p>
<h3 id="configure-terraform---prometheus-user">Configure Terraform - Prometheus User</h3>
<p>Next, we need to create a user for Prometheus to query the AWS API. To do this, go into the <code>terraform/prometheus-access</code> directory, and create the <code>providers.tf</code> file as we did before: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#75715e"># AWS Provider
</span><span style="color:#75715e"></span><span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;aws&#34;</span> {
  region  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;eu-west-2&#34;</span>
}
</code></pre></div><p>Next, we need to create an IAM user and give it Read-Only access to the EC2 API: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_iam_user&#34; &#34;prometheus-sd-user&#34;</span> {
  name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;prometheus-sd-user&#34;</span>
  path <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/&#34;</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_iam_user_policy_attachment&#34; &#34;prometheus-sd-user-ec2-ro&#34;</span> {
  user <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_iam_user</span>.<span style="color:#66d9ef">prometheus</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">sd</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">user</span>.<span style="color:#66d9ef">name</span>
  policy_arn <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess&#34;</span>
}
</code></pre></div><p>We could create our own policy if we want to reduce the IAM permissions even further, but the inbuilt <strong>AmazonEC2ReadOnlyAccess</strong> is sufficient for now. Apply these the same way as before, using <code>terraform apply</code>.</p>
<p>Once this is done, get the AWS Access Key and Secret Key from the AWS Console. Go to the <strong>Services</strong> menu and then search for <strong>IAM</strong>: -</p>
<p><img src="/img/prometheus/aws-iam-menu.png" alt="AWS IAM Menu">.</p>
<p>After this, go to <strong>Users</strong>, click on the user you created (in our case <code>prometheus-sd-user</code>), and then go to the <strong>Security Credentials</strong> tab. Click <strong>Create Access Key</strong>, and save the credentials. We will need them when configuring our Prometheus instance later.</p>
<p><img src="/img/prometheus/aws-iam-sec-cred.png" alt="AWS IAM Security Credentials"></p>
<h2 id="azure">Azure</h2>
<p>Cloud Instances within Azure are known as Virtual Machines.</p>
<p>If you do not already have an Azure account, first you&rsquo;ll need to create a <a href="https://signup.live.com/">Live</a> account.</p>
<p>Once you have done this, you can then sign in to the <a href="https://portal.azure.com">Azure Portal</a>. You&rsquo;ll then need to create an Azure Subscription.</p>
<p><img src="/img/prometheus/azure-subscription.png" alt="Azure Subscription"></p>
<p>A subscription is your environment within Azure. You can also use it as a billing account, if the billing needs to differ from your primary billing method. It serves as a separation between environments, allowing you to restrict access to production environments, while giving access to pre-production and development environments.</p>
<p>Once you have signed up and created your first subscription, you&rsquo;ll need to install the Azure CLI tool. Terraform can use the CLI authentication configuration to manage Azure resources.</p>
<h3 id="installing-the-azure-cli">Installing the Azure CLI</h3>
<p>The Azure CLI can be installed via <a href="https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest">multiple methods</a>. Once you have followed the instructions to install it, you should be able to use the <code>az</code> command: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ az

     /<span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>    /  <span style="color:#ae81ff">\ </span>   _____   _ _  ___ _
   / /<span style="color:#ae81ff">\ \ </span> |_  / | | | <span style="color:#ae81ff">\&#39;</span>__/ _<span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  / ____ <span style="color:#ae81ff">\ </span> / /| |_| | | |  __/
 /_/    <span style="color:#ae81ff">\_\/</span>___|<span style="color:#ae81ff">\_</span>_,_|_|  <span style="color:#ae81ff">\_</span>__|


Welcome to the cool new Azure CLI!

Use <span style="color:#e6db74">`</span>az --version<span style="color:#e6db74">`</span> to display the current version.
Here are the base commands:

    account             : Manage Azure subscription information.
    acr                 : Manage private registries with Azure Container Registries.
    ad                  : Manage Azure Active Directory Graph entities needed <span style="color:#66d9ef">for</span> Role Based Access
                         Control.
<span style="color:#f92672">[</span>...<span style="color:#f92672">]</span>
</code></pre></div><h3 id="authenticate-the-cli">Authenticate the CLI</h3>
<p>To authenticate the CLI against Azure, use <code>az login</code>. Depending on whether the machine you run this on is headless or has a GUI, you will be given either of the following: -</p>
<p><strong>Headless</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">$ az login
To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code $AUTH_CODE to authenticate.
</code></pre></div><p><strong>With GUI</strong>
<img src="/img/prometheus/az-login.png" alt="Azure Login Screen"></p>
<p>With either of the above, you can login with your <strong>Live</strong> account. After this, the CLI will be authenticated, producing a list of your subscriptions: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">You have logged in. Now let us find all the subscriptions to which you have access...
<span style="color:#f92672">[</span>
  <span style="color:#f92672">{</span>
    <span style="color:#e6db74">&#34;cloudName&#34;</span>: <span style="color:#e6db74">&#34;AzureCloud&#34;</span>,
    <span style="color:#e6db74">&#34;homeTenantId&#34;</span>: <span style="color:#e6db74">&#34;</span>$TENANT_ID<span style="color:#e6db74">&#34;</span>,
    <span style="color:#e6db74">&#34;id&#34;</span>: <span style="color:#e6db74">&#34;</span>$SUBSCRIPTION_ID<span style="color:#e6db74">&#34;</span>,
    <span style="color:#e6db74">&#34;isDefault&#34;</span>: true,
    <span style="color:#e6db74">&#34;managedByTenants&#34;</span>: <span style="color:#f92672">[]</span>,
    <span style="color:#e6db74">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Free Trial&#34;</span>,
    <span style="color:#e6db74">&#34;state&#34;</span>: <span style="color:#e6db74">&#34;Enabled&#34;</span>,
    <span style="color:#e6db74">&#34;tenantId&#34;</span>: <span style="color:#e6db74">&#34;</span>$TENANT_ID<span style="color:#e6db74">&#34;</span>,
    <span style="color:#e6db74">&#34;user&#34;</span>: <span style="color:#f92672">{</span>
      <span style="color:#e6db74">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;stuart@yetiops.net&#34;</span>,
      <span style="color:#e6db74">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;user&#34;</span>
    <span style="color:#f92672">}</span>
  <span style="color:#f92672">}</span>
<span style="color:#f92672">]</span>
</code></pre></div><p>You should now be able to use Terraform with Azure.</p>
<h3 id="configure-terraform---virtual-machines">Configure Terraform - Virtual Machines</h3>
<p>Now that we are authenticated with Azure via the <code>az</code> CLI, we can add an Azure section to our <code>terraform/basic-vms/providers.tf</code> file: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#75715e"># AWS Provider
</span><span style="color:#75715e"></span><span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;aws&#34;</span> {
  region  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;eu-west-2&#34;</span>
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e"># Azure Provider
</span><span style="color:#75715e"></span><span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;azurerm&#34;</span> {
  subscription_id <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;$SUBSCRIPTION_ID&#34;</span>
  <span style="color:#66d9ef">features</span> {}
</code></pre></div><p>To find the correct subscription ID, use either the output from the <code>az login</code> command, or run <code>az account list</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ az account list
<span style="color:#f92672">[</span>
  <span style="color:#f92672">{</span>
    <span style="color:#e6db74">&#34;cloudName&#34;</span>: <span style="color:#e6db74">&#34;AzureCloud&#34;</span>,
    <span style="color:#e6db74">&#34;homeTenantId&#34;</span>: <span style="color:#e6db74">&#34;</span>$TENANT_ID<span style="color:#e6db74">&#34;</span>,
    <span style="color:#e6db74">&#34;id&#34;</span>: <span style="color:#e6db74">&#34;</span>$SUBSCRIPTION_ID<span style="color:#e6db74">&#34;</span>,
    <span style="color:#e6db74">&#34;isDefault&#34;</span>: true,
    <span style="color:#e6db74">&#34;managedByTenants&#34;</span>: <span style="color:#f92672">[]</span>,
    <span style="color:#e6db74">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Free Trial&#34;</span>,
    <span style="color:#e6db74">&#34;state&#34;</span>: <span style="color:#e6db74">&#34;Enabled&#34;</span>,
    <span style="color:#e6db74">&#34;tenantId&#34;</span>: <span style="color:#e6db74">&#34;</span>$TENANT_ID<span style="color:#e6db74">&#34;</span>,
    <span style="color:#e6db74">&#34;user&#34;</span>: <span style="color:#f92672">{</span>
      <span style="color:#e6db74">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;stuart@yetiops.net&#34;</span>,
      <span style="color:#e6db74">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;user&#34;</span>
    <span style="color:#f92672">}</span>
  <span style="color:#f92672">}</span>
<span style="color:#f92672">]</span>
</code></pre></div><p>Run <code>terraform init</code> to bring in the Azure provider: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ terraform init

Initializing the backend...

Initializing provider plugins...
- Checking <span style="color:#66d9ef">for</span> available provider plugins...
- Downloading plugin <span style="color:#66d9ef">for</span> provider <span style="color:#e6db74">&#34;aws&#34;</span> <span style="color:#f92672">(</span>hashicorp/aws<span style="color:#f92672">)</span> 2.67.0...
- Downloading plugin <span style="color:#66d9ef">for</span> provider <span style="color:#e6db74">&#34;azurerm&#34;</span> <span style="color:#f92672">(</span>hashicorp/azurerm<span style="color:#f92672">)</span> 2.15.0...
- Downloading plugin <span style="color:#66d9ef">for</span> provider <span style="color:#e6db74">&#34;template&#34;</span> <span style="color:#f92672">(</span>hashicorp/template<span style="color:#f92672">)</span> 2.1.2...

The following providers <span style="color:#66d9ef">do</span> not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;...&#34;</span> constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* provider.aws: version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;~&gt; 2.67&#34;</span>
* provider.azurerm: version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;~&gt; 2.15&#34;</span>
* provider.template: version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;~&gt; 2.1&#34;</span>

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running <span style="color:#e6db74">&#34;terraform plan&#34;</span> to see
any changes that are required <span style="color:#66d9ef">for</span> your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration <span style="color:#66d9ef">for</span> Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to <span style="color:#66d9ef">do</span> so <span style="color:#66d9ef">if</span> necessary.
</code></pre></div><h3 id="define-the-infrastructure---virtual-machines">Define the infrastructure - Virtual Machines</h3>
<p>You can now put the infrastructure together to create your first Azure Virtual Machine instance. The below is from the file <code>azure.tf</code> in the <code>terraform/basic-vms</code> directory: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;azurerm_resource_group&#34; &#34;yetiops-azure-prom&#34;</span> {
  name     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-azure-prom&#34;</span>
  location <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;West Europe&#34;</span>
}


<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;azurerm_virtual_network&#34; &#34;yetiops-azure-prom&#34;</span> {
  name                <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-azure-prom&#34;</span>
  address_space       <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;10.0.0.0/16&#34;</span>]
  location            <span style="color:#f92672">=</span> <span style="color:#66d9ef">azurerm_resource_group</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">azure</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">location</span>
  resource_group_name <span style="color:#f92672">=</span> <span style="color:#66d9ef">azurerm_resource_group</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">azure</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">name</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;azurerm_subnet&#34; &#34;yetiops-azure-prom&#34;</span> {
  name                 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-azure-prom-main&#34;</span>
  resource_group_name  <span style="color:#f92672">=</span> <span style="color:#66d9ef">azurerm_resource_group</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">azure</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">name</span>
  virtual_network_name <span style="color:#f92672">=</span> <span style="color:#66d9ef">azurerm_virtual_network</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">azure</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">name</span>
  address_prefixes     <span style="color:#f92672">=</span> [
    <span style="color:#e6db74">&#34;10.0.2.0/24&#34;</span>
  ]
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;azurerm_public_ip&#34; &#34;yetiops-azure-prom-pip&#34;</span> {
  name                <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-azure-prom-pip&#34;</span>
  resource_group_name <span style="color:#f92672">=</span> <span style="color:#66d9ef">azurerm_resource_group</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">azure</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">name</span>
  location            <span style="color:#f92672">=</span> <span style="color:#66d9ef">azurerm_resource_group</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">azure</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">location</span>
  allocation_method   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Dynamic&#34;</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;azurerm_network_interface&#34; &#34;yetiops-azure-prom&#34;</span> {
  name                <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-azure-prom-nic&#34;</span>
  location            <span style="color:#f92672">=</span> <span style="color:#66d9ef">azurerm_resource_group</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">azure</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">location</span>
  resource_group_name <span style="color:#f92672">=</span> <span style="color:#66d9ef">azurerm_resource_group</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">azure</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">name</span>

  <span style="color:#66d9ef">ip_configuration</span> {
    name                          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-azure-prom-main&#34;</span>
    subnet_id                     <span style="color:#f92672">=</span> <span style="color:#66d9ef">azurerm_subnet</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">azure</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">id</span>
    private_ip_address_allocation <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Dynamic&#34;</span>
    public_ip_address_id          <span style="color:#f92672">=</span> <span style="color:#66d9ef">azurerm_public_ip</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">azure</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">pip</span>.<span style="color:#66d9ef">id</span>
  }
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;azurerm_network_security_group&#34; &#34;yetiops-azure-prom&#34;</span> {
  name                <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-azure-prom&#34;</span>
  location            <span style="color:#f92672">=</span> <span style="color:#66d9ef">azurerm_resource_group</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">azure</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">location</span>
  resource_group_name <span style="color:#f92672">=</span> <span style="color:#66d9ef">azurerm_resource_group</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">azure</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">name</span>
  <span style="color:#66d9ef">security_rule</span> {
    access                     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Allow&#34;</span>
    direction                  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Inbound&#34;</span>
    name                       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ssh&#34;</span>
    priority                   <span style="color:#f92672">=</span> <span style="color:#ae81ff">100</span>
    protocol                   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Tcp&#34;</span>
    source_port_range          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;*&#34;</span>
    source_address_prefixes    <span style="color:#f92672">=</span> [
      <span style="color:#e6db74">&#34;$MY_PUBLIC_IP&#34;</span>
    ]
    destination_port_range     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;22&#34;</span>
    destination_address_prefix <span style="color:#f92672">=</span> <span style="color:#66d9ef">azurerm_network_interface</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">azure</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">private_ip_address</span>
  }
  <span style="color:#66d9ef">security_rule</span> {
    access                     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Allow&#34;</span>
    direction                  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Inbound&#34;</span>
    name                       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;NodeExporter&#34;</span>
    priority                   <span style="color:#f92672">=</span> <span style="color:#ae81ff">101</span>
    protocol                   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Tcp&#34;</span>
    source_port_range          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;*&#34;</span>
    source_address_prefixes    <span style="color:#f92672">=</span> [
      <span style="color:#e6db74">&#34;$MY_PUBLIC_IP&#34;</span>
    ]
    destination_port_range     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;9100&#34;</span>
    destination_address_prefix <span style="color:#f92672">=</span> <span style="color:#66d9ef">azurerm_network_interface</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">azure</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">private_ip_address</span>
  }
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;azurerm_linux_virtual_machine&#34; &#34;yetiops-azure-prom&#34;</span> {
  name                <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-azure-prom-machine&#34;</span>
  resource_group_name <span style="color:#f92672">=</span> <span style="color:#66d9ef">azurerm_resource_group</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">azure</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">name</span>
  location            <span style="color:#f92672">=</span> <span style="color:#66d9ef">azurerm_resource_group</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">azure</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">location</span>
  size                <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Standard_F2&#34;</span>
  admin_username      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;adminuser&#34;</span>
  custom_data         <span style="color:#f92672">=</span> <span style="color:#66d9ef">base64encode</span>(<span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">template_cloudinit_config</span>.<span style="color:#66d9ef">ubuntu</span>.<span style="color:#66d9ef">rendered</span>)

  network_interface_ids <span style="color:#f92672">=</span> [
    <span style="color:#66d9ef">azurerm_network_interface</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">azure</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">id</span>,
  ]

  <span style="color:#66d9ef">admin_ssh_key</span> {
    username   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;adminuser&#34;</span>
    public_key <span style="color:#f92672">=</span> <span style="color:#66d9ef">file</span>(<span style="color:#e6db74">&#34;~/.ssh/id_rsa.pub&#34;</span>)
  }

  <span style="color:#66d9ef">os_disk</span> {
    caching              <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ReadWrite&#34;</span>
    storage_account_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Standard_LRS&#34;</span>
  }

  tags <span style="color:#f92672">=</span> {
    prometheus <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
    node_exporter <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
    public_ip <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${azurerm_public_ip.yetiops-azure-prom-pip.ip_address}&#34;</span>
  }

  <span style="color:#66d9ef">source_image_reference</span> {
    publisher <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Canonical&#34;</span>
    offer     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;UbuntuServer&#34;</span>
    sku       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;18.04-LTS&#34;</span>
    version   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;latest&#34;</span>
  }
}
</code></pre></div><p>To summarize what this does: -</p>
<ul>
<li>Creates a resource group
<ul>
<li>Resource groups are a logical grouping of resources in Azure (e.g. Virtual Machines, firewalls and databases for a single application or project)</li>
</ul>
</li>
<li>Creates a virtual network and subnet for the Virtual Machine</li>
<li>Creates a public IP so we can reach the machine externally</li>
<li>Creates a network interfaces to attach to the Virtual Machine</li>
<li>Creates a network security group (a firewall) to allow traffic to the virtual machine from my public IP to TCP port 22 (SSH) and TCP port 9100 (Node Exporter)</li>
<li>Creates a Linux virtual machine of size <code>Standard_F2</code> (2 CPU cores, 4G of Memory), with some caveats
<ul>
<li>We can only use RSA keys for SSH</li>
<li>Ubuntu 20.04 LTS (at the time of writing) is not available on Azure</li>
</ul>
</li>
</ul>
<p>In this, we also need to Base64-encode the <strong>cloud-config</strong>, otherwise the machine will not be built correctly. Because we use common configuration though, we can use the same <strong>cloud-config</strong> file as we did for the AWS EC2.</p>
<p>You may also notice that we place the public IP of the instance in a tag on the machine. I will explain why in the Prometheus configuration section.</p>
<h3 id="build-the-infrastructure---virtual-machines">Build the infrastructure - Virtual Machines</h3>
<p>We can now apply our configuration, and see if it builds an Azure Virtual Machine. This time, rather than running a plan first, we&rsquo;ll use <code>terraform apply</code>, as this shows a plan before you apply: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ terraform apply
data.template_file.ubuntu: Refreshing state...
data.template_cloudinit_config.ubuntu: Refreshing state...
data.aws_vpc.default: Refreshing state...
data.aws_ami.ubuntu: Refreshing state...
aws_key_pair.yetiops-aws-prom: Refreshing state... <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>yetiops-aws-prom<span style="color:#f92672">]</span>
aws_security_group.yetiops-aws-prom: Refreshing state... <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>sg-04e928f187b556e09<span style="color:#f92672">]</span>
aws_security_group_rule.ingress_ssh_in: Refreshing state... <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>sgrule-703042014<span style="color:#f92672">]</span>
aws_security_group_rule.egress_allow_all: Refreshing state... <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>sgrule-2079816963<span style="color:#f92672">]</span>
aws_security_group_rule.ingress_node_exporter_in: Refreshing state... <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>sgrule-147676407<span style="color:#f92672">]</span>
aws_instance.yetiops-aws-prom: Refreshing state... <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>i-04cb1d016718cef07<span style="color:#f92672">]</span>

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  <span style="color:#75715e"># azurerm_linux_virtual_machine.yetiops-azure-prom will be created</span>
  + resource <span style="color:#e6db74">&#34;azurerm_linux_virtual_machine&#34;</span> <span style="color:#e6db74">&#34;yetiops-azure-prom&#34;</span> <span style="color:#f92672">{</span>
      + admin_username                  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;adminuser&#34;</span>
      + allow_extension_operations      <span style="color:#f92672">=</span> true
      + computer_name                   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + custom_data                     <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>sensitive value<span style="color:#f92672">)</span>
      + disable_password_authentication <span style="color:#f92672">=</span> true
      + id                              <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + location                        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;westeurope&#34;</span>
      + max_bid_price                   <span style="color:#f92672">=</span> -1
      + name                            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-azure-prom-machine&#34;</span>
      + network_interface_ids           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + priority                        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Regular&#34;</span>
      + private_ip_address              <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + private_ip_addresses            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + provision_vm_agent              <span style="color:#f92672">=</span> true
      + public_ip_address               <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + public_ip_addresses             <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + resource_group_name             <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-azure-prom&#34;</span>
      + size                            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Standard_F2&#34;</span>
      + tags                            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + virtual_machine_id              <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + zone                            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>

      + admin_ssh_key <span style="color:#f92672">{</span>
          + public_key <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;</span>$SSH_RSA_KEY<span style="color:#e6db74">&#34;</span>
          + username   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;adminuser&#34;</span>
        <span style="color:#f92672">}</span>

      + os_disk <span style="color:#f92672">{</span>
          + caching                   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ReadWrite&#34;</span>
          + disk_size_gb              <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + name                      <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + storage_account_type      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Standard_LRS&#34;</span>
          + write_accelerator_enabled <span style="color:#f92672">=</span> false
        <span style="color:#f92672">}</span>

      + source_image_reference <span style="color:#f92672">{</span>
          + offer     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;UbuntuServer&#34;</span>
          + publisher <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Canonical&#34;</span>
          + sku       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;18.04-LTS&#34;</span>
          + version   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;latest&#34;</span>
        <span style="color:#f92672">}</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># azurerm_network_interface.yetiops-azure-prom will be created</span>
  + resource <span style="color:#e6db74">&#34;azurerm_network_interface&#34;</span> <span style="color:#e6db74">&#34;yetiops-azure-prom&#34;</span> <span style="color:#f92672">{</span>
      + applied_dns_servers           <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + dns_servers                   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + enable_accelerated_networking <span style="color:#f92672">=</span> false
      + enable_ip_forwarding          <span style="color:#f92672">=</span> false
      + id                            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + internal_dns_name_label       <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + internal_domain_name_suffix   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + location                      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;westeurope&#34;</span>
      + mac_address                   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + name                          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-azure-prom-nic&#34;</span>
      + private_ip_address            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + private_ip_addresses          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + resource_group_name           <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-azure-prom&#34;</span>
      + virtual_machine_id            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>

      + ip_configuration <span style="color:#f92672">{</span>
          + name                          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-azure-prom-main&#34;</span>
          + primary                       <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + private_ip_address            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + private_ip_address_allocation <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;dynamic&#34;</span>
          + private_ip_address_version    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;IPv4&#34;</span>
          + public_ip_address_id          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + subnet_id                     <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
        <span style="color:#f92672">}</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># azurerm_network_security_group.yetiops-azure-prom will be created</span>
  + resource <span style="color:#e6db74">&#34;azurerm_network_security_group&#34;</span> <span style="color:#e6db74">&#34;yetiops-azure-prom&#34;</span> <span style="color:#f92672">{</span>
      + id                  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + location            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;westeurope&#34;</span>
      + name                <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-azure-prom&#34;</span>
      + resource_group_name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-azure-prom&#34;</span>
      + security_rule       <span style="color:#f92672">=</span> <span style="color:#f92672">[</span>
          + <span style="color:#f92672">{</span>
              + access                                     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Allow&#34;</span>
              + description                                <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;&#34;</span>
              + destination_address_prefix                 <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
              + destination_address_prefixes               <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
              + destination_application_security_group_ids <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
              + destination_port_range                     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;22&#34;</span>
              + destination_port_ranges                    <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
              + direction                                  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Inbound&#34;</span>
              + name                                       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ssh&#34;</span>
              + priority                                   <span style="color:#f92672">=</span> <span style="color:#ae81ff">100</span>
              + protocol                                   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Tcp&#34;</span>
              + source_address_prefix                      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;&#34;</span>
              + source_address_prefixes                    <span style="color:#f92672">=</span> <span style="color:#f92672">[</span>
                  + <span style="color:#e6db74">&#34;</span>$MY_PUBLIC_IP<span style="color:#e6db74">&#34;</span>,
                <span style="color:#f92672">]</span>
              + source_application_security_group_ids      <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
              + source_port_range                          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;*&#34;</span>
              + source_port_ranges                         <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
            <span style="color:#f92672">}</span>,
          + <span style="color:#f92672">{</span>
              + access                                     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Allow&#34;</span>
              + description                                <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;&#34;</span>
              + destination_address_prefix                 <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
              + destination_address_prefixes               <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
              + destination_application_security_group_ids <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
              + destination_port_range                     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;9100&#34;</span>
              + destination_port_ranges                    <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
              + direction                                  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Inbound&#34;</span>
              + name                                       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;NodeExporter&#34;</span>
              + priority                                   <span style="color:#f92672">=</span> <span style="color:#ae81ff">101</span>
              + protocol                                   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Tcp&#34;</span>
              + source_address_prefix                      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;&#34;</span>
              + source_address_prefixes                    <span style="color:#f92672">=</span> <span style="color:#f92672">[</span>
                  + <span style="color:#e6db74">&#34;</span>$MY_PUBLIC_IP<span style="color:#e6db74">&#34;</span>,
                <span style="color:#f92672">]</span>
              + source_application_security_group_ids      <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
              + source_port_range                          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;*&#34;</span>
              + source_port_ranges                         <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
            <span style="color:#f92672">}</span>,
        <span style="color:#f92672">]</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># azurerm_public_ip.yetiops-azure-prom-pip will be created</span>
  + resource <span style="color:#e6db74">&#34;azurerm_public_ip&#34;</span> <span style="color:#e6db74">&#34;yetiops-azure-prom-pip&#34;</span> <span style="color:#f92672">{</span>
      + allocation_method       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Dynamic&#34;</span>
      + fqdn                    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id                      <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + idle_timeout_in_minutes <span style="color:#f92672">=</span> <span style="color:#ae81ff">4</span>
      + ip_address              <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + ip_version              <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;IPv4&#34;</span>
      + location                <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;westeurope&#34;</span>
      + name                    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-azure-prom-pip&#34;</span>
      + resource_group_name     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-azure-prom&#34;</span>
      + sku                     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Basic&#34;</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># azurerm_resource_group.yetiops-azure-prom will be created</span>
  + resource <span style="color:#e6db74">&#34;azurerm_resource_group&#34;</span> <span style="color:#e6db74">&#34;yetiops-azure-prom&#34;</span> <span style="color:#f92672">{</span>
      + id       <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + location <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;westeurope&#34;</span>
      + name     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-azure-prom&#34;</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># azurerm_subnet.yetiops-azure-prom will be created</span>
  + resource <span style="color:#e6db74">&#34;azurerm_subnet&#34;</span> <span style="color:#e6db74">&#34;yetiops-azure-prom&#34;</span> <span style="color:#f92672">{</span>
      + address_prefix                                 <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + address_prefixes                               <span style="color:#f92672">=</span> <span style="color:#f92672">[</span>
          + <span style="color:#e6db74">&#34;10.0.2.0/24&#34;</span>,
        <span style="color:#f92672">]</span>
      + enforce_private_link_endpoint_network_policies <span style="color:#f92672">=</span> false
      + enforce_private_link_service_network_policies  <span style="color:#f92672">=</span> false
      + id                                             <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + name                                           <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-azure-prom-main&#34;</span>
      + resource_group_name                            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-azure-prom&#34;</span>
      + virtual_network_name                           <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-azure-prom&#34;</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># azurerm_virtual_network.yetiops-azure-prom will be created</span>
  + resource <span style="color:#e6db74">&#34;azurerm_virtual_network&#34;</span> <span style="color:#e6db74">&#34;yetiops-azure-prom&#34;</span> <span style="color:#f92672">{</span>
      + address_space       <span style="color:#f92672">=</span> <span style="color:#f92672">[</span>
          + <span style="color:#e6db74">&#34;10.0.0.0/16&#34;</span>,
        <span style="color:#f92672">]</span>
      + guid                <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id                  <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + location            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;westeurope&#34;</span>
      + name                <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-azure-prom&#34;</span>
      + resource_group_name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-azure-prom&#34;</span>
      + subnet              <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
    <span style="color:#f92672">}</span>

Plan: <span style="color:#ae81ff">7</span> to add, <span style="color:#ae81ff">0</span> to change, <span style="color:#ae81ff">0</span> to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only <span style="color:#e6db74">&#39;yes&#39;</span> will be accepted to approve.

  Enter a value: yes

azurerm_resource_group.yetiops-azure-prom: Creating...
azurerm_resource_group.yetiops-azure-prom: Creation complete after 1s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>/subscriptions/$SUBSCRIPTION_ID/resourceGroups/yetiops-azure-prom<span style="color:#f92672">]</span>
azurerm_virtual_network.yetiops-azure-prom: Creating...
azurerm_public_ip.yetiops-azure-prom-pip: Creating...
azurerm_public_ip.yetiops-azure-prom-pip: Creation complete after 1s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>/subscriptions/$SUBSCRIPTION_ID/resourceGroups/yetiops-azure-prom/providers/Microsoft.Network/publicIPAddresses/yetiops-azure-prom-pip<span style="color:#f92672">]</span>
azurerm_virtual_network.yetiops-azure-prom: Still creating... <span style="color:#f92672">[</span>10s elapsed<span style="color:#f92672">]</span>
azurerm_virtual_network.yetiops-azure-prom: Creation complete after 12s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>/subscriptions/$SUBSCRIPTION_ID/resourceGroups/yetiops-azure-prom/providers/Microsoft.Network/virtualNetworks/yetiops-azure-prom<span style="color:#f92672">]</span>
azurerm_subnet.yetiops-azure-prom: Creating...
azurerm_subnet.yetiops-azure-prom: Creation complete after 0s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>/subscriptions/$SUBSCRIPTION_ID/resourceGroups/yetiops-azure-prom/providers/Microsoft.Network/virtualNetworks/yetiops-azure-prom/subnets/yetiops-azure-prom-main<span style="color:#f92672">]</span>
azurerm_network_interface.yetiops-azure-prom: Creating...
azurerm_network_interface.yetiops-azure-prom: Creation complete after 2s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>/subscriptions/$SUBSCRIPTION_ID/resourceGroups/yetiops-azure-prom/providers/Microsoft.Network/networkInterfaces/yetiops-azure-prom-nic<span style="color:#f92672">]</span>
azurerm_network_security_group.yetiops-azure-prom: Creating...
azurerm_linux_virtual_machine.yetiops-azure-prom: Creating...
azurerm_network_security_group.yetiops-azure-prom: Creation complete after 1s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>/subscriptions/$SUBSCRIPTION_ID/resourceGroups/yetiops-azure-prom/providers/Microsoft.Network/networkSecurityGroups/yetiops-azure-prom<span style="color:#f92672">]</span>
azurerm_linux_virtual_machine.yetiops-azure-prom: Still creating... <span style="color:#f92672">[</span>10s elapsed<span style="color:#f92672">]</span>
azurerm_linux_virtual_machine.yetiops-azure-prom: Still creating... <span style="color:#f92672">[</span>20s elapsed<span style="color:#f92672">]</span>
azurerm_linux_virtual_machine.yetiops-azure-prom: Still creating... <span style="color:#f92672">[</span>30s elapsed<span style="color:#f92672">]</span>
azurerm_linux_virtual_machine.yetiops-azure-prom: Still creating... <span style="color:#f92672">[</span>40s elapsed<span style="color:#f92672">]</span>
azurerm_linux_virtual_machine.yetiops-azure-prom: Still creating... <span style="color:#f92672">[</span>50s elapsed<span style="color:#f92672">]</span>
azurerm_linux_virtual_machine.yetiops-azure-prom: Still creating... <span style="color:#f92672">[</span>1m0s elapsed<span style="color:#f92672">]</span>
azurerm_linux_virtual_machine.yetiops-azure-prom: Still creating... <span style="color:#f92672">[</span>1m10s elapsed<span style="color:#f92672">]</span>
azurerm_linux_virtual_machine.yetiops-azure-prom: Still creating... <span style="color:#f92672">[</span>1m20s elapsed<span style="color:#f92672">]</span>
azurerm_linux_virtual_machine.yetiops-azure-prom: Still creating... <span style="color:#f92672">[</span>1m30s elapsed<span style="color:#f92672">]</span>
azurerm_linux_virtual_machine.yetiops-azure-prom: Creation complete after 1m37s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>/subscriptions/$SUBSCRIPTION_ID/resourceGroups/yetiops-azure-prom/providers/Microsoft.Compute/virtualMachines/yetiops-azure-prom-machine<span style="color:#f92672">]</span>

Apply complete! Resources: <span style="color:#ae81ff">7</span> added, <span style="color:#ae81ff">0</span> changed, <span style="color:#ae81ff">0</span> destroyed.
</code></pre></div><p>We can double check that Terraform is managing these resources now with <code>terraform state list</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ terraform state list
data.aws_ami.ubuntu
data.aws_vpc.default
data.template_cloudinit_config.ubuntu
data.template_file.ubuntu
aws_instance.yetiops-aws-prom
aws_key_pair.yetiops-aws-prom
aws_security_group.yetiops-aws-prom
aws_security_group_rule.egress_allow_all
aws_security_group_rule.ingress_node_exporter_in
aws_security_group_rule.ingress_ssh_in
azurerm_linux_virtual_machine.yetiops-azure-prom
azurerm_network_interface.yetiops-azure-prom
azurerm_network_security_group.yetiops-azure-prom
azurerm_public_ip.yetiops-azure-prom-pip
azurerm_resource_group.yetiops-azure-prom
azurerm_subnet.yetiops-azure-prom
azurerm_virtual_network.yetiops-azure-prom
</code></pre></div><p>We can check to see if the instance and other resources are in the Azure Portal: -</p>
<p><img src="/img/prometheus/az-resource-group.png" alt="Azure Resource Group"></p>
<p>Now lets try SSH: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ssh adminuser@$AZURE_VM_IP
Welcome to Ubuntu 18.04.4 LTS <span style="color:#f92672">(</span>GNU/Linux 5.3.0-1028-azure x86_64<span style="color:#f92672">)</span>

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Sat Jun <span style="color:#ae81ff">20</span> 17:37:46 UTC <span style="color:#ae81ff">2020</span>

  System load:  0.04              Processes:           <span style="color:#ae81ff">119</span>
  Usage of /:   4.9% of 28.90GB   Users logged in:     <span style="color:#ae81ff">0</span>
  Memory usage: 7%                IP address <span style="color:#66d9ef">for</span> eth0: 10.0.2.4
  Swap usage:   0%


<span style="color:#ae81ff">9</span> packages can be updated.
<span style="color:#ae81ff">2</span> updates are security updates.



The programs included with the Ubuntu system are free software;
the exact distribution terms <span style="color:#66d9ef">for</span> each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

To run a command as administrator <span style="color:#f92672">(</span>user <span style="color:#e6db74">&#34;root&#34;</span><span style="color:#f92672">)</span>, use <span style="color:#e6db74">&#34;sudo &lt;command&gt;&#34;</span>.
See <span style="color:#e6db74">&#34;man sudo_root&#34;</span> <span style="color:#66d9ef">for</span> details.

adminuser@yetiops-azure-prom-machine:~$  ps aux | grep -i node
prometh+   <span style="color:#ae81ff">2285</span>  0.0  0.2 <span style="color:#ae81ff">242556</span> <span style="color:#ae81ff">10928</span> ?        Ssl  17:30   0:00 /usr/bin/prometheus-node-exporter --collector.diskstats.ignored-devices<span style="color:#f92672">=</span>^<span style="color:#f92672">(</span>ram|loop|fd|<span style="color:#f92672">(</span>h|s|v|xv<span style="color:#f92672">)</span>d<span style="color:#f92672">[</span>a-z<span style="color:#f92672">]</span>|nvmed+nd+p<span style="color:#f92672">)</span>d+$ --collector.filesystem.ignored-mount-points<span style="color:#f92672">=</span>^/<span style="color:#f92672">(</span>sys|proc|dev|run<span style="color:#f92672">)(</span>$|/<span style="color:#f92672">)</span> --collector.netdev.ignored-devices<span style="color:#f92672">=</span>^lo$ --collector.textfile.directory<span style="color:#f92672">=</span>/var/lib/prometheus/node-exporter
adminus+   <span style="color:#ae81ff">3071</span>  0.0  0.0  <span style="color:#ae81ff">14852</span>  <span style="color:#ae81ff">1060</span> pts/0    S+   17:38   0:00 grep --color<span style="color:#f92672">=</span>auto -i node
</code></pre></div><h3 id="configure-terraform---prometheus-user-1">Configure Terraform - Prometheus User</h3>
<p>Next, we need to create a user that Prometheus will use to query the Azure API. To do this, go into the <code>terraform/prometheus-access</code> directory, and create the <code>providers.tf</code> file as we did before: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#75715e"># AWS Provider
</span><span style="color:#75715e"></span><span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;aws&#34;</span> {
  region  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;eu-west-2&#34;</span>
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e"># Azure Provider
</span><span style="color:#75715e"></span><span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;azurerm&#34;</span> {
  subscription_id <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;$SUBSCRIPTION_ID&#34;</span>
  <span style="color:#66d9ef">features</span> {}
}

<span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;azuread&#34;</span> {
}
</code></pre></div><p>One point to note with Azure is that all resources are created using the <strong>azurerm</strong> provider. However for user access, you need to use the <strong>azuread</strong> provider instead. This is because Azure Active Directory can be used separately from Azure resources (for example, running Azure AD with Office 365), using a different API altogether.</p>
<p>The below file configures the user for Prometheus access: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;azurerm_subscription&#34; &#34;current&#34;</span> {
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;azuread_application&#34; &#34;prometheus-sd-user&#34;</span> {
  name                              <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;prometheus-sd-user&#34;</span>
  oauth2_allow_implicit_flow        <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
  available_to_other_tenants        <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>
  type                              <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;webapp/api&#34;</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;azuread_service_principal&#34; &#34;prometheus-sd-user&#34;</span> {
  application_id                    <span style="color:#f92672">=</span> <span style="color:#66d9ef">azuread_application</span>.<span style="color:#66d9ef">prometheus</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">sd</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">user</span>.<span style="color:#66d9ef">application_id</span>
  app_role_assignment_required      <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;azurerm_role_assignment&#34; &#34;prometheus-sd-user-reader&#34;</span> {
  scope                <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">azurerm_subscription</span>.<span style="color:#66d9ef">current</span>.<span style="color:#66d9ef">id</span>
  principal_id         <span style="color:#f92672">=</span> <span style="color:#66d9ef">azuread_service_principal</span>.<span style="color:#66d9ef">prometheus</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">sd</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">user</span>.<span style="color:#66d9ef">id</span>
  role_definition_name <span style="color:#f92672">=</span>  <span style="color:#e6db74">&#34;Reader&#34;</span>
}
</code></pre></div><p>In the above, we first get our current Azure subscription ID. We then create an Azure Application. The Azure Application is equivalent to a service account.</p>
<p>We create a service principal to attach to the service account, so that we can assign roles to this service account.</p>
<p>Finally, we assign the Azure Reader role, scoped to the current subscription. Do not use this role in production, instead try and use one has less access (or create your own).</p>
<p>Once this is done, you will need to generate a Client Secret for use with Prometheus. Go to the Azure Portal, and find Azure Active Directory: -</p>
<p><img src="/img/prometheus/azure-ad.png" alt="Azure Active Directory"></p>
<p>Go to <strong>App Registrations</strong>, at which point you should see your Prometheus user: -</p>
<p><img src="/img/prometheus/azure-app-registration.png" alt="Azure Application Regstrations"></p>
<p>Get the <strong>Client ID</strong> from here, and then click on the user. From here, you need to go to <strong>Certificates and Secrets</strong>, and then <strong>New Client Secret</strong>: -</p>
<p><img src="/img/prometheus/azure-client-secret.png" alt="Azure Client Secret"></p>
<p>When you create a secret, you can choose between expiring in a year, two years or never. Choose what works for your environment, bearing in mind that if you choose a year or two years, you will need to renew this at a later date. If you forget to renew it, your monitoring for Azure resources will fail.</p>
<p>Save these details, ready for configuring Prometheus later.</p>
<h2 id="google-cloud-platform">Google Cloud Platform</h2>
<p>Cloud instances within the Google Cloud Platform are known as Compute Instances.</p>
<p>To start using Google Cloud Platform, first you need a Google account. If you have a Google account already (i.e. for Gmail, Youtube or otherwise) you can use this account, or you can create one solely for demo purposes.</p>
<p>Once you have a Google account, you can then sign in to the <a href="https://console.cloud.google.com/">Google Cloud Console</a>. When you first access the Console, you&rsquo;ll need to go through the steps of activating the Google Cloud Platform for your account, and then you&rsquo;ll need to create a project: -</p>
<p><img src="/img/prometheus/gcp-project.png" alt="Google Cloud Platform Project"></p>
<p>Similar to Azure Subscriptions, projects are used for billing purposes and to separate your environments. You can use this to provide fine-grained access controls to users who should be able to access pre-production, but not production.</p>
<p>After this, you will need to install the Google Cloud SDK</p>
<h3 id="install-the-sdk">Install the SDK</h3>
<p>To install the SDK on your chosen platform, you can follow the instructions <a href="https://cloud.google.com/sdk/docs/quickstarts">here</a>. Once installed, you can run <code>gcloud version</code> to make sure it works: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ gcloud version
Google Cloud SDK 294.0.0
bq 2.0.57
core 2020.05.21
gsutil 4.51
</code></pre></div><p>You then need to authenticate the CLI with your account. This is done using <code>gcloud init</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ gcloud init
To <span style="color:#66d9ef">continue</span>, you must log in. Would you like to log in <span style="color:#f92672">(</span>Y/n<span style="color:#f92672">)</span>? Y
</code></pre></div><p>This command will open your web browser to the Google Account login page. Alternatively if you run it with <code> --console-only</code>, you can authenticate a headless machine by using a web browser on another machine.</p>
<p>It will then ask you which project and default region you would like to use: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">Pick cloud project to use:
 <span style="color:#f92672">[</span>1<span style="color:#f92672">]</span> <span style="color:#f92672">[</span>my-project-1<span style="color:#f92672">]</span>
 <span style="color:#f92672">[</span>2<span style="color:#f92672">]</span> <span style="color:#f92672">[</span>my-project-2<span style="color:#f92672">]</span>
 ...
 Please enter your numeric choice: <span style="color:#ae81ff">1</span>

Which compute zone would you like to use as project default?
 <span style="color:#f92672">[</span>1<span style="color:#f92672">]</span> <span style="color:#f92672">[</span>asia-east1-a<span style="color:#f92672">]</span>
 <span style="color:#f92672">[</span>2<span style="color:#f92672">]</span> <span style="color:#f92672">[</span>asia-east1-b<span style="color:#f92672">]</span>
 ...
 <span style="color:#f92672">[</span>14<span style="color:#f92672">]</span> Do not use default zone
 Please enter your numeric choice: <span style="color:#ae81ff">2</span>
</code></pre></div><p>After this, you should now be able to use the <code>gcloud</code> CLI with your account. We can now use Terraform with Google Cloud.</p>
<h3 id="configure-terraform---compute-instances">Configure Terraform - Compute Instances</h3>
<p>Now that we are authenticated with Google Cloud Platform via the <code>gcloud</code> CLI, we can add a Google Cloud Platform section to our <code>terraform/basic-vms/providers.tf</code> file: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#75715e"># AWS Provider
</span><span style="color:#75715e"></span><span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;aws&#34;</span> {
  region  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;eu-west-2&#34;</span>
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e"># Azure Provider
</span><span style="color:#75715e"></span><span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;azurerm&#34;</span> {
  subscription_id <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;$SUBSCRIPTION_ID&#34;</span>
  <span style="color:#66d9ef">features</span> {}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e"># Google Cloud Platform Provider
</span><span style="color:#75715e"></span><span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;google&#34;</span> {
  credentials <span style="color:#f92672">=</span> <span style="color:#66d9ef">file</span>(<span style="color:#e6db74">&#34;~/.gcloud/account.json&#34;</span>)
  project <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-blog&#34;</span>
  region  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;europe-west3&#34;</span>
  zone    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;europe-west3-c&#34;</span>
}
</code></pre></div><p>The <code>~/.gcloud/account.json</code> file should have been created by the <code>gcloud</code> CLI. If it has not, check your <code>.gcloud</code> directory to see where this resides.</p>
<p>Run <code>terraform init</code> to bring in the Google Cloud Platform provider: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ terraform init
Initializing the backend...

Initializing provider plugins...
- Checking <span style="color:#66d9ef">for</span> available provider plugins...
- Downloading plugin <span style="color:#66d9ef">for</span> provider <span style="color:#e6db74">&#34;azurerm&#34;</span> <span style="color:#f92672">(</span>hashicorp/azurerm<span style="color:#f92672">)</span> 2.15.0...
- Downloading plugin <span style="color:#66d9ef">for</span> provider <span style="color:#e6db74">&#34;google&#34;</span> <span style="color:#f92672">(</span>hashicorp/google<span style="color:#f92672">)</span> 3.26.0...
- Downloading plugin <span style="color:#66d9ef">for</span> provider <span style="color:#e6db74">&#34;template&#34;</span> <span style="color:#f92672">(</span>hashicorp/template<span style="color:#f92672">)</span> 2.1.2...
- Downloading plugin <span style="color:#66d9ef">for</span> provider <span style="color:#e6db74">&#34;aws&#34;</span> <span style="color:#f92672">(</span>hashicorp/aws<span style="color:#f92672">)</span> 2.67.0...

The following providers <span style="color:#66d9ef">do</span> not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;...&#34;</span> constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* provider.aws: version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;~&gt; 2.67&#34;</span>
* provider.azurerm: version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;~&gt; 2.15&#34;</span>
* provider.google: version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;~&gt; 3.26&#34;</span>
* provider.template: version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;~&gt; 2.1&#34;</span>

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running <span style="color:#e6db74">&#34;terraform plan&#34;</span> to see
any changes that are required <span style="color:#66d9ef">for</span> your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration <span style="color:#66d9ef">for</span> Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to <span style="color:#66d9ef">do</span> so <span style="color:#66d9ef">if</span> necessary.
</code></pre></div><h3 id="define-the-infrastructure---compute-instances">Define the infrastructure - Compute Instances</h3>
<p>You can now configure your first Google Cloud Platform Compute Instance. The below is in the file <code>gcp.tf</code> in the <code>terraform/basic-vms</code> directory: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#75715e"># GCP VM
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;google_compute_instance&#34; &#34;yetiops-gcp-prom&#34;</span> {
  name         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-gcp-prom&#34;</span>
  machine_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;f1-micro&#34;</span>

  <span style="color:#66d9ef">boot_disk</span> {
    <span style="color:#66d9ef">initialize_params</span> {
      image <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ubuntu-2004-lts&#34;</span>
    }
  }

  <span style="color:#66d9ef">network_interface</span> {
    network       <span style="color:#f92672">=</span> <span style="color:#66d9ef">google_compute_network</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">gcp</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">net</span>.<span style="color:#66d9ef">name</span>
    <span style="color:#66d9ef">access_config</span> {
    }
  }

  labels <span style="color:#f92672">=</span> {
    &#34;prometheus&#34; <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
    &#34;node_exporter&#34; <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
  }

  metadata <span style="color:#f92672">=</span> {
    enable-oslogin <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;TRUE&#34;</span>
    user-data <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">template_cloudinit_config</span>.<span style="color:#66d9ef">ubuntu</span>.<span style="color:#66d9ef">rendered</span>
  }
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e"># GCP Firewall
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;google_compute_firewall&#34; &#34;yetiops-gcp-prom-fw-icmp&#34;</span> {
  name    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-gcp-prom-fw-icmp&#34;</span>
  network <span style="color:#f92672">=</span> <span style="color:#66d9ef">google_compute_network</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">gcp</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">net</span>.<span style="color:#66d9ef">name</span>

  <span style="color:#66d9ef">allow</span> {
    protocol <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;icmp&#34;</span>
  }

}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;google_compute_firewall&#34; &#34;yetiops-gcp-prom-fw-ssh&#34;</span> {
  name    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-gcp-prom-ssh&#34;</span>
  network <span style="color:#f92672">=</span> <span style="color:#66d9ef">google_compute_network</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">gcp</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">net</span>.<span style="color:#66d9ef">name</span>
  direction <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;INGRESS&#34;</span>
  source_ranges <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;$MY_PUBLIC_IP/32&#34;</span>]

  <span style="color:#66d9ef">allow</span> {
    protocol <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
    ports <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;22&#34;</span>]
  }
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;google_compute_firewall&#34; &#34;yetiops-gcp-prom-fw-node-exporter&#34;</span> {
  name    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-gcp-prom-node-exporter&#34;</span>
  network <span style="color:#f92672">=</span> <span style="color:#66d9ef">google_compute_network</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">gcp</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">net</span>.<span style="color:#66d9ef">name</span>
  direction <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;INGRESS&#34;</span>
  source_ranges <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;$MY_PUBLIC_IP/32&#34;</span>]

  <span style="color:#66d9ef">allow</span> {
    protocol <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
    ports <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;9100&#34;</span>]
  }
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e"># GCP VPC Network
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;google_compute_network&#34; &#34;yetiops-gcp-prom-net&#34;</span> {
  name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-gcp-prom-net&#34;</span>
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e"># OS Login SSH Key
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;google_client_openid_userinfo&#34; &#34;me&#34;</span> {
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;google_os_login_ssh_public_key&#34; &#34;cache&#34;</span> {
  user <span style="color:#f92672">=</span>  <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">google_client_openid_userinfo</span>.<span style="color:#66d9ef">me</span>.<span style="color:#66d9ef">email</span>
  key <span style="color:#f92672">=</span> <span style="color:#66d9ef">file</span>(<span style="color:#e6db74">&#34;~/.ssh/id_ed25519.pub&#34;</span>)
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;google_project_iam_member&#34; &#34;role-binding&#34;</span> {
  project <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-blog&#34;</span>
  role    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;roles/compute.osAdminLogin&#34;</span>
  member  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;user:yetiops.gcp@gmail.com&#34;</span>
}
</code></pre></div><p>To summarize what Terraform is going to do: -</p>
<ul>
<li>Create an Ubuntu 20.04 Compute Instance of size <code>f1-micro</code> (1 vCPU, 0.6G of memory)
<ul>
<li>We enable <a href="https://cloud.google.com/compute/docs/oslogin">OS Login</a> which ties your Linux login to your Google Account directly</li>
<li>We supply the same <code>user-data</code> as we do for Azure and AWS</li>
</ul>
</li>
<li>Create a firewall to allow all ICMP, as well as SSH and the Node Exporter port (TCP 9100) through from my public IP</li>
<li>Create a network for the instance and firewall to run inside of</li>
<li>Use our SSH key for OS Login</li>
</ul>
<p>One small difference to note is that Google use the term <code>labels</code> rather then <code>tags</code> (like in AWS and Azure).</p>
<p>An interesting point to note is that in AWS and Azure, the AWS VPC and the Azure networks are regional. Communication between regions requires routers, VPNs, or in the case of AWS, <a href="https://docs.aws.amazon.com/vpc/latest/peering/what-is-vpc-peering.html">VPC Peering</a>. In the Google Cloud Platform, the VPC is multi-region by default: -</p>
<p><img src="/img/prometheus/gcp-networks.png" alt="Google Cloud Platform VPC"></p>
<h3 id="build-the-infrastructure---compute-instances">Build the infrastructure - Compute Instances</h3>
<p>We can now apply our configuration, and see if it builds a Compute Instance: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ terraform apply
data.template_file.ubuntu: Refreshing state...
data.template_cloudinit_config.ubuntu: Refreshing state...
data.google_client_openid_userinfo.me: Refreshing state...
data.aws_vpc.default: Refreshing state...
data.aws_ami.ubuntu: Refreshing state...
aws_key_pair.yetiops-aws-prom: Refreshing state... <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>yetiops-aws-prom<span style="color:#f92672">]</span>
aws_security_group.yetiops-aws-prom: Refreshing state... <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>sg-0843cc8542221927d<span style="color:#f92672">]</span>
aws_security_group_rule.ingress_node_exporter_in: Refreshing state... <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>sgrule-1236498832<span style="color:#f92672">]</span>
aws_security_group_rule.egress_allow_all: Refreshing state... <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>sgrule-1341973878<span style="color:#f92672">]</span>
aws_security_group_rule.ingress_ssh_in: Refreshing state... <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>sgrule-977439385<span style="color:#f92672">]</span>
aws_instance.yetiops-aws-prom: Refreshing state... <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>i-0f409fa1d4b5dd373<span style="color:#f92672">]</span>
azurerm_resource_group.yetiops-azure-prom: Refreshing state... <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>/subscriptions/$SUBSCRIPTION_ID/resourceGroups/yetiops-azure-prom<span style="color:#f92672">]</span>
azurerm_virtual_network.yetiops-azure-prom: Refreshing state... <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>/subscriptions/$SUBSCRIPTION_ID/resourceGroups/yetiops-azure-prom/providers/Microsoft.Network/virtualNetworks/yetiops-azure-prom<span style="color:#f92672">]</span>
azurerm_public_ip.yetiops-azure-prom-pip: Refreshing state... <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>/subscriptions/$SUBSCRIPTION_ID/resourceGroups/yetiops-azure-prom/providers/Microsoft.Network/publicIPAddresses/yetiops-azure-prom-pip<span style="color:#f92672">]</span>
azurerm_subnet.yetiops-azure-prom: Refreshing state... <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>/subscriptions/$SUBSCRIPTION_ID/resourceGroups/yetiops-azure-prom/providers/Microsoft.Network/virtualNetworks/yetiops-azure-prom/subnets/yetiops-azure-prom-main<span style="color:#f92672">]</span>
azurerm_network_interface.yetiops-azure-prom: Refreshing state... <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>/subscriptions/$SUBSCRIPTION_ID/resourceGroups/yetiops-azure-prom/providers/Microsoft.Network/networkInterfaces/yetiops-azure-prom-nic<span style="color:#f92672">]</span>
azurerm_linux_virtual_machine.yetiops-azure-prom: Refreshing state... <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>/subscriptions/$SUBSCRIPTION_ID/resourceGroups/yetiops-azure-prom/providers/Microsoft.Compute/virtualMachines/yetiops-azure-prom-machine<span style="color:#f92672">]</span>
azurerm_network_security_group.yetiops-azure-prom: Refreshing state... <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>/subscriptions/$SUBSCRIPTION_ID/resourceGroups/yetiops-azure-prom/providers/Microsoft.Network/networkSecurityGroups/yetiops-azure-prom<span style="color:#f92672">]</span>

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create
  ~ update in-place

Terraform will perform the following actions:

  <span style="color:#75715e"># google_compute_firewall.yetiops-gcp-prom-fw-icmp will be created</span>
  + resource <span style="color:#e6db74">&#34;google_compute_firewall&#34;</span> <span style="color:#e6db74">&#34;yetiops-gcp-prom-fw-icmp&#34;</span> <span style="color:#f92672">{</span>
      + creation_timestamp <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + destination_ranges <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + direction          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id                 <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + name               <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-gcp-prom-fw-icmp&#34;</span>
      + network            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-gcp-prom-net&#34;</span>
      + priority           <span style="color:#f92672">=</span> <span style="color:#ae81ff">1000</span>
      + project            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + self_link          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + source_ranges      <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>

      + allow <span style="color:#f92672">{</span>
          + ports    <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
          + protocol <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;icmp&#34;</span>
        <span style="color:#f92672">}</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># google_compute_firewall.yetiops-gcp-prom-fw-node-exporter will be created</span>
  + resource <span style="color:#e6db74">&#34;google_compute_firewall&#34;</span> <span style="color:#e6db74">&#34;yetiops-gcp-prom-fw-node-exporter&#34;</span> <span style="color:#f92672">{</span>
      + creation_timestamp <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + destination_ranges <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + direction          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;INGRESS&#34;</span>
      + id                 <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + name               <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-gcp-prom-node-exporter&#34;</span>
      + network            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-gcp-prom-net&#34;</span>
      + priority           <span style="color:#f92672">=</span> <span style="color:#ae81ff">1000</span>
      + project            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + self_link          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + source_ranges      <span style="color:#f92672">=</span> <span style="color:#f92672">[</span>
          + <span style="color:#e6db74">&#34;</span>$MY_PUBLIC_IP<span style="color:#e6db74">/32&#34;</span>,
        <span style="color:#f92672">]</span>

      + allow <span style="color:#f92672">{</span>
          + ports    <span style="color:#f92672">=</span> <span style="color:#f92672">[</span>
              + <span style="color:#e6db74">&#34;9100&#34;</span>,
            <span style="color:#f92672">]</span>
          + protocol <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
        <span style="color:#f92672">}</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># google_compute_firewall.yetiops-gcp-prom-fw-ssh will be created</span>
  + resource <span style="color:#e6db74">&#34;google_compute_firewall&#34;</span> <span style="color:#e6db74">&#34;yetiops-gcp-prom-fw-ssh&#34;</span> <span style="color:#f92672">{</span>
      + creation_timestamp <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + destination_ranges <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + direction          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;INGRESS&#34;</span>
      + id                 <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + name               <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-gcp-prom-ssh&#34;</span>
      + network            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-gcp-prom-net&#34;</span>
      + priority           <span style="color:#f92672">=</span> <span style="color:#ae81ff">1000</span>
      + project            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + self_link          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + source_ranges      <span style="color:#f92672">=</span> <span style="color:#f92672">[</span>
          + <span style="color:#e6db74">&#34;</span>$MY_PUBLIC_IP<span style="color:#e6db74">/32&#34;</span>,
        <span style="color:#f92672">]</span>

      + allow <span style="color:#f92672">{</span>
          + ports    <span style="color:#f92672">=</span> <span style="color:#f92672">[</span>
              + <span style="color:#e6db74">&#34;22&#34;</span>,
            <span style="color:#f92672">]</span>
          + protocol <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
        <span style="color:#f92672">}</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># google_compute_instance.yetiops-gcp-prom will be created</span>
  + resource <span style="color:#e6db74">&#34;google_compute_instance&#34;</span> <span style="color:#e6db74">&#34;yetiops-gcp-prom&#34;</span> <span style="color:#f92672">{</span>
      + can_ip_forward       <span style="color:#f92672">=</span> false
      + cpu_platform         <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + current_status       <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + deletion_protection  <span style="color:#f92672">=</span> false
      + guest_accelerator    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id                   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + instance_id          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + label_fingerprint    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + labels               <span style="color:#f92672">=</span> <span style="color:#f92672">{</span>
          + <span style="color:#e6db74">&#34;node_exporter&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
          + <span style="color:#e6db74">&#34;prometheus&#34;</span>    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
        <span style="color:#f92672">}</span>
      + machine_type         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;f1-micro&#34;</span>
      + metadata             <span style="color:#f92672">=</span> <span style="color:#f92672">{</span>
          + <span style="color:#e6db74">&#34;enable-oslogin&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;TRUE&#34;</span>
          + <span style="color:#e6db74">&#34;user-data&#34;</span>      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Content-Type: multipart/mixed; boundary=\&#34;MIMEBOUNDARY\&#34;\nMIME-Version: 1.0\r\n\r\n--MIMEBOUNDARY\r\nContent-Disposition: attachment; filename=\&#34;init.cfg\&#34;\r\nContent-Transfer-Encoding: 7bit\r\nContent-Type: text/cloud-config\r\nMime-Version: 1.0\r\n\r\n#cloud-config\npackages:\n - prometheus-node-exporter\n\r\n--MIMEBOUNDARY--\r\n&#34;</span>
        <span style="color:#f92672">}</span>
      + metadata_fingerprint <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + min_cpu_platform     <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + name                 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-gcp-prom&#34;</span>
      + project              <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + self_link            <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + tags_fingerprint     <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + zone                 <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>

      + boot_disk <span style="color:#f92672">{</span>
          + auto_delete                <span style="color:#f92672">=</span> true
          + device_name                <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + disk_encryption_key_sha256 <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + kms_key_self_link          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + mode                       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;READ_WRITE&#34;</span>
          + source                     <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>

          + initialize_params <span style="color:#f92672">{</span>
              + image  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ubuntu-2004-lts&#34;</span>
              + labels <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
              + size   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
              + type   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
            <span style="color:#f92672">}</span>
        <span style="color:#f92672">}</span>

      + network_interface <span style="color:#f92672">{</span>
          + name               <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + network            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-gcp-prom-net&#34;</span>
          + network_ip         <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + subnetwork         <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + subnetwork_project <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>

          + access_config <span style="color:#f92672">{</span>
              + nat_ip       <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
              + network_tier <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
            <span style="color:#f92672">}</span>
        <span style="color:#f92672">}</span>

      + scheduling <span style="color:#f92672">{</span>
          + automatic_restart   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + on_host_maintenance <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
          + preemptible         <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>

          + node_affinities <span style="color:#f92672">{</span>
              + key      <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
              + operator <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
              + values   <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
            <span style="color:#f92672">}</span>
        <span style="color:#f92672">}</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># google_compute_network.yetiops-gcp-prom-net will be created</span>
  + resource <span style="color:#e6db74">&#34;google_compute_network&#34;</span> <span style="color:#e6db74">&#34;yetiops-gcp-prom-net&#34;</span> <span style="color:#f92672">{</span>
      + auto_create_subnetworks         <span style="color:#f92672">=</span> true
      + delete_default_routes_on_create <span style="color:#f92672">=</span> false
      + gateway_ipv4                    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id                              <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + ipv4_range                      <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + name                            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-gcp-prom-net&#34;</span>
      + project                         <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + routing_mode                    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + self_link                       <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># google_os_login_ssh_public_key.cache will be created</span>
  + resource <span style="color:#e6db74">&#34;google_os_login_ssh_public_key&#34;</span> <span style="color:#e6db74">&#34;cache&#34;</span> <span style="color:#f92672">{</span>
      + fingerprint <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id          <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + key         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;</span>$MY_SSH_KEY<span style="color:#e6db74">&#34;</span>
      + user        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;</span>$GCP_SERVICE_ACCOUNT<span style="color:#e6db74">&#34;</span>
    <span style="color:#f92672">}</span>

  <span style="color:#75715e"># google_project_iam_member.role-binding will be created</span>
  + resource <span style="color:#e6db74">&#34;google_project_iam_member&#34;</span> <span style="color:#e6db74">&#34;role-binding&#34;</span> <span style="color:#f92672">{</span>
      + etag    <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + id      <span style="color:#f92672">=</span> <span style="color:#f92672">(</span>known after apply<span style="color:#f92672">)</span>
      + member  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;user:yetiops.gcp@gmail.com&#34;</span>
      + project <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-blog&#34;</span>
      + role    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;roles/compute.osAdminLogin&#34;</span>
    <span style="color:#f92672">}</span>

Plan: <span style="color:#ae81ff">7</span> to add, <span style="color:#ae81ff">0</span> to change, <span style="color:#ae81ff">0</span> to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only <span style="color:#e6db74">&#39;yes&#39;</span> will be accepted to approve.

  Enter a value: yes

google_os_login_ssh_public_key.cache: Creating...
google_project_iam_member.role-binding: Creating...
google_compute_network.yetiops-gcp-prom-net: Creating...
google_os_login_ssh_public_key.cache: Creation complete after 1s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>users/$GCP_SERVICE_ACCOUNT/sshPublicKeys/$PUBLIC_KEY_ID<span style="color:#f92672">]</span>
google_compute_network.yetiops-gcp-prom-net: Still creating... <span style="color:#f92672">[</span>10s elapsed<span style="color:#f92672">]</span>
google_project_iam_member.role-binding: Still creating... <span style="color:#f92672">[</span>10s elapsed<span style="color:#f92672">]</span>
google_project_iam_member.role-binding: Creation complete after 10s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>yetiops-blog/roles/compute.osAdminLogin/user:yetiops.gcp@gmail.com<span style="color:#f92672">]</span>
google_compute_network.yetiops-gcp-prom-net: Still creating... <span style="color:#f92672">[</span>20s elapsed<span style="color:#f92672">]</span>
google_compute_network.yetiops-gcp-prom-net: Still creating... <span style="color:#f92672">[</span>30s elapsed<span style="color:#f92672">]</span>
google_compute_network.yetiops-gcp-prom-net: Still creating... <span style="color:#f92672">[</span>40s elapsed<span style="color:#f92672">]</span>
google_compute_network.yetiops-gcp-prom-net: Creation complete after 43s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>projects/yetiops-blog/global/networks/yetiops-gcp-prom-net<span style="color:#f92672">]</span>
google_compute_firewall.yetiops-gcp-prom-fw-ssh: Creating...
google_compute_firewall.yetiops-gcp-prom-fw-icmp: Creating...
google_compute_firewall.yetiops-gcp-prom-fw-node-exporter: Creating...
google_compute_instance.yetiops-gcp-prom: Creating...
google_compute_firewall.yetiops-gcp-prom-fw-icmp: Still creating... <span style="color:#f92672">[</span>10s elapsed<span style="color:#f92672">]</span>
google_compute_firewall.yetiops-gcp-prom-fw-ssh: Still creating... <span style="color:#f92672">[</span>10s elapsed<span style="color:#f92672">]</span>
google_compute_firewall.yetiops-gcp-prom-fw-node-exporter: Still creating... <span style="color:#f92672">[</span>10s elapsed<span style="color:#f92672">]</span>
google_compute_instance.yetiops-gcp-prom: Still creating... <span style="color:#f92672">[</span>10s elapsed<span style="color:#f92672">]</span>
google_compute_firewall.yetiops-gcp-prom-fw-node-exporter: Creation complete after 12s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>projects/yetiops-blog/global/firewalls/yetiops-gcp-prom-node-exporter<span style="color:#f92672">]</span>
google_compute_firewall.yetiops-gcp-prom-fw-ssh: Creation complete after 12s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>projects/yetiops-blog/global/firewalls/yetiops-gcp-prom-ssh<span style="color:#f92672">]</span>
google_compute_firewall.yetiops-gcp-prom-fw-icmp: Creation complete after 13s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>projects/yetiops-blog/global/firewalls/yetiops-gcp-prom-fw-icmp<span style="color:#f92672">]</span>
google_compute_instance.yetiops-gcp-prom: Creation complete after 15s <span style="color:#f92672">[</span>id<span style="color:#f92672">=</span>projects/yetiops-blog/zones/europe-west3-c/instances/yetiops-gcp-prom<span style="color:#f92672">]</span>

Apply complete! Resources: <span style="color:#ae81ff">7</span> added, <span style="color:#ae81ff">0</span> changed, <span style="color:#ae81ff">0</span> destroyed.
</code></pre></div><p>We can double check that Terraform is managing these resources now with <code>terraform state list</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ terraform state list
data.aws_ami.ubuntu
data.aws_vpc.default
data.google_client_openid_userinfo.me
data.template_cloudinit_config.ubuntu
data.template_file.ubuntu
aws_instance.yetiops-aws-prom
aws_key_pair.yetiops-aws-prom
aws_security_group.yetiops-aws-prom
aws_security_group_rule.egress_allow_all
aws_security_group_rule.ingress_node_exporter_in
aws_security_group_rule.ingress_ssh_in
azurerm_linux_virtual_machine.yetiops-azure-prom
azurerm_network_interface.yetiops-azure-prom
azurerm_network_security_group.yetiops-azure-prom
azurerm_public_ip.yetiops-azure-prom-pip
azurerm_resource_group.yetiops-azure-prom
azurerm_subnet.yetiops-azure-prom
azurerm_virtual_network.yetiops-azure-prom
google_compute_firewall.yetiops-gcp-prom-fw-icmp
google_compute_firewall.yetiops-gcp-prom-fw-node-exporter
google_compute_firewall.yetiops-gcp-prom-fw-ssh
google_compute_instance.yetiops-gcp-prom
google_compute_network.yetiops-gcp-prom-net
google_os_login_ssh_public_key.cache
google_project_iam_member.role-binding
</code></pre></div><p>We can check to see if the instance and other resources are in the Google Cloud Console: -</p>
<p><img src="/img/prometheus/gcp-instance.png" alt="GCP Instance"></p>
<p>Now we should be able to use the <code>gcloud</code> CLI to login to the machine: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ gcloud compute ssh yetiops-gcp-prom
Welcome to Ubuntu 20.04 LTS <span style="color:#f92672">(</span>GNU/Linux 5.4.0-1015-gcp x86_64<span style="color:#f92672">)</span>

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Mon Jun <span style="color:#ae81ff">22</span> 11:53:06 UTC <span style="color:#ae81ff">2020</span>

  System load:  0.02              Processes:             <span style="color:#ae81ff">105</span>
  Usage of /:   15.5% of 9.52GB   Users logged in:       <span style="color:#ae81ff">0</span>
  Memory usage: 42%               IPv4 address <span style="color:#66d9ef">for</span> ens4: 10.156.0.2
  Swap usage:   0%

<span style="color:#ae81ff">29</span> updates can be installed immediately.
<span style="color:#ae81ff">11</span> of these updates are security updates.
To see these additional updates run: apt list --upgradable



The programs included with the Ubuntu system are free software;
the exact distribution terms <span style="color:#66d9ef">for</span> each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.


The programs included with the Ubuntu system are free software;
the exact distribution terms <span style="color:#66d9ef">for</span> each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

Creating directory <span style="color:#e6db74">&#39;/home/yetiops_gcp_gmail_com&#39;</span>.
yetiops_gcp_gmail_com@yetiops-gcp-prom:~$

yetiops_gcp_gmail_com@yetiops-gcp-prom:~$ ps aux | grep -i node
prometh+    <span style="color:#ae81ff">2497</span>  0.0  1.7 <span style="color:#ae81ff">336924</span> <span style="color:#ae81ff">10516</span> ?        Ssl  11:48   0:00 /usr/bin/prometheus-node-exporter
yetiops+    <span style="color:#ae81ff">3751</span>  0.0  0.1   <span style="color:#ae81ff">8156</span>   <span style="color:#ae81ff">672</span> pts/0    S+   11:53   0:00 grep --color<span style="color:#f92672">=</span>auto -i node
</code></pre></div><h3 id="configure-terraform---prometheus-user-2">Configure Terraform - Prometheus User</h3>
<p>Next, we need to create a user that Prometheus will use to query the Google Cloud API. To do this, go into the <code>terraform/prometheus-access</code> directory, and update the <code>providers.tf</code> file as we did before: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#75715e"># AWS Provider
</span><span style="color:#75715e"></span><span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;aws&#34;</span> {
  region  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;eu-west-2&#34;</span>
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e"># Azure Provider
</span><span style="color:#75715e"></span><span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;azurerm&#34;</span> {
  subscription_id <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;$SUBSCRIPTION_ID&#34;</span>
  <span style="color:#66d9ef">features</span> {}
}

<span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;azuread&#34;</span> {
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e"># GCP Provider
</span><span style="color:#75715e"></span><span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;google&#34;</span> {
  credentials <span style="color:#f92672">=</span> <span style="color:#66d9ef">file</span>(<span style="color:#e6db74">&#34;~/.gcloud/account.json&#34;</span>)
  project <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-blog&#34;</span>
  region  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;europe-west3&#34;</span>
  zone    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;europe-west3-c&#34;</span>
}
</code></pre></div><p>The below file configures the user for Prometheus access: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;google_service_account&#34; &#34;prometheus-sd-user&#34;</span> {
  account_id   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;prometheus-sd-user&#34;</span>
  display_name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Prometheus Service Discovery&#34;</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;google_project_iam_member&#34; &#34;prometheus-sd-user-compute-viewer&#34;</span> {
  role    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;roles/compute.viewer&#34;</span>
  member  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;serviceAccount:${google_service_account.prometheus-sd-user.email}&#34;</span>
}
</code></pre></div><p>We create the service account, and we also assign the role of <code>compute.viewer</code>. This restricts it to viewing only Compute resources. You do not need to generate a Client ID and Secret, as we will use the Google Cloud SDK when configuring the Prometheus instance.</p>
<h2 id="prometheus">Prometheus</h2>
<p>Now that we have our infrastructure in AWS, Azure and Google Compute Platform, we can configure our Prometheus instance. I am using an Ubuntu 20.04 virtual machine in my lab for this.</p>
<h3 id="aws-service-discovery">AWS Service Discovery</h3>
<p>To enable Prometheus to discover services on AWS, you need to <a href="/posts/prometheus-service-discovery-aws-gcp-azure/#install-the-aws-cli">install the AWS CLI</a> on the virtual machine. This time though, rather than using the credentials from your user or root account, use the credentials generated for the <code>prometheus-sd-user</code> <a href="/posts/prometheus-service-discovery-aws-gcp-azure/#configure-terraform---prometheus-user">earlier</a>.</p>
<p>Also, you need to configure the AWS CLI with the user that Prometheus executes as on your instance. In my case, this is the <code>prometheus</code> user: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># Change to the prometheus user</span>
$ sudo su - prometheus

<span style="color:#75715e"># Configure the AWS CLI</span>
$ aws configure
AWS Access Key ID <span style="color:#f92672">[</span>None<span style="color:#f92672">]</span>: <span style="color:#75715e">###PROMETHEUS-SD-USER-ACCESS_KEY###</span> 
AWS Secret Access Key <span style="color:#f92672">[</span>None<span style="color:#f92672">]</span>: <span style="color:#75715e">###PROMETHEUS-SD-USER-SECRET_KEY###</span>
Default region name <span style="color:#f92672">[</span>None<span style="color:#f92672">]</span>: eu-west-2 <span style="color:#75715e">### Replace this with whatever region you prefer (eg us-east-1, ap-southeast-1)</span>
Default output format <span style="color:#f92672">[</span>None<span style="color:#f92672">]</span>:

<span style="color:#75715e"># Check that you can list EC2 instances</span>
$  aws ec2 describe-instances | jq .Reservations<span style="color:#f92672">[]</span>.Instances<span style="color:#f92672">[]</span>.Tags
<span style="color:#f92672">[</span>
  <span style="color:#f92672">{</span>
    <span style="color:#e6db74">&#34;Key&#34;</span>: <span style="color:#e6db74">&#34;node_exporter&#34;</span>,
    <span style="color:#e6db74">&#34;Value&#34;</span>: <span style="color:#e6db74">&#34;true&#34;</span>
  <span style="color:#f92672">}</span>,
  <span style="color:#f92672">{</span>
    <span style="color:#e6db74">&#34;Key&#34;</span>: <span style="color:#e6db74">&#34;prometheus&#34;</span>,
    <span style="color:#e6db74">&#34;Value&#34;</span>: <span style="color:#e6db74">&#34;true&#34;</span>
  <span style="color:#f92672">}</span>,
  <span style="color:#f92672">{</span>
    <span style="color:#e6db74">&#34;Key&#34;</span>: <span style="color:#e6db74">&#34;Name&#34;</span>,
    <span style="color:#e6db74">&#34;Value&#34;</span>: <span style="color:#e6db74">&#34;yetiops-aws-prom&#34;</span>
  <span style="color:#f92672">}</span>
<span style="color:#f92672">]</span>
</code></pre></div><p>If you run your Prometheus instance within AWS, you could use <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2_instance-profiles.html">AWS Instance Profiles</a> and IAM Roles rather than using Client Keys and Secret Keys. When Prometheus runs within AWS, it can assume the role of the instance it is running on. This role can then be assign the appropriate  permissions. This is a cleaner than using keys and secrets. To make use of this, you would use the following in your Terraform: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_iam_role&#34; &#34;prometheus-ec2-role&#34;</span> {
  name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;prometheus&#34;</span>
  path <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/&#34;</span>

  assume_role_policy <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">&lt;&lt;</span><span style="color:#66d9ef">EOF</span>
{
    <span style="color:#e6db74">&#34;Version&#34;: &#34;2012-10-17&#34;</span>,
    <span style="color:#e6db74">&#34;Statement&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> [
        {
            <span style="color:#e6db74">&#34;Action&#34;: &#34;sts:AssumeRole&#34;</span>,
            <span style="color:#e6db74">&#34;Principal&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> {
               <span style="color:#e6db74">&#34;Service&#34;: &#34;ec2.amazonaws.com&#34;</span>
            },
            <span style="color:#e6db74">&#34;Effect&#34;: &#34;Allow&#34;</span>,
            <span style="color:#e6db74">&#34;Sid&#34;: &#34;&#34;</span>
        }
    ]
}
<span style="color:#66d9ef">EOF</span>

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_iam_role_policy_attachment&#34; &#34;prometheus-sd-policy-attach&#34;</span> {
  role       <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_iam_role</span>.<span style="color:#66d9ef">prometheus</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">ec2</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">role</span>.<span style="color:#66d9ef">name</span>
  policy_arn <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess&#34;</span> 
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_iam_instance_profile&#34; &#34;prometheus-instance-profile&#34;</span> {
  name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;promtheus&#34;</span>
  role <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_iam_role</span>.<span style="color:#66d9ef">prometheus</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">ec2</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">role</span>.<span style="color:#66d9ef">name</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_instance&#34; &#34;prometheus&#34;</span> {
  [...]
  iam_instance_profile <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_iam_instance_profile</span>.<span style="color:#66d9ef">prometheus</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">instance</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">profile</span>.<span style="color:#66d9ef">name</span>
}
</code></pre></div><p>Once the above is configured, you can now update your Prometheus configuration (<code>prometheus.yaml</code>) for EC2 service discovery: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">global</span>:
  <span style="color:#66d9ef">scrape_interval</span>:     15s
  <span style="color:#66d9ef">evaluation_interval</span>: 15s

<span style="color:#66d9ef">scrape_configs</span>:
  - <span style="color:#66d9ef">job_name</span>: <span style="color:#e6db74">&#39;ec2-nodes&#39;</span>
    <span style="color:#66d9ef">ec2_sd_configs</span>:
      - <span style="color:#66d9ef">region</span>: eu-west<span style="color:#ae81ff">-2</span>
        <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">9100</span>
    <span style="color:#66d9ef">relabel_configs</span>:
      - <span style="color:#66d9ef">source_labels</span>: [__meta_ec2_tag_prometheus]
        <span style="color:#66d9ef">regex</span>: <span style="color:#66d9ef">true</span>.*
        <span style="color:#66d9ef">action</span>: keep
      - <span style="color:#66d9ef">source_labels</span>: [__meta_ec2_tag_node_exporter]
        <span style="color:#66d9ef">regex</span>: <span style="color:#66d9ef">true</span>.*
        <span style="color:#66d9ef">action</span>: keep
      - <span style="color:#66d9ef">source_labels</span>: [__meta_ec2_instance_id]
        <span style="color:#66d9ef">target_label</span>: instance
      - <span style="color:#66d9ef">source_labels</span>: [__meta_ec2_public_ip]
        <span style="color:#66d9ef">regex</span>: (.+)
        <span style="color:#66d9ef">replacement</span>: ${<span style="color:#ae81ff">1</span>}:<span style="color:#ae81ff">9100</span>
        <span style="color:#66d9ef">target_label</span>: __address__
</code></pre></div><p>Prometheus EC2 discovery inherits the configuration from the AWS <code>credentials</code> file generated by the AWS CLI.</p>
<p>First, Prometheus looks through all of our EC2 instances. If any have the tag <code>prometheus</code>, and the value matches the regex <code>true.*</code> (e.g. the word <code>true</code>), then we <strong>keep</strong> the service. In the context of our Prometheus configuration, <strong>keep</strong> means that any instance discovered in AWS, in our account and in the <code>eu-west-2</code> region is only registered as a target if it has a tag of <code>prometheus: true</code>. If it is any other value, or lacks the <code>prometheus</code> tag, then we ignore the instance. This is useful for hosts without any exporters.</p>
<p>Next, we do the same for EC2 instances with the tag named <code>node_exporter</code>. Again, if the value is <code>true</code>, we register the target. This is useful where you are running mixed workloads (e.g. Linux and Windows), as this job only works for the Node Exporter (which does not run on Windows).</p>
<p>Next, we take the <code>instance_id</code> and put it into the <code>instance</code> label.</p>
<p>Finally, we take the <code>public_ip</code>, append <code>:9100</code> to it, and replace the contents of the <code>__address__</code> label (i.e. the address that Prometheus will scrape the metrics from). By default, the <code>__address__</code> label uses the private IP of the instance. This is fine if you have VPNs between your network and the AWS environment, have a Direct Connect (i.e. a physical link(s) into Amazon&rsquo;s network) or the Prometheus instance runs inside AWS. If the Prometheus instance cannot route to the private IP, then this relabelling action is required.</p>
<p>The above labels are sourced from the Prometheus EC2 Service Discovery metadata. You can see what metadata is discovered from an instance by going to the <strong>Service Discovery</strong> page in Prometheus&rsquo;s <strong>Status</strong> Menu: -</p>
<p><img src="/img/prometheus/aws-ec2-sd.png" alt="AWS EC2 Service Discovery"></p>
<p>As you can see, we have our tags, the public IP, private IP, the <code>__address__</code> label (which we replace) and more. You could use all of these labels, or only the relevant labels.</p>
<p>Lets see if the instance is up, and if we can reach the Node Exporter: -</p>
<p><img src="/img/prometheus/aws-sd-target.png" alt="AWS EC2 Target"></p>
<p>There we go!</p>
<h3 id="google-cloud-platform-service-discovery">Google Cloud Platform Service Discovery</h3>
<p>To enable Prometheus to discover services on Google Cloud Platform, <a href="/posts/prometheus-service-discovery-aws-gcp-azure/#install-the-sdk">install the Google Cloud SDK</a> on the Prometheus instance.</p>
<p>Once install, we retrieve a JSON file containing the credentials for your <code>prometheus-sd-user</code> service account using the <code>gcloud</code> CLI: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># Login to the Google Cloud Platform using your Google Account credentials</span>
$ gcloud auth login

<span style="color:#75715e"># Retrieve the JSON file</span>
$ gcloud iam service-accounts keys create ~/key.json   --iam-account prometheus-sd-user@yetiops-blog.iam.gserviceaccount.com
</code></pre></div><p>The IAM Account string is the Prometheus service account user i.e. <code>prometheus-sd-user</code>), followed by <code>@$YOUR_GCP_PROJECT.iam.gserviceaccount.com</code>. Once you have retrieved the file, it needs to be moved to the following location: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ mv ~/key.json $PROMETHEUS_USER_HOME_DIRECTORY/.config/gcloud/application_default_credentials.json
</code></pre></div><p>On my Prometheus instance, the home directory for the Prometheus user is <code>/home/prometheus</code>.</p>
<p>As with AWS, if you run Prometheus on a Google Compute Instance, you can make use of <a href="https://cloud.google.com/iam/docs/service-accounts#what_are_service_accounts">Cloud IAM Service Accounts</a>. These would be attached directly to the Compute Instance that runs Prometheus. This negates the need to retrieve the JSON file with the service account credentials. You can apply this with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;google_compute_instance&#34; &#34;prometheus&#34;</span> {

  <span style="color:#66d9ef">service_account</span> {
    scopes <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;compute-ro&#34;</span>]
  }
}
</code></pre></div><p>Once this is done, you should now be able to discover your Compute Instances with Prometheus. The Prometheus configuration required for Google Cloud Platform is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  - <span style="color:#66d9ef">job_name</span>: <span style="color:#e6db74">&#39;gce-nodes&#39;</span>
    <span style="color:#66d9ef">gce_sd_configs</span>:
      - <span style="color:#66d9ef">project</span>: yetiops-blog
        <span style="color:#66d9ef">zone</span>: europe-west3-c
        <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">9100</span>
    <span style="color:#66d9ef">relabel_configs</span>:
      - <span style="color:#66d9ef">source_labels</span>: [__meta_gce_label_prometheus]
        <span style="color:#66d9ef">regex</span>: <span style="color:#66d9ef">true</span>.*
        <span style="color:#66d9ef">action</span>: keep
      - <span style="color:#66d9ef">source_labels</span>: [__meta_gce_label_node_exporter]
        <span style="color:#66d9ef">regex</span>: <span style="color:#66d9ef">true</span>.*
        <span style="color:#66d9ef">action</span>: keep
      - <span style="color:#66d9ef">source_labels</span>: [__meta_gce_public_ip]
        <span style="color:#66d9ef">regex</span>: (.+)
        <span style="color:#66d9ef">replacement</span>: ${<span style="color:#ae81ff">1</span>}:<span style="color:#ae81ff">9100</span>
        <span style="color:#66d9ef">target_label</span>: __address__
      - <span style="color:#66d9ef">source_labels</span>: [__meta_gce_instance_id]
        <span style="color:#66d9ef">target_label</span>: instance
</code></pre></div><p>This is very similar to the AWS EC2 configuration, with a couple of exceptions: -</p>
<ul>
<li>We supply the Project name</li>
<li>We use labels rather than tags</li>
</ul>
<p>Otherwise, we perform the same operations (keeping services/instances that have the <code>prometheus: true</code> and <code>node_exporter: true</code> labels, and rewriting the <code>__address__</code> to use the public IP).</p>
<p>Again, all of the above is obtained from the Prometheus Google Cloud Platform Service Discovery metadata.</p>
<p><img src="/img/prometheus/gcp-gce-metadata.png" alt="GCP GCE Metadata"></p>
<p>A point to note is that Google Cloud Platform uses the term <strong>metadata</strong> to include things like the <code>user-data</code>, enabling OS Login, and other custom fields you may add to the instance. This is not the same as the Prometheus metadata that is generated during Service Discovery.</p>
<p>Can we reach the instance and the Node Exporter?</p>
<p><img src="/img/prometheus/gcp-gce-target.png" alt="GCP GCE SD"></p>
<p>We can!</p>
<h3 id="azure-service-discovery">Azure Service Discovery</h3>
<p>To enable Prometheus to discover services in Azure, you need to use the following details: -</p>
<ul>
<li><strong>Subscription ID</strong> - You can check this with <code>az account list</code> (in the field <code>id</code>)</li>
<li><strong>Tenant ID</strong> - You can check this with <code>az account list</code> (in the field <code>tenantId</code>)</li>
<li><strong>Client ID</strong> - This is the ID from the steps <a href="/posts/prometheus-service-discovery-aws-gcp-azure/#configure-terraform---prometheus-user-1">here</a></li>
<li><strong>Client Secret</strong> - This is the secret generated in the steps <a href="/posts/prometheus-service-discovery-aws-gcp-azure/#configure-terraform---prometheus-user-1">here</a></li>
</ul>
<p>These details will be placed directly into the Prometheus configuration. Unlike AWS or Google Cloud Platform, the Azure service discovery provider does not use any locally saved credentials from the <code>az</code> CLI.</p>
<p>If you run the Prometheus instance inside Azure, you can use <a href="https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview">Managed Identities</a>. These are similar to AWS Instance Profiles/IAM Roles and Google Cloud Platform&rsquo;s Cloud IAM Service Accounts attached to Cloud Instances. This identity is attached to the Azure Virtual Machine. You can then assign the appropriate permissions, negating the need to use IDs and secrets. To use a Managed Identity instead, you can use: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;azurerm_virtual_machine&#34; &#34;prometheus&#34;</span> {<span style="color:#75715e">
</span><span style="color:#75715e">  # ...
</span><span style="color:#75715e"></span>
  <span style="color:#66d9ef">identity</span> {
    type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;SystemAssigned&#34;</span>
  }
}

<span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;azurerm_role_definition&#34; &#34;reader&#34;</span> {
  name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Reader&#34;</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;azurerm_role_assignment&#34; &#34;prometheus&#34;</span> {
  name               <span style="color:#f92672">=</span> <span style="color:#66d9ef">azurerm_virtual_machine</span>.<span style="color:#66d9ef">prometheus</span>.<span style="color:#66d9ef">name</span>
  scope              <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">azurerm_subscription</span>.<span style="color:#66d9ef">primary</span>.<span style="color:#66d9ef">id</span>
  role_definition_id <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${data.azurerm_subscription.subscription.id}${data.azurerm_role_definition.reader.id}&#34;</span>
  principal_id       <span style="color:#f92672">=</span> <span style="color:#66d9ef">azurerm_virtual_machine</span>.<span style="color:#66d9ef">prometheus</span>.<span style="color:#66d9ef">identity</span>[<span style="color:#ae81ff">0</span>][<span style="color:#e6db74">&#34;principal_id&#34;</span>]
}
</code></pre></div><p>Now you can configure the Azure service discovery in Prometheus. The required configuration is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  - <span style="color:#66d9ef">job_name</span>: <span style="color:#e6db74">&#39;azure-nodes&#39;</span>
    <span style="color:#66d9ef">azure_sd_configs</span>:
      - <span style="color:#66d9ef">subscription_id</span>: <span style="color:#e6db74">&#39;$SUBSCRIPTION_ID&#39;</span>
        <span style="color:#66d9ef">tenant_id</span>: <span style="color:#e6db74">&#39;$TENANT_ID&#39;</span>
        <span style="color:#66d9ef">client_id</span>: <span style="color:#e6db74">&#39;$CLIENT_ID&#39;</span>
        <span style="color:#66d9ef">client_secret</span>: <span style="color:#e6db74">&#39;$CLIENT_SECRET&#39;</span>
        <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">9100</span>
    <span style="color:#66d9ef">relabel_configs</span>:
      - <span style="color:#66d9ef">source_labels</span>: [__meta_azure_machine_tag_prometheus]
        <span style="color:#66d9ef">regex</span>: <span style="color:#66d9ef">true</span>.*
        <span style="color:#66d9ef">action</span>: keep
      - <span style="color:#66d9ef">source_labels</span>: [__meta_azure_machine_tag_node_exporter]
        <span style="color:#66d9ef">regex</span>: <span style="color:#66d9ef">true</span>.*
        <span style="color:#66d9ef">action</span>: keep
      - <span style="color:#66d9ef">source_labels</span>: [__meta_azure_machine_tag_public_ip]
        <span style="color:#66d9ef">regex</span>: (.+)
        <span style="color:#66d9ef">action</span>: keep
      - <span style="color:#66d9ef">source_labels</span>: [__meta_azure_machine_name]
        <span style="color:#66d9ef">target_label</span>: instance
      - <span style="color:#66d9ef">source_labels</span>: [__meta_azure_machine_tag_public_ip]
        <span style="color:#66d9ef">regex</span>: (.+)
        <span style="color:#66d9ef">replacement</span>: ${<span style="color:#ae81ff">1</span>}:<span style="color:#ae81ff">9100</span>
        <span style="color:#66d9ef">target_label</span>: __address__
</code></pre></div><p>This is similar to the AWS and Google Cloud Platform configuration. The credentials are configured in the <code>azure_sd_config</code> section.</p>
<p>One point to note here is that we are discovering the Public IP of the instance through a tag. Why is this?</p>
<p><img src="/img/prometheus/azure-sd-metadata-nopubip.png" alt="Azure Service Discovery - No Public IP"></p>
<p>In the above, the public IP is not actually exposed by the metadata. If look at the Terraform configuration again, we can see why: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;azurerm_public_ip&#34; &#34;yetiops-azure-prom-pip&#34;</span> {
  name                <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-azure-prom-pip&#34;</span>
  resource_group_name <span style="color:#f92672">=</span> <span style="color:#66d9ef">azurerm_resource_group</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">azure</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">name</span>
  location            <span style="color:#f92672">=</span> <span style="color:#66d9ef">azurerm_resource_group</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">azure</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">location</span>
  allocation_method   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Dynamic&#34;</span>
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;azurerm_network_interface&#34; &#34;yetiops-azure-prom&#34;</span> {
  name                <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-azure-prom-nic&#34;</span>
  location            <span style="color:#f92672">=</span> <span style="color:#66d9ef">azurerm_resource_group</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">azure</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">location</span>
  resource_group_name <span style="color:#f92672">=</span> <span style="color:#66d9ef">azurerm_resource_group</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">azure</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">name</span>

  <span style="color:#66d9ef">ip_configuration</span> {
    name                          <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yetiops-azure-prom-main&#34;</span>
    subnet_id                     <span style="color:#f92672">=</span> <span style="color:#66d9ef">azurerm_subnet</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">azure</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span>.<span style="color:#66d9ef">id</span>
    private_ip_address_allocation <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Dynamic&#34;</span>
    public_ip_address_id          <span style="color:#f92672">=</span> <span style="color:#66d9ef">azurerm_public_ip</span>.<span style="color:#66d9ef">yetiops</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">azure</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prom</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">pip</span>.<span style="color:#66d9ef">id</span>
  }
}
</code></pre></div><p>A public IP is not assigned automatically when provisioning a machine. Instead you need to create a public IP resource, and attach it to a network interface. This is then attached to the virtual machine. While it may be possible to infer what public IPs a virtual machine has attached, because Azure sees them as a separate resource, the Azure service discovery in Prometheus does not expose them.</p>
<p>To get around this issue, we added the following into our Virtual Machine tags: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl">  tags <span style="color:#f92672">=</span> {
    prometheus <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
    node_exporter <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
    public_ip <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${azurerm_public_ip.yetiops-azure-prom-pip.ip_address}&#34;</span>
  }
</code></pre></div><p>We configure a tag that contains its public IP, and assign it to the Virtual Machine. The Azure Prometheus service discovery metadata includes all of the tags, which will now include one containing the public IP. We can see this in the below: -</p>
<p><img src="/img/prometheus/azure-sd-metadata.png" alt="Azure Service Discovery"></p>
<p>We perform the same operations (keeping services/instances that have the <code>prometheus: true</code> and <code>node_exporter: true</code> labels, and rewriting the <code>__address__</code> to use the public IP). The additional <code>public_ip</code> tag is now also a requirement, otherwise we wouldn&rsquo;t be able to monitor Azure machines.</p>
<p>Can we reach the instance and the Node Exporter?</p>
<p><img src="/img/prometheus/azure-sd-targets.png" alt="Azure Service Discovery Targets"></p>
<p>There we go!</p>
<h2 id="grafana">Grafana</h2>
<p>With the above, we can use any Node Exporter dashboard in Grafana to view all of the discovered instances. The <a href="https://grafana.com/grafana/dashboards/1860">Node Exporter Full</a> is always a good start when using the Node Exporter: -</p>
<p><img src="/img/prometheus/cloud-node-exporter.png" alt="Cloud Node Exporter Dashboard"></p>
<p>As you can see, we have the Azure and EC2 jobs, as well as the GCE (Google Compute Engine) job that is being viewed.</p>
<p>Whenever an extra node is provisioned it will appear in this dashboard.</p>
<h2 id="other-exporters">Other exporters</h2>
<p>If you are running multiple exporters, you will need to create different jobs for each exporter. While many applications try to avoid repeating configuration, the Prometheus project recommend this approach. Additional jobs add little to  overhead to Prometheus over using one job for multiple uses. They also recommend using configuration management to generate the Prometheus jobs, meaning more jobs is just an artifact of your environment, rather than an issue.</p>
<p>You could create something like: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  - <span style="color:#66d9ef">job_name</span>: <span style="color:#e6db74">&#39;gce-win-nodes&#39;</span>
    <span style="color:#66d9ef">gce_sd_configs</span>:
      - <span style="color:#66d9ef">project</span>: yetiops-blog
        <span style="color:#66d9ef">zone</span>: europe-west3-c
        <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">9182</span>
    <span style="color:#66d9ef">relabel_configs</span>:
      - <span style="color:#66d9ef">source_labels</span>: [__meta_gce_label_prometheus]
        <span style="color:#66d9ef">regex</span>: <span style="color:#66d9ef">true</span>.*
        <span style="color:#66d9ef">action</span>: keep
      - <span style="color:#66d9ef">source_labels</span>: [__meta_gce_label_windows_exporter]
        <span style="color:#66d9ef">regex</span>: <span style="color:#66d9ef">true</span>.*
        <span style="color:#66d9ef">action</span>: keep
      - <span style="color:#66d9ef">source_labels</span>: [__meta_gce_public_ip]
        <span style="color:#66d9ef">regex</span>: (.+)
        <span style="color:#66d9ef">replacement</span>: ${<span style="color:#ae81ff">1</span>}:<span style="color:#ae81ff">9182</span>
        <span style="color:#66d9ef">target_label</span>: __address__
      - <span style="color:#66d9ef">source_labels</span>: [__meta_gce_instance_id]
        <span style="color:#66d9ef">target_label</span>: instance
</code></pre></div><p>This would then discover your Windows nodes running the <a href="https://github.com/prometheus-community/windows_exporter">Windows Exporter</a>.</p>
<h2 id="summary">Summary</h2>
<p>The EC2, Azure and Google Cloud Platform service discovery options within Prometheus are incredibly useful. You can cut down on the amount of infrastructure/configuration required for monitoring your instances, without losing flexibility.</p>
<p>The use of tags to discover instances makes it a quick process to enable. Install the Node Exporter, allow it through your cloud firewall/security group, add the <code>prometheus</code> and <code>node_exporter</code> tags, and Prometheus will discover it.</p>
<p>There are uses cases for something like Consul (multiple service discovery, service meshing, common configuration for hybrid environments), but if all you need is a couple of exporters, then the Cloud service discovery options are a very useful option.</p>
]]></content>
        </item>
        
        <item>
            <title>Prometheus - Auto-deploying Consul and Exporters using Saltstack Part 2: Windows</title>
            <link>https://yetiops.net/posts/prometheus-consul-saltstack-part-2-windows/</link>
            <pubDate>Fri, 19 Jun 2020 11:46:32 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/prometheus-consul-saltstack-part-2-windows/</guid>
            <description>This is the second part in my ongoing series on using SaltStack to deploy Consul and Prometheus Exporters, enabling Prometheus to discover machines and services to monitor. You can view the other posts in the series below: -
 Part 1 - Linux Part 3 - OpenBSD Part 4 - FreeBSD Part 5 - illumos Part 6 - MacOS  All of the states (as well as those for future posts, if you want a quick preview) are available in my Salt Lab repository.</description>
            <content type="html"><![CDATA[<p>This is the second part in my ongoing series on using SaltStack to deploy Consul and Prometheus Exporters, enabling Prometheus to discover machines and services to monitor. You can view the other posts in the series below: -</p>
<ul>
<li><a href="/posts/prometheus-consul-saltstack-part-1-linux/">Part 1 - Linux</a></li>
<li><a href="/posts/prometheus-consul-saltstack-part-3-openbsd/">Part 3 - OpenBSD</a></li>
<li><a href="/posts/prometheus-consul-saltstack-part-4-freebsd/">Part 4 - FreeBSD</a></li>
<li><a href="/posts/prometheus-consul-saltstack-part-5-illumos/">Part 5 - illumos</a></li>
<li><a href="/posts/prometheus-consul-saltstack-part-6-macos/">Part 6 - MacOS</a></li>
</ul>
<p>All of the states (as well as those for future posts, if you want a quick preview) are available in my <a href="https://gitlab.com/stuh84/salt-lab">Salt Lab</a> repository.</p>
<p>This post covers Windows Server 2019. The same Salt states have been tested across Windows 2008, 2012 and 2016, so you should not run into any issues for even 12 year old operating systems!</p>
<h2 id="windows-and-saltstack">Windows and SaltStack</h2>
<p>Managing Windows with SaltStack is quite similar to managing Linux or other systems. The state files are similar, the SaltStack configuration files are the same, and the applications themselves are similar too.</p>
<p>The main differences are permissions, paths, and the lack of an included command line text editor. If you are using servers with the GUI (Desktop Experience) enabled, or using RDP to manage a Windows <strong>Core</strong> (i.e. Windows without the GUI) install, then you can use Notepad or other editors with a GUI. If however you are managing the machines entirely via the command line, this may prove to be a problem.</p>
<p>Why not just use RDP? In my scenario, it was because my lab network is isolated inside the machine it is running on (KVM hypervisor), with the machine running headless too. If you run in environments requiring jump/bastion hosts to manage infrastructure, then you may run into issues too. Also, managing via the command line uses far less bandwidth (which may or may not be an issue, depending on your network infrastructure).</p>
<h2 id="configuring-a-static-ip-on-windows">Configuring a static IP on Windows</h2>
<p>If you work primarily with Linux/Unix, you may not be familiar with managing network interfaces on Windows via the command line. You can use the <code>netsh</code> utility, or you can use PowerShell.</p>
<p>You can gain access to Powershell remotely through a virtual machine console, or through <a href="https://docs.microsoft.com/en-us/windows/win32/winrm/installation-and-configuration-for-windows-remote-management">WinRM</a>. It is now possible to use SSH too, which I&rsquo;ll cover later in this post. This is presuming you are managing via a virtual machine console (as the machine may not have an IP yet).</p>
<p>First, you need to get the list of Network Adapters: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-powershell" data-lang="powershell">PS C:\Users\Administrator&gt; Get-NetIPInterface

ifIndex InterfaceAlias                  AddressFamily NlMtu(Bytes) InterfaceMetric Dhcp     ConnectionState PolicyStore
------- --------------                  ------------- ------------ --------------- ----     --------------- -----------
5       Ethernet 2                      IPv6                  1500              25 Enabled  Connected       ActiveStore
2       Ethernet                        IPv6                  1500              25 Enabled  Connected       ActiveStore
1       Loopback Pseudo-Interface 1     IPv6            4294967295              75 Disabled Connected       ActiveStore
5       Ethernet 2                      IPv4                  1500              25 Disabled Connected       ActiveStore
2       Ethernet                        IPv4                  1500              25 Enabled  Connected       ActiveStore
1       Loopback Pseudo-Interface 1     IPv4            4294967295              75 Disabled Connected       ActiveStore
</code></pre></div><p>You need the <code>ifIndex</code> value for the interface you want to configure. In my case, this is <strong>Ethernet 2</strong>, so the <code>ifIndex</code> value is <strong>5</strong>.</p>
<p>After this, apply the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-powershell" data-lang="powershell">PS C:\Users\Administrator&gt; New-NetIPAddress -InterfaceIndex 5 -IPAddress 10.15.31.25 -PrefixLength 24
</code></pre></div><p>Change the <code>IPAddress</code> values and <code>PrefixLength</code> values for your chosen IP Address and Subnet.</p>
<h2 id="enabling-openssh-on-windows">Enabling OpenSSH on Windows</h2>
<p>I decided to enable OpenSSH on Windows, enabling me to manage all the machines in the lab the same way. You can enable this yourself by doing the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-powershell" data-lang="powershell"><span style="color:#75715e"># Install the OpenSSH Server feature</span>
PS C:\Users\Administrator&gt; Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0

<span style="color:#75715e"># Enable the services</span>
PS C:\Users\Administrator&gt; Set-Service -Name sshd -StartupType Automatic
PS C:\Users\Administrator&gt; Set-Service -Name ssh-agent -StartupType Automatic
</code></pre></div><p>After this, you should be able to SSH into the machine. You can also use SCP to transfer files as well.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ssh Administrator@10.15.31.25
Administrator@10.15.31.25<span style="color:#960050;background-color:#1e0010">&#39;</span>s password:

Microsoft Windows <span style="color:#f92672">[</span>Version 10.0.17763.737<span style="color:#f92672">]</span>
<span style="color:#f92672">(</span>c<span style="color:#f92672">)</span> <span style="color:#ae81ff">2018</span> Microsoft Corporation. All rights reserved.

administrator@WIN2019-01 C:<span style="color:#ae81ff">\U</span>sers<span style="color:#ae81ff">\A</span>dministrator&gt;
</code></pre></div><h2 id="installing-the-salt-minion">Installing the Salt Minion</h2>
<p>If you have the desktop experience (i.e. the GUI) enabled, you can download the Salt Minion from <a href="https://repo.saltstack.com/#windows">here</a>, double click the executable and fill in the details of your Salt Master. This will install the Salt Minion, and attempt to register it to your Salt Master.</p>
<p>If you are doing this entirely through the command line though, then follow the below steps: -</p>
<h3 id="download-the-installer">Download the installer</h3>
<p>To download the installer, use the PowerShell <code>Invoke-WebRequest</code> command: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-powershell" data-lang="powershell">PS C:\Users\Administrator&gt; Invoke-WebRequest -uri https<span style="color:#960050;background-color:#1e0010">:</span>//repo.saltstack.com/windows/Salt-Minion-3000.3-Py3-AMD64-Setup.exe -outfile salt-minion.exe
</code></pre></div><p>If you need to specify a proxy server, add the <code>-proxy http://$PROXY_IP:$PORT_PORT</code> option.</p>
<p>Some older versions of Windows do not automatically use TLS when using <code>Invoke-WebRequest</code>, instead attempting with SSLv3. If you find that the download fails due to this, you can preface the command with <code>[Net.ServicePointManager]::SecurityProtocol = &quot;tls12, tls11, tls&quot;;</code>, which enables TLS for the current Powershell session.</p>
<h3 id="installing-the-minion">Installing the minion</h3>
<p>To install the minion run: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-powershell" data-lang="powershell">PS C:\Users\Administrator&gt; Salt-Minion-3000.3-Py3-AMD64-Setup.exe /S /master=$SALT_MASTER /minion-name=$NODENAME
</code></pre></div><p>This should install Salt silently (i.e. without spawning an installation wizard). You can check if this worked by running: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-powershell" data-lang="powershell">PS C:\Users\Administrator&gt; Get-Service <span style="color:#e6db74">&#39;salt-minion&#39;</span>

Status   Name               DisplayName
------   ----               -----------
Running  salt-minion        salt-minion
</code></pre></div><h3 id="configuring-the-minion">Configuring the minion</h3>
<p>While the installer configures the minion, I prefer to change the configuration to including the <code>id</code> and <code>nodename</code> (as I use them in certain states). Windows has no command line text editor out of the box, so we need to find a different way to edit the Minion configuration.</p>
<p>Create the configuration file on a Linux machine, like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">master: salt-master.yetiops.lab
id: win2019-01.yetiops.lab
nodename: win2019-01
</code></pre></div><p>You can then use SCP to transfer it to the machine: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ scp win2019-01-minion.cfg Administrator@10.15.31.25:
</code></pre></div><p>Once this is done, place it in the correct directory with the filename <code>minion</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-powershell" data-lang="powershell">PS C:\Users\Administrator&gt; mv win2019-01-minion.cfg c:\salt\conf\minion
</code></pre></div><p>After this, restart the service (<code>Restart-Service 'salt-minion'</code>). You should now see the machine attempt to register to the Salt Master: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo salt-key -L
Accepted Keys:
alpine-01.yetiops.lab
arch-01.yetiops.lab
centos-01.yetiops.lab
salt-master.yetiops.lab
suse-01.yetiops.lab
ubuntu-01.yetiops.lab
void-01.yetiops.lab
Denied Keys:
Unaccepted Keys:
win2019-01.yetiops.lab
Rejected Keys:
</code></pre></div><p>Accept the key with <code>salt-key -a 'win2019-01*'</code>. Once this is done, you should now be able to manage the machine using Salt: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo salt <span style="color:#e6db74">&#39;win*&#39;</span> test.ping
win2019-01.yetiops.lab:
    True

$ sudo salt <span style="color:#e6db74">&#39;win*&#39;</span> grains.item os
win2019-01.yetiops.lab:
    ----------
    os:
        Windows
</code></pre></div><h2 id="salt-states">Salt States</h2>
<p>We use two sets of states to deploy to Windows. The first deploys Consul, the other deploys the <a href="https://github.com/prometheus-community/windows_exporter">Windows Exporter</a>. The Windows Exporter (previously known as the WMI exporter) is the Windows equivalent of the Prometheus Node Exporter. It exposes CPU, memory, storage, service states and much more.</p>
<h3 id="applying-salt-states">Applying Salt States</h3>
<p>Once you have configured the states detailed below, use one of the following options to deploy the changes to the Windows machine: -</p>
<ul>
<li><code>salt '*' state.highstate</code> from the Salt server (to configure every machine and every state)</li>
<li><code>salt 'win*' state.highstate</code> from the Salt server (to configure all machines with a name beginning with <code>win*</code>, applying all states)</li>
<li><code>salt 'win*.' state.apply consul</code> from the Salt server (to configure all machines with a name beginning with <code>win*</code>, applying only the <code>consul</code> state)</li>
<li><code>salt-call state.highstate</code> from a machine running the Salt agent (to configure just one machine with all states)</li>
<li><code>salt-call state.apply consul</code> from a machine running the Salt agent (to configure just one machine with only the <code>consul</code> state)</li>
</ul>
<p>You can also use the <code>salt -C</code> option to apply based upon grains, pillars or other types of matches. For example, to apply to all machines running a Windows kernel, you could run <code>salt -C 'G@kernel:Windows' state.highstate</code>.</p>
<h3 id="consul---deployment">Consul - Deployment</h3>
<h4 id="states">States</h4>
<p>The following Salt state is used to deploy Consul onto a Windows host: -</p>
<p><strong>/srv/salt/states/consul/windows.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul_user</span>:
  <span style="color:#66d9ef">user.present</span>:
    - <span style="color:#66d9ef">name</span>: Consul
    - <span style="color:#66d9ef">groups</span>:
      - Users
    - <span style="color:#66d9ef">fullname</span>: Hashicorp Consul
    - <span style="color:#66d9ef">enforce_password</span>: False

<span style="color:#66d9ef">consul_base_dir</span>:
  <span style="color:#66d9ef">file.directory</span>:
  - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#39;C:\consul&#39;</span>
  - <span style="color:#66d9ef">win_owner</span>: Consul
  - <span style="color:#66d9ef">win_perms</span>: {<span style="color:#66d9ef">&#39;Users&#39;</span>: {<span style="color:#66d9ef">&#39;perms&#39;: &#39;full_control&#39;, &#39;applies_to&#39;</span>: <span style="color:#e6db74">&#39;this_folder_only&#39;</span>}}
  - <span style="color:#66d9ef">makedirs</span>: True

{% if not salt[<span style="color:#e6db74">&#39;file.file_exists&#39;</span>](<span style="color:#e6db74">&#39;c:\consul\consul.exe&#39;</span>) %}

<span style="color:#66d9ef">retrieve_consul</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#39;[Net.ServicePointManager]::SecurityProtocol = &#34;tls12, tls11, tls&#34;; Invoke-WebRequest -uri https://releases.hashicorp.com/consul/1.7.2/consul_1.7.2_windows_amd64.zip -OutFile c:\consul\consul.zip&#39;</span>
    - <span style="color:#66d9ef">shell</span>: powershell

<span style="color:#66d9ef">extract_consul</span>:
  <span style="color:#66d9ef">archive.extracted</span>:
    - <span style="color:#66d9ef">name</span>: c:\consul
    - <span style="color:#66d9ef">enforce_toplevel</span>: <span style="color:#66d9ef">false</span>
    - <span style="color:#66d9ef">source</span>: c:\consul\consul.zip

<span style="color:#66d9ef">remove_consul_zip</span>:
  <span style="color:#66d9ef">file.absent</span>:
    - <span style="color:#66d9ef">name</span>: c:\consul\consul.zip
{% endif %}

<span style="color:#66d9ef">consul_conf_dir</span>:
  <span style="color:#66d9ef">file.directory</span>:
  - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#39;C:\consul\conf&#39;</span>
  - <span style="color:#66d9ef">win_owner</span>: Consul
  - <span style="color:#66d9ef">win_perms</span>: {<span style="color:#66d9ef">&#39;Users&#39;</span>: {<span style="color:#66d9ef">&#39;perms&#39;: &#39;full_control&#39;, &#39;applies_to&#39;</span>: <span style="color:#e6db74">&#39;this_folder_only&#39;</span>}}
  - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">consul_data_dir</span>:
  <span style="color:#66d9ef">file.directory</span>:
  - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#39;C:\consul\data&#39;</span>
  - <span style="color:#66d9ef">win_owner</span>: Consul
  - <span style="color:#66d9ef">win_perms</span>: {<span style="color:#66d9ef">&#39;Users&#39;</span>: {<span style="color:#66d9ef">&#39;perms&#39;: &#39;full_control&#39;, &#39;applies_to&#39;</span>: <span style="color:#e6db74">&#39;this_folder_only&#39;</span>}}
  - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">c:\consul\conf\consul.hcl</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://consul/client/files/consul.hcl.j2
    - <span style="color:#66d9ef">win_owner</span>: consul
    - <span style="color:#66d9ef">template</span>: jinja

{% if salt[<span style="color:#e6db74">&#39;service.available&#39;</span>](<span style="color:#e6db74">&#39;consul&#39;</span>) %}
<span style="color:#66d9ef">modify_consul_service</span>:
  <span style="color:#66d9ef">module.run</span>:
    - <span style="color:#66d9ef">name</span>: service.modify
    - <span style="color:#66d9ef">m_name</span>: consul
    - <span style="color:#66d9ef">bin_path</span>: <span style="color:#e6db74">&#39;c:\consul\consul.exe&#39;</span>
    - <span style="color:#66d9ef">display_name</span>: <span style="color:#e6db74">&#39;HashiCorp Consul Client Agent&#39;</span>
    - <span style="color:#66d9ef">exe_args</span>: <span style="color:#e6db74">&#39;agent -config-dir=c:\consul\conf&#39;</span>
    - <span style="color:#66d9ef">start_type</span>: auto
{% else %}
<span style="color:#66d9ef">create_consul_service</span>:
  <span style="color:#66d9ef">module.run</span>:
    - <span style="color:#66d9ef">name</span>: service.create
    - <span style="color:#66d9ef">m_name</span>: consul
    - <span style="color:#66d9ef">bin_path</span>: <span style="color:#e6db74">&#39;c:\consul\consul.exe&#39;</span>
    - <span style="color:#66d9ef">display_name</span>: <span style="color:#e6db74">&#39;HashiCorp Consul Client Agent&#39;</span>
    - <span style="color:#66d9ef">exe_args</span>: <span style="color:#e6db74">&#39;agent -config-dir=c:\consul\conf&#39;</span>
    - <span style="color:#66d9ef">start_type</span>: auto
{% endif %}

<span style="color:#66d9ef">running_consul_service</span>:
  <span style="color:#66d9ef">service.running</span>:
    - <span style="color:#66d9ef">name</span>: consul

{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>] is defined %}
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;prometheus_services&#39;</span>] is defined %}
{% for service in pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;prometheus_services&#39;</span>] %}
c:\consul\conf\{{ <span style="color:#66d9ef">service }}.hcl</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://consul/services/files/{{ service }}.hcl
    - <span style="color:#66d9ef">win_owner</span>: consul
    - <span style="color:#66d9ef">template</span>: jinja

consul_reload_{{ <span style="color:#66d9ef">service }}</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: c:\consul\consul.exe reload
    - <span style="color:#66d9ef">shell</span>: powershell
    - <span style="color:#66d9ef">watch</span>:
      - <span style="color:#66d9ef">file</span>: c:\consul\conf\{{ service }}.hcl
{% endfor %}
{% endif %}
{% endif %}
</code></pre></div><p>As mentioned, the state isn&rsquo;t too dissimilar to the Linux Consul state. The biggest differences are the paths (e.g. <code>c:\</code> vs <code>/</code> as your base/root directory), and permissions in Windows are different than in Linux or Unix.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul_user</span>:
  <span style="color:#66d9ef">user.present</span>:
    - <span style="color:#66d9ef">name</span>: Consul
    - <span style="color:#66d9ef">groups</span>:
      - Users
    - <span style="color:#66d9ef">fullname</span>: Hashicorp Consul
    - <span style="color:#66d9ef">enforce_password</span>: False
</code></pre></div><p>Rather than creating a Consul-specific group, we leverage the Users group. We do not need more permissions than this, so this is sufficient. We also avoid setting a password, as this is not required for this user.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul_base_dir</span>:
  <span style="color:#66d9ef">file.directory</span>:
  - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#39;C:\consul&#39;</span>
  - <span style="color:#66d9ef">win_owner</span>: Consul
  - <span style="color:#66d9ef">win_perms</span>: {<span style="color:#66d9ef">&#39;Users&#39;</span>: {<span style="color:#66d9ef">&#39;perms&#39;: &#39;full_control&#39;, &#39;applies_to&#39;</span>: <span style="color:#e6db74">&#39;this_folder_only&#39;</span>}}
  - <span style="color:#66d9ef">makedirs</span>: True
</code></pre></div><p>This creates a folder called <code>C:\consul</code>, owned by the Consul user, controllable by the Users group. We use <code>makedirs: True</code> so that directories will be created if they do not already exist.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">{% if not salt[<span style="color:#e6db74">&#39;file.file_exists&#39;</span>](<span style="color:#e6db74">&#39;c:\consul\consul.exe&#39;</span>) %}

<span style="color:#66d9ef">retrieve_consul</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#39;[Net.ServicePointManager]::SecurityProtocol = &#34;tls12, tls11, tls&#34;; Invoke-WebRequest -uri https://releases.hashicorp.com/consul/1.7.2/consul_1.7.2_windows_amd64.zip -OutFile c:\consul\consul.zip&#39;</span>
    - <span style="color:#66d9ef">shell</span>: powershell

<span style="color:#66d9ef">extract_consul</span>:
  <span style="color:#66d9ef">archive.extracted</span>:
    - <span style="color:#66d9ef">name</span>: c:\consul
    - <span style="color:#66d9ef">enforce_toplevel</span>: <span style="color:#66d9ef">false</span>
    - <span style="color:#66d9ef">source</span>: c:\consul\consul.zip

<span style="color:#66d9ef">remove_consul_zip</span>:
  <span style="color:#66d9ef">file.absent</span>:
    - <span style="color:#66d9ef">name</span>: c:\consul\consul.zip
{% endif %}
</code></pre></div><p>Here we download the Consul zip archive containing the binary <code>consul.exe</code>, extract it to the <code>c:\consul</code> directory and remove the <code>consul.zip</code> file once it is extracted. This only runs if the <code>c:\consul\consul.exe</code> file does not already exist. This stops the task running on subsequent state applies.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul_conf_dir</span>:
  <span style="color:#66d9ef">file.directory</span>:
  - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#39;C:\consul\conf&#39;</span>
  - <span style="color:#66d9ef">win_owner</span>: Consul
  - <span style="color:#66d9ef">win_perms</span>: {<span style="color:#66d9ef">&#39;Users&#39;</span>: {<span style="color:#66d9ef">&#39;perms&#39;: &#39;full_control&#39;, &#39;applies_to&#39;</span>: <span style="color:#e6db74">&#39;this_folder_only&#39;</span>}}
  - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">consul_data_dir</span>:
  <span style="color:#66d9ef">file.directory</span>:
  - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#39;C:\consul\data&#39;</span>
  - <span style="color:#66d9ef">win_owner</span>: Consul
  - <span style="color:#66d9ef">win_perms</span>: {<span style="color:#66d9ef">&#39;Users&#39;</span>: {<span style="color:#66d9ef">&#39;perms&#39;: &#39;full_control&#39;, &#39;applies_to&#39;</span>: <span style="color:#e6db74">&#39;this_folder_only&#39;</span>}}
  - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">c:\consul\conf\consul.hcl</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://consul/client/files/consul.hcl.j2
    - <span style="color:#66d9ef">win_owner</span>: consul
    - <span style="color:#66d9ef">template</span>: jinja
</code></pre></div><p>Here we create the <code>c:\consul\conf</code> directory (for Consul configuration files) and <code>c:\consul\data</code> (where Consul stores its data and running state). We also create the <code>c:\consul\conf\consul.hcl</code> file. This template used is the same regardless of operating system.</p>
<pre><code class="language-jinja2" data-lang="jinja2">{
{%- for ip in grains['ipv4'] %}
{%- if '10.15.31' in ip %}
  &quot;advertise_addr&quot;: &quot;{{ ip }}&quot;,
  &quot;bind_addr&quot;: &quot;{{ ip }}&quot;,
{%- endif %}
{%- endfor %}
  &quot;data_dir&quot;: &quot;{{ pillar['consul']['data_dir'] }}&quot;,
  &quot;datacenter&quot;: &quot;{{ pillar['consul']['dc'] }}&quot;,
  &quot;encrypt&quot;: &quot;{{ pillar['consul']['enc_key'] }}&quot;,
  &quot;node_name&quot;: &quot;{{ grains['nodename'] }}&quot;,
  &quot;retry_join&quot;: [
{%- for server in pillar['consul']['servers'] %}
    &quot;{{ server }}&quot;,
{%- endfor %}
  ],
  &quot;server&quot;: false,
}
</code></pre><p>The only difference this file has when generated, compared to the other hosts in the lab, is the <code>data_dir</code>. We will see this in the pillars later.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">{% if salt[<span style="color:#e6db74">&#39;service.available&#39;</span>](<span style="color:#e6db74">&#39;consul&#39;</span>) %}
<span style="color:#66d9ef">modify_consul_service</span>:
  <span style="color:#66d9ef">module.run</span>:
    - <span style="color:#66d9ef">name</span>: service.modify
    - <span style="color:#66d9ef">m_name</span>: consul
    - <span style="color:#66d9ef">bin_path</span>: <span style="color:#e6db74">&#39;c:\consul\consul.exe&#39;</span>
    - <span style="color:#66d9ef">display_name</span>: <span style="color:#e6db74">&#39;HashiCorp Consul Client Agent&#39;</span>
    - <span style="color:#66d9ef">exe_args</span>: <span style="color:#e6db74">&#39;agent -config-dir=c:\consul\conf&#39;</span>
    - <span style="color:#66d9ef">start_type</span>: auto
{% else %}
<span style="color:#66d9ef">create_consul_service</span>:
  <span style="color:#66d9ef">module.run</span>:
    - <span style="color:#66d9ef">name</span>: service.create
    - <span style="color:#66d9ef">m_name</span>: consul
    - <span style="color:#66d9ef">bin_path</span>: <span style="color:#e6db74">&#39;c:\consul\consul.exe&#39;</span>
    - <span style="color:#66d9ef">display_name</span>: <span style="color:#e6db74">&#39;HashiCorp Consul Client Agent&#39;</span>
    - <span style="color:#66d9ef">exe_args</span>: <span style="color:#e6db74">&#39;agent -config-dir=c:\consul\conf&#39;</span>
    - <span style="color:#66d9ef">start_type</span>: auto
{% endif %}
</code></pre></div><p>In this section, we use <code>module.run</code>, which is where Salt uses modules rather than state directives. States use the &ldquo;desired state&rdquo; approach, where if what we aim to achieve (e.g. configuration files generated by Salt match what is already on the machine) then no changes are made.</p>
<p>Modules are intended for running ad-hoc jobs (running a command or making changes to a file once). Every time they run, they try to make the same changes, regardless of whether the intended state matches what is already on the machine.</p>
<p>Currently there is no Salt state directive for defining Windows services natively, only modules. Because of this, we implement some conditional logic (<strong>if/else</strong>). If the service already exists (i.e. <code>service.available</code>), modify the existing service. If it does not, create the service.</p>
<p>The <code>name</code> value states the module we will use. The <code>m_name</code> is used in place of supplying the <code>name</code> argument to the module itself. This is the same as running <code>salt 'win2019-01*' service.modify name=consul</code>.</p>
<p>The out come is that there will always be a module run on every state apply, regardless of whether any changes need to happen (either creation or modification of the service). This isn&rsquo;t ideal, but it is better to do this than adding more complexity (e.g. pulling back each field from an existing service and checking it).</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">running_consul_service</span>:
  <span style="color:#66d9ef">service.running</span>:
    - <span style="color:#66d9ef">name</span>: consul

{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>] is defined %}
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;prometheus_services&#39;</span>] is defined %}
{% for service in pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;prometheus_services&#39;</span>] %}
c:\consul\conf\{{ <span style="color:#66d9ef">service }}.hcl</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://consul/services/files/{{ service }}.hcl
    - <span style="color:#66d9ef">win_owner</span>: consul
    - <span style="color:#66d9ef">template</span>: jinja

consul_reload_{{ <span style="color:#66d9ef">service }}</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: c:\consul\consul.exe reload
    - <span style="color:#66d9ef">shell</span>: powershell
    - <span style="color:#66d9ef">watch</span>:
      - <span style="color:#66d9ef">file</span>: c:\consul\conf\{{ service }}.hcl
{% endfor %}
{% endif %}
{% endif %}
</code></pre></div><p>The above states are very similar to what we use for Linux. We ensure the service runs, and then we add the HCL configuration files for services we would like to register with Consul.</p>
<p>The state is applied as such: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">base</span>:
  <span style="color:#66d9ef">&#39;G@init:systemd and G@kernel:Linux&#39;</span>:
    - <span style="color:#66d9ef">match</span>: compound
    - consul
    - exporters.node_exporter.systemd

  <span style="color:#66d9ef">&#39;os:Alpine&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.alpine
    - exporters.node_exporter.alpine

  <span style="color:#66d9ef">&#39;os:Void&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.void
    - exporters.node_exporter.void

  <span style="color:#66d9ef">&#39;kernel:Windows&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.windows
</code></pre></div><p>In the above, we match any OS which has a Salt grain of <code>kernel: Windows</code>, demonstrated below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ salt <span style="color:#e6db74">&#39;win2019-01*&#39;</span> grains.item kernel
win2019-01.yetiops.lab:
    ----------
    kernel:
        Windows

$ salt -C <span style="color:#e6db74">&#39;G@kernel:Windows&#39;</span> test.ping
win2019-01.yetiops.lab:
    True
</code></pre></div><p>In our lab, this only matches one machine. If we add more Windows machines, then we would see them here too.</p>
<h4 id="pillars">Pillars</h4>
<p>The pillars (i.e. the host/group specific variables) are defined as such: -</p>
<p><strong>consul-client-win.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul</span>:
  <span style="color:#66d9ef">data_dir</span>: c:/consul/data
  <span style="color:#66d9ef">prometheus_services</span>:
  - windows_exporter
</code></pre></div><p><strong>consul-dc.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul</span>:
  <span style="color:#66d9ef">dc</span>: yetiops
  <span style="color:#66d9ef">enc_key</span>: <span style="color:#75715e">###CONSUL_KEY### </span>
  <span style="color:#66d9ef">servers</span>:
  - salt-master.yetiops.lab
</code></pre></div><p>These pillars reside in <code>/srv/salt/pillars/consul</code>. The <code>consul-dc.sls</code> file is the same as we use for all of the Consul clients.</p>
<p>The differences here compared to the other hosts are that we have a different <code>data_dir</code>, and that we want to register the <code>windows_exporter</code> service with Consul rather than <code>node_exporter</code>.</p>
<p>A point to note is that Consul expects the directories to use forward-slashes, even on Windows. If you supply them as a standard Windows path (e.g. <code>c:\consul\data</code>), Consul will not start.</p>
<p>The Consul service file that will get deployed based upon the above is: -</p>
<p><strong>/srv/salt/states/consul/services/windows_exporter.hcl</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{<span style="color:#f92672">&#34;service&#34;</span>:
  {<span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;windows_exporter&#34;</span>,
   <span style="color:#f92672">&#34;tags&#34;</span>: [<span style="color:#e6db74">&#34;windows_exporter&#34;</span>, <span style="color:#e6db74">&#34;prometheus&#34;</span>],
   <span style="color:#f92672">&#34;port&#34;</span>: <span style="color:#ae81ff">9182</span>
  }
}
</code></pre></div><p>This is tagged with <code>prometheus</code> (so Prometheus knows to match against this service) and <code>windows_exporter</code> (for easy grouping when looking through the Consul UI or API).</p>
<p>These pillars are applied as such: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">base</span>:
  <span style="color:#66d9ef">&#39;*&#39;</span>:
    - consul.consul-dc

  <span style="color:#66d9ef">&#39;G@kernel:Linux&#39;</span>:
    - <span style="color:#66d9ef">match</span>: compound
    - consul.consul

  <span style="color:#66d9ef">&#39;kernel:Windows&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.consul-client-win

  <span style="color:#66d9ef">&#39;salt-master*&#39;</span>:
    - consul.consul-server
</code></pre></div><p>As before, the <code>consul.consul-dc</code> pillar is applied to everything. We also apply the <code>consul.consul-client-win</code> pillar only to Windows. We are not running any Windows hosts as the Consul server, so this is enough.</p>
<h3 id="consul---verification">Consul - Verification</h3>
<p>We can verify that Consul is working with the below: -</p>
<p><strong>On the Windows host</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-powershell" data-lang="powershell">administrator@WIN2019-01 c:\consul&gt;.\consul.exe members
Node                   Address           Status  Type    Build  Protocol  DC       Segment
salt-master            10.15.31.5<span style="color:#960050;background-color:#1e0010">:</span>8301   alive   server  1.7.3  2         yetiops  &lt;all&gt;
alpine-01              10.15.31.27<span style="color:#960050;background-color:#1e0010">:</span>8301  alive   client  1.7.3  2         yetiops  &lt;<span style="color:#66d9ef">default</span>&gt;
arch-01                10.15.31.26<span style="color:#960050;background-color:#1e0010">:</span>8301  alive   client  1.7.3  2         yetiops  &lt;<span style="color:#66d9ef">default</span>&gt;
centos-01.yetiops.lab  10.15.31.24<span style="color:#960050;background-color:#1e0010">:</span>8301  alive   client  1.7.3  2         yetiops  &lt;<span style="color:#66d9ef">default</span>&gt;
suse-01                10.15.31.22<span style="color:#960050;background-color:#1e0010">:</span>8301  alive   client  1.7.3  2         yetiops  &lt;<span style="color:#66d9ef">default</span>&gt;
ubuntu-01              10.15.31.33<span style="color:#960050;background-color:#1e0010">:</span>8301  alive   client  1.7.3  2         yetiops  &lt;<span style="color:#66d9ef">default</span>&gt;
void-01                10.15.31.31<span style="color:#960050;background-color:#1e0010">:</span>8301  alive   client  1.7.2  2         yetiops  &lt;<span style="color:#66d9ef">default</span>&gt;
win2019-01             10.15.31.25<span style="color:#960050;background-color:#1e0010">:</span>8301  alive   client  1.7.2  2         yetiops  &lt;<span style="color:#66d9ef">default</span>&gt;

administrator@WIN2019-01 c:\consul&gt;.\consul.exe catalog nodes -service=windows_exporter
Node        ID        Address      DC
win2019-01  7a185c82  10.15.31.25  yetiops
</code></pre></div><p><strong>On a Linux host</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ consul members    
Node                   Address           Status  Type    Build  Protocol  DC       Segment
salt-master            10.15.31.5:8301   alive   server  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;all&gt;
alpine-01              10.15.31.27:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
arch-01                10.15.31.26:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
centos-01.yetiops.lab  10.15.31.24:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
suse-01                10.15.31.22:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
ubuntu-01              10.15.31.33:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
void-01                10.15.31.31:8301  alive   client  1.7.2  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
win2019-01             10.15.31.25:8301  alive   client  1.7.2  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;

$ consul catalog nodes -service windows_exporter
Node        ID        Address      DC
win2019-01  7a185c82  10.15.31.25  yetiops
</code></pre></div><h3 id="windows-exporter---deployment">Windows Exporter - Deployment</h3>
<p>As in the Linux post, we have informed Consul that the <code>windows_exporter</code> service should be running. This state is what deploys the exporter itself.</p>
<h4 id="states-1">States</h4>
<p>The following Salt states are used to deploy the Prometheus Windows Exporter onto a Windows host: -</p>
<p><strong>win_exporter.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">exporter_user</span>:
  <span style="color:#66d9ef">user.present</span>:
    - <span style="color:#66d9ef">name</span>: exporter
    - <span style="color:#66d9ef">groups</span>:
      - Users
    - <span style="color:#66d9ef">fullname</span>: Prometheus Exporter User 
    - <span style="color:#66d9ef">enforce_password</span>: False

<span style="color:#66d9ef">exporter_base_dir</span>:
  <span style="color:#66d9ef">file.directory</span>:
  - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#39;C:\exporter&#39;</span>
  - <span style="color:#66d9ef">win_owner</span>: exporter
  - <span style="color:#66d9ef">win_perms</span>: {<span style="color:#66d9ef">&#39;Users&#39;</span>: {<span style="color:#66d9ef">&#39;perms&#39;: &#39;full_control&#39;, &#39;applies_to&#39;</span>: <span style="color:#e6db74">&#39;this_folder_only&#39;</span>}}
  - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">exporter_textfile_dir</span>:
  <span style="color:#66d9ef">file.directory</span>:
  - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#39;C:\exporter\textfile&#39;</span>
  - <span style="color:#66d9ef">win_owner</span>: exporter
  - <span style="color:#66d9ef">win_perms</span>: {<span style="color:#66d9ef">&#39;Users&#39;</span>: {<span style="color:#66d9ef">&#39;perms&#39;: &#39;full_control&#39;, &#39;applies_to&#39;</span>: <span style="color:#e6db74">&#39;this_folder_only&#39;</span>}}
  - <span style="color:#66d9ef">makedirs</span>: True
</code></pre></div><p>The above state is a prerequisite state for all exporters we may install on Windows. This creates the <strong>exporter</strong> user and folder, rather than setting this up for every exporter we install. As with the Consul state, we do not enforce a password, and we place the <code>exporter</code> user in the <strong>Users</strong> group.</p>
<p>Without this state, we would have a lot of boilerplate/repeated tasks to create the same folders for every additional exporter.</p>
<p><strong>windows_exporter.sls</strong></p>
<pre><code>windows_exporter_dir:
  file.directory:
  - name: 'C:\exporter\windows_exporter'
  - win_owner: exporter
  - win_perms: {'Users': {'perms': 'full_control', 'applies_to': 'this_folder_only'}}
  - makedirs: True

{% if not salt['file.file_exists']('c:\exporter\windows_exporter\windows_exporter.msi') %}

retrieve_windows_exporter:
  cmd.run:
    - name: '[Net.ServicePointManager]::SecurityProtocol = &quot;tls12, tls11, tls&quot;; Invoke-WebRequest -uri https://github.com/prometheus-community/windows_exporter/releases/download/v0.13.0/windows_exporter-0.13.0-amd64.msi -OutFile c:\exporter\windows_exporter\windows_exporter.msi'
    - shell: powershell
{% endif %}

{% if not salt['service.available']('windows_exporter') %}
windows_exporter_install:
  cmd.run:
    - name: msiexec /i C:\exporter\windows_exporter\windows_exporter.msi /quiet ENABLED_COLLECTORS=&quot;cpu,cs,logical_disk,memory,net,os,process,service,system,textfile&quot; TEXTFILE_DIR=&quot;C:\exporter\textfile&quot;
    - shell: powershell
{% endif %}
</code></pre><p>Because the Windows Exporter comes with its own installation file (i.e. an MSI), it automatically creates the Windows services and installs the exporter into the correct location. This means the state has far fewer parts than some of the states for the Node Exporter (especially the Linux SystemD state).</p>
<p>As with Consul, we need to specify that the server will require TLS for retrieving the installer. We only download the MSI if it does not already exist on the machine.</p>
<p>Finally, we check if the <code>windows_exporter</code> service is available, and if not, we install from the MSI. We specify the collectors we want enabled (as some are not enabled by default). We also enable the Textfile directory in case we want to run scripts that generate their own metrics.</p>
<p>These states are applied with the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">base</span>:
  <span style="color:#66d9ef">&#39;G@init:systemd and G@kernel:Linux&#39;</span>:
    - <span style="color:#66d9ef">match</span>: compound
    - consul
    - exporters.node_exporter.systemd

  <span style="color:#66d9ef">&#39;os:Alpine&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.alpine
    - exporters.node_exporter.alpine

  <span style="color:#66d9ef">&#39;os:Void&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.void
    - exporters.node_exporter.void

  <span style="color:#66d9ef">&#39;kernel:Windows&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.windows
    - exporters.windows_exporter.win_exporter
    - exporters.windows_exporter.windows_exporter
</code></pre></div><p>As you can see, we apply the <code>win_exporter</code> and <code>windows_exporter</code> states to any host running the Windows kernel.</p>
<h4 id="pillars-1">Pillars</h4>
<p>There are no pillars in this lab specific to the Windows Exporter.</p>
<h3 id="windows-exporter---verification">Windows Exporter - Verification</h3>
<p>After this, we should see that the service is running: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-powershell" data-lang="powershell">PS C:\Users\Administrator&gt; Get-Service <span style="color:#e6db74">&#39;windows_exporter&#39;</span> 

Status   Name               DisplayName
------   ----               -----------
Running  windows_exporter   windows_exporter
</code></pre></div><p>We should also see that it is listening on TCP port <strong>9182</strong>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-powershell" data-lang="powershell">PS C:\Users\Administrator&gt; netstat -ano | Select-String 9182

  TCP    0.0.0.0<span style="color:#960050;background-color:#1e0010">:</span>9182           0.0.0.0<span style="color:#960050;background-color:#1e0010">:</span>0              LISTENING       2140 
  TCP    10.15.31.25<span style="color:#960050;background-color:#1e0010">:</span>9182       10.15.31.254<span style="color:#960050;background-color:#1e0010">:</span>42876     ESTABLISHED     2140
  TCP    [::]<span style="color:#960050;background-color:#1e0010">:</span>9182              [::]<span style="color:#960050;background-color:#1e0010">:</span>0                 LISTENING       2140
</code></pre></div><p>Finally, we can run a cURL request from another machine to make sure it is responding: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ curl 10.15.31.25:9182/metrics | grep -i <span style="color:#ae81ff">2019</span> 

windows_cs_hostname<span style="color:#f92672">{</span>domain<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;WORKGROUP&#34;</span>,fqdn<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;win2019-01&#34;</span>,hostname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;win2019-01&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>
windows_os_info<span style="color:#f92672">{</span>product<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Microsoft Windows Server 2019 Standard Evaluation&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;10.0.17763&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>
</code></pre></div><h2 id="prometheus-targets">Prometheus Targets</h2>
<p>As Prometheus is already set up (see <a href="/posts/prometheus-consul-saltstack-part-1-linux/#configuring-prometheus">here</a>), and matches on the <code>prometheus</code> tag, we should see this within the Prometheus targets straight away: -</p>
<p><img src="/img/prometheus/prometheus-consul-targets-windows.png" alt="Prometheus Consul SD Targets Windows"></p>
<p>The last one in the list is the Windows machine, successfully being scraped.</p>
<p><img src="/img/prometheus/prometheus-consul-sd-windows.png" alt="Prometheus Consul SD MetaData Windows"></p>
<p>Above is the Metadata we receive from Consul about this host</p>
<h2 id="grafana-dashboards">Grafana Dashboards</h2>
<p>The Windows node <a href="https://grafana.com/grafana/dashboards/2129">dashboard</a> is a good place to start. However as the exporter is now the Windows Exporter (which includes changing all metrics to be prefixed by <code>windows_</code> rather than <code>wmi_</code>), this version of the dashboard will not work any longer.</p>
<p>A version has already been created that changes the prefix, <a href="https://grafana.com/grafana/dashboards/12422">here</a>.</p>
<p><img src="/img/prometheus/prometheus-grafana-windows-exporter.png" alt="Prometheus Grafana Windows Exporter Dashboard"></p>
<p>Another to look at is the <a href="https://grafana.com/grafana/dashboards/6593">Windows Node with Process Info dashboard</a>. This one was made previous to the WMI to Windows exporter change, so you will need to update it to work with the different metric prefix (a find/replace in the JSON file would potentially cover most of it)</p>
<h2 id="summary">Summary</h2>
<p>For those who are in a mixed environment of different systems, the approach to monitoring and managing both can often be quite disparate. However thanks to SaltStack and Consul, we can treat both Linux and Windows equally.</p>
<p>Not only are we saving time by deploying our monitoring automatically, but also by using the same systems to manage them too. With a little bit of effort in creating the States in the first place, you can make your infrastructure feel less like separate silos of operating systems and more like one environment.</p>
<p>In the next post in this series, we will cover how you deploy SaltStack on an OpenBSD host, which will then deploy Consul and the Prometheus Node Exporter.</p>
]]></content>
        </item>
        
        <item>
            <title>Prometheus - Auto-deploying Consul and Exporters using Saltstack Part 1: Linux</title>
            <link>https://yetiops.net/posts/prometheus-consul-saltstack-part-1-linux/</link>
            <pubDate>Mon, 15 Jun 2020 13:19:42 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/prometheus-consul-saltstack-part-1-linux/</guid>
            <description>Prometheus is a monitoring tool that uses a time-series database to store metrics, gathered from multiple endpoints. These endpoints are either applications, or agents running alongside the applications themselves. These agents are known as exporters. There are exporters for everything from AWS Cloudwatch to Plex.
If you are a regular visitor to this site, you will have seen posts on Prometheus before. One of the posts covered monitoring OpenWRT, Windows, OpenBSD and FreeBSD.</description>
            <content type="html"><![CDATA[<p><a href="https://prometheus.io">Prometheus</a> is a monitoring tool that uses a time-series database to store metrics, gathered from multiple endpoints. These endpoints are either applications, or agents running alongside the applications themselves. These agents are known as  <strong>exporters</strong>. There are <strong>exporters</strong> for everything from <a href="https://github.com/prometheus/cloudwatch_exporter">AWS Cloudwatch</a> to <a href="https://github.com/othalla/plex_exporter">Plex</a>.</p>
<p>If you are a regular visitor to this site, you will have seen posts on Prometheus <a href="/categories/prometheus/">before</a>. One of the posts covered monitoring OpenWRT, Windows, OpenBSD and FreeBSD. Since then, I have started to look into monitoring other systems, like <a href="https://www.illumos.org/">illumos-based</a> and others.</p>
<p>I also wrote a <a href="/posts/prometheus-consul-node_exporter/">post</a> on how to use <a href="https://consul.io">Consul</a> to discover hosts and the services running on them, so that Prometheus can monitor them.</p>
<p>In addition, I have <a href="/posts/saltstack-introduction/">written</a> about using <a href="https://www.saltstack.com/">SaltStack</a> for configuration management of different operating systems.</p>
<h2 id="new-series">New Series</h2>
<p>As I am now in the process of deploying Consul and Prometheus across my workplace&rsquo;s infrastructure using SaltStack, I have decided to share what I have learned in a new series. This will cover deploying across the following systems: -</p>
<ul>
<li>Linux (SystemD-based, as well as Alpine and Void Linux)</li>
<li>OpenBSD</li>
<li>FreeBSD</li>
<li>Windows</li>
<li>illumos (specifically OmniOS)</li>
<li>MacOS</li>
</ul>
<p>This first post will cover how to deploy SaltStack, Consul and the Prometheus Node Exporter on Linux.</p>
<p>You can view the other posts in the series below: -</p>
<ul>
<li><a href="/posts/prometheus-consul-saltstack-part-2-windows/">Part 2 - Windows</a></li>
<li><a href="/posts/prometheus-consul-saltstack-part-3-openbsd/">Part 3 - OpenBSD</a></li>
<li><a href="/posts/prometheus-consul-saltstack-part-4-freebsd/">Part 4 - FreeBSD</a></li>
<li><a href="/posts/prometheus-consul-saltstack-part-5-illumos/">Part 5 - illumos</a></li>
<li><a href="/posts/prometheus-consul-saltstack-part-6-macos/">Part 6 - MacOS</a></li>
</ul>
<h2 id="setting-up-your-saltstack-environment">Setting up your SaltStack environment</h2>
<p>In my post <a href="/posts/saltstack-introduction/">Configuration Seasoning: Getting started with Saltstack</a>, I covered how to setup a Salt &ldquo;master&rdquo; (the central Salt server that agents connect to), version control of your Salt states and pillars, and also how to use it across teams (use file-system ACLs).</p>
<p>The <code>salt-master</code> in my lab is set up exactly as per the above article (I even followed my own tutorial to create it!). From here, I will refer to it as the Salt central server.</p>
<p>Every machine in this lab is running on a Dell Optiplex 3020, running Ubuntu 20.04, using KVM for the virtual machines. Each machine has two network interfaces: -</p>
<ul>
<li>Primary - NAT to the internet for obtaining packages/updates
<ul>
<li>Subnet - <code>192.168.122.0/24</code></li>
</ul>
</li>
<li>Management - All communication between the machines (including between Salt agents and the central Salt server)
<ul>
<li>Subnet - <code>10.15.31.0/24</code></li>
</ul>
</li>
</ul>
<h2 id="deploying-to-linux">Deploying to Linux</h2>
<p>This post covers how to install the <code>salt-minion</code> (the Salt agent that runs on a host), <code>consul</code> and <code>node_exporter</code> on Linux. The Salt central server will also act as a Consul <strong>server</strong> rather than a <strong>client</strong>, so that each other Consul <strong>clients</strong> can register to it. In a production environment, it is advisable to have multiple dedicated Consul servers.</p>
<h3 id="init-systems">Init systems</h3>
<p>Most Linux operating systems now use SystemD for initialization (<a href="https://en.wikipedia.org/wiki/Init">init</a>) and managing services, but there are some exceptions. Below lists the operating systems that are included in this lab, and what init system they use: -</p>
<table>
<thead>
<tr>
<th>OS</th>
<th>init</th>
</tr>
</thead>
<tbody>
<tr>
<td>Alpine Linux</td>
<td><a href="https://en.wikipedia.org/wiki/OpenRC">openrc</a></td>
</tr>
<tr>
<td>Arch Linux</td>
<td><a href="https://en.wikipedia.org/wiki/Systemd">systemd (by default)</a></td>
</tr>
<tr>
<td>CentOS</td>
<td><a href="https://en.wikipedia.org/wiki/Systemd">systemd (by default)</a></td>
</tr>
<tr>
<td>Debian</td>
<td><a href="https://en.wikipedia.org/wiki/Systemd">systemd (by default)</a></td>
</tr>
<tr>
<td>OpenSUSE</td>
<td><a href="https://en.wikipedia.org/wiki/Systemd">systemd (by default)</a></td>
</tr>
<tr>
<td>Ubuntu</td>
<td><a href="https://en.wikipedia.org/wiki/Systemd">systemd (by default)</a></td>
</tr>
<tr>
<td>Void Linux</td>
<td><a href="https://en.wikipedia.org/wiki/Runit">runit</a></td>
</tr>
</tbody>
</table>
<p>The reasons for including Alpine is that it is very useful for smaller/appliance-style applications, and it is very popular as a container base image. I included Void Linux mainly out of curiosity!</p>
<h2 id="installing-the-salt-minion">Installing the Salt Minion</h2>
<p>There are four main methods for installing the Salt Minion (the agent): -</p>
<ul>
<li>Install from the distribution&rsquo;s package repositories
<ul>
<li>This may not be the most up to date version</li>
</ul>
</li>
<li>Add Salt&rsquo;s package repository to your operating system (if available)
<ul>
<li>Latest version, but may not be packaged for your chosen distribution</li>
</ul>
</li>
<li>Installing using PyPi (Python&rsquo;s package/module repository)</li>
<li>Install from source</li>
</ul>
<p>At the time of writing, Salt (version 3000) has issues with <a href="https://github.com/saltstack/salt/issues/55310">Python 3.8</a>. Alpine, Ubuntu and Void all come with  Python 3.8. This does appear to be resolved in Salt version 3001, but it is in RC (Release Candidate) status currently rather than GA (General Availability).</p>
<p>The RC version is available via a separate package repository if you wish to install it. Alternatively, if you clone the <a href="https://github.com/saltstack/salt/">Salt</a> GitHub repository and build it, it will include the latest fixes (including Python 3.8 support).</p>
<p>Salt 3001 is scheduled for generally availability in late June 2020. By the time you read this post, the latest version may already support Python 3.8 (and therefore Alpine, Ubuntu and Void).</p>
<h3 id="installing-from-the-distributions-package-repositories">Installing from the distribution&rsquo;s package repositories</h3>
<p>Below is a list for how to install Salt from each distribution&rsquo;s package repository</p>
<table>
<thead>
<tr>
<th>OS</th>
<th>Command to install</th>
</tr>
</thead>
<tbody>
<tr>
<td>Alpine Linux</td>
<td><code>apk add salt-minion salt-minion-openrc</code></td>
</tr>
<tr>
<td>Arch Linux</td>
<td><code>pacman -S salt</code></td>
</tr>
<tr>
<td>CentOS 7 and below</td>
<td><code>yum install salt-minion</code></td>
</tr>
<tr>
<td>CentOS 8 and above</td>
<td><code>dnf install salt-minion</code></td>
</tr>
<tr>
<td>Debian</td>
<td><code>apt install salt-minion</code></td>
</tr>
<tr>
<td>OpenSUSE</td>
<td><code>zypper install salt-minion</code></td>
</tr>
<tr>
<td>Ubuntu</td>
<td><code>apt install salt-minion</code></td>
</tr>
<tr>
<td>Void</td>
<td><code>xbps-install salt</code></td>
</tr>
</tbody>
</table>
<p>The version in the Ubuntu repositories is the Python 2 version, meaning that it can still run despite Ubuntu&rsquo;s default Python interpreter being Python 3.8. However Python 2 is now end-of-life. You would also be without the latest security updates for Salt too.</p>
<p>At the time of writing (early June 2020), the Alpine packages will install, but will not run (due to Python 3.8). Void&rsquo;s package does not install due to conflicts with <code>libcrypto</code>, <code>libssl</code>, <code>libressl</code> and <code>libtls</code>. For both Alpine and Void, the best options currently are to install from Python&rsquo;s PyPI or build from source.</p>
<h3 id="add-salts-package-repository">Add Salt&rsquo;s package repository</h3>
<p>Salt provides package repositories for the following OSs: -</p>
<ul>
<li>Debian</li>
<li>Red Hat/CentOS</li>
<li>Ubuntu</li>
<li>SUSE</li>
<li>Fedora</li>
<li>Amazon Linux</li>
<li>Raspbian/Raspberry Pi OS</li>
</ul>
<p>These can be found at the <a href="https://repo.saltstack.com/">SaltStack Package Repo</a> page with full instructions adding the repository, and installing the relevant packages.</p>
<p>With Ubuntu, due to the Python 3.8 incompatibility, you can add the <a href="https://docs.saltstack.com/en/latest/topics/releases/releasecandidate.html">SaltStack Release Candidate repository</a>. Please note as it is an RC version, you run it at your own risk.</p>
<h3 id="installing-using-pypi">Installing using PyPI</h3>
<p>PyPI, or the Python Package Index, is effectively the package manager for Python. For those distributions without Salt in their package repositories, and no official SaltStack repository available, you can make use of PyPI. Most, if not all Linux distributions ship with Python support, many now defaulting to Python 3.</p>
<p>If you are familiar with Python, or at least installing packages, you will have likely used the <code>pip</code> tool, which installs packages/modules from PyPI. Instructions for installing <code>pip</code> will depend upon your distribution (<a href="https://packaging.python.org/guides/installing-using-linux-tools/">this</a> covers most common distributions).</p>
<p>After <code>pip</code> is installed, you can then install the Salt minion with the following: -</p>
<pre><code>pip install salt
</code></pre><p>In some distributions, you may need to use <code>pip3</code> rather than <code>pip</code> to ensure that the version installed uses Python 3.</p>
<p>As mentioned previously Python 2 is now end-of-life, so you will want to use the Python 3 version in nearly all cases. The only time to consider using the Python 2 version would be if the Python 3 version has conflicts with other packages on your system. In my lab, this was only necessary for Void Linux, however I believe future versions of Salt (i.e. 3001) may address this anyway.</p>
<h3 id="install-from-source">Install from source</h3>
<p>To install from source, you will need to do the following: -</p>
<pre><code>git clone https://github.com/saltstack/salt
cd salt
sudo python setup.py install --force
</code></pre><p>After this, you will need to add Init scripts (i.e. SystemD units, runit SV files, OpenRC service files), examples of which can be found <a href="https://github.com/saltstack/salt/tree/master/pkg">here</a></p>
<h2 id="configuring-the-minion">Configuring the minion</h2>
<p>To configure the minion, it needs to know what Salt server (the &ldquo;master&rdquo;) to register with. Additionally, I also configure the <code>id</code> (the host&rsquo;s full hostname) and the <code>nodename</code> (the hostname without the full domain suffix): -</p>
<pre><code>master: salt-master.yetiops.lab
id: alpine-01.yetiops.lab
nodename: alpine-01
</code></pre><p>The configuration on all the machines is identical, other than changing the ID and nodename to match the host.</p>
<p>This configuration goes into the <code>/etc/salt/minion</code> file. You can either replace the contents of this file with the above, or append it to the end.</p>
<p>After this, restart and enable your <code>salt-minion</code> service: -</p>
<ul>
<li>SystemD
<ul>
<li>Restart - <code>systemctl restart salt-minion</code></li>
<li>Enable - <code>systemctl enable salt-minion</code></li>
</ul>
</li>
<li>OpenRC (Alpine)
<ul>
<li>Restart - <code>rc-service salt-minion restart</code></li>
<li>Enable - <code>rc-update add salt-minion default</code></li>
</ul>
</li>
<li>Runit (Void)
<ul>
<li>Restart - <code>sv restart salt-minion</code></li>
<li>Enable - <code>ln -s /run/runit/supervise.salt-minion /etc/sv/salt-minion/supervise</code></li>
</ul>
</li>
</ul>
<h3 id="accept-the-minions-on-the-salt-server">Accept the minions on the Salt server</h3>
<p>To accept the minions on the Salt server run <code>salt-key -a 'HOSTNAME*'</code>, replacing <em>HOSTNAME</em> with the agent&rsquo;s hostname  (e.g. <code>ubuntu-01</code>). After this, you should see all of your hosts in <code>salt-key -L</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo salt-key -L

Accepted Keys:
alpine-01.yetiops.lab
arch-01.yetiops.lab
centos-01.yetiops.lab
salt-master.yetiops.lab
suse-01.yetiops.lab
ubuntu-01.yetiops.lab
void-01.yetiops.lab
Denied Keys:
Unaccepted Keys:
Rejected Keys:
</code></pre></div><p>Once this is done, you can build your state files and pillars, ready to deploy Consul and <code>node_exporter</code>.</p>
<h2 id="salt-states">Salt States</h2>
<p>In my lab environment, I have three different groups of state files for Linux. One of them covers all distributions using SystemD. The other two are for Alpine and Void specifically.</p>
<h3 id="consul---systemd">Consul - SystemD</h3>
<h4 id="states">States</h4>
<p>The following Salt state is used to deploy Consul onto a SystemD-based host: -</p>
<p><strong>/srv/salt/states/consul/init.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul_binary</span>:
  <span style="color:#66d9ef">archive.extracted</span>:
    - <span style="color:#66d9ef">name</span>: /usr/local/bin
    - <span style="color:#66d9ef">source</span>: https://releases.hashicorp.com/consul/<span style="color:#ae81ff">1.7.3</span>/consul_1<span style="color:#ae81ff">.7</span>.3_linux_amd64.zip
    - <span style="color:#66d9ef">source_hash</span>: sha256=453814aa5d0c2bc1f8843b7985f2a101976433db3e6c0c81782a3c21dd3f9ac3
    - <span style="color:#66d9ef">enforce_toplevel</span>: <span style="color:#66d9ef">false</span>
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root
    - <span style="color:#66d9ef">if_missing</span>: /usr/local/bin/consul

<span style="color:#66d9ef">consul_user</span>:
  <span style="color:#66d9ef">user.present</span>:
    - <span style="color:#66d9ef">name</span>: consul
    - <span style="color:#66d9ef">fullname</span>: Consul
    - <span style="color:#66d9ef">shell</span>: /bin/<span style="color:#66d9ef">false</span>
    - <span style="color:#66d9ef">home</span>: /etc/consul.d

<span style="color:#66d9ef">consul_group</span>:
  <span style="color:#66d9ef">group.present</span>:
    - <span style="color:#66d9ef">name</span>: consul

<span style="color:#66d9ef">/etc/systemd/system/consul.service</span>:
  <span style="color:#66d9ef">file.managed</span>:
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>] is defined %}
{% if <span style="color:#e6db74">&#39;server&#39;</span> in pillar[<span style="color:#e6db74">&#39;consul&#39;</span>] %}
    - <span style="color:#66d9ef">source</span>: salt://consul/server/files/consul.service.j2
{% else %}
    - <span style="color:#66d9ef">source</span>: salt://consul/client/files/consul.service.j2
{% endif %}
{% endif %}
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0644</span>
    - <span style="color:#66d9ef">template</span>: jinja

<span style="color:#66d9ef">/opt/consul</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/etc/consul.d</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/etc/consul.d/consul.hcl</span>:
  <span style="color:#66d9ef">file.managed</span>:
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>] is defined %}
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;server&#39;</span>] is defined %}
    - <span style="color:#66d9ef">source</span>: salt://consul/server/files/consul.hcl.j2
{% else %}
    - <span style="color:#66d9ef">source</span>: salt://consul/client/files/consul.hcl.j2
{% endif %}
{% endif %}
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0640</span>
    - <span style="color:#66d9ef">template</span>: jinja

<span style="color:#66d9ef">consul_service</span>:
  <span style="color:#66d9ef">service.running</span>:
  - <span style="color:#66d9ef">name</span>: consul
  - <span style="color:#66d9ef">enable</span>: True
  - <span style="color:#66d9ef">reload</span>: True
  - <span style="color:#66d9ef">watch</span>:
    - <span style="color:#66d9ef">file</span>: /etc/consul.d/consul.hcl

{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>] is defined %}
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;prometheus_services&#39;</span>] is defined %}
{% for service in pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;prometheus_services&#39;</span>] %}
/etc/consul.d/{{ <span style="color:#66d9ef">service }}.hcl</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://consul/services/files/{{ service }}.hcl
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0640</span>
    - <span style="color:#66d9ef">template</span>: jinja

consul_reload_{{ <span style="color:#66d9ef">service }}</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: consul reload
    - <span style="color:#66d9ef">watch</span>:
      - <span style="color:#66d9ef">file</span>: /etc/consul.d/{{ service }}.hcl
{% endfor %}
{% endif %}
{% endif %}
</code></pre></div><p>If you are used to Ansible, then this Salt state isn&rsquo;t too dissimilar than the equivalent Ansible playbook. One useful feature of Salt is being able to use Jinja2 looping and conditional syntax to repeat a task (compared to using <code>loop</code>, <code>when</code> and/or <code>with_</code> in Ansible).</p>
<p>Writing the equivalent of the above in Ansible would likely require either duplicate tasks for the Consul <code>server</code> and <code>client</code>, or referencing maps/dictionaries and variables to get the right files. With Salt, you can generate the parameters of a task (and potentially the tasks themselves) conditionally.</p>
<p>To explain each part of this: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul_binary</span>:
  <span style="color:#66d9ef">archive.extracted</span>:
    - <span style="color:#66d9ef">name</span>: /usr/local/bin
    - <span style="color:#66d9ef">source</span>: https://releases.hashicorp.com/consul/<span style="color:#ae81ff">1.7.3</span>/consul_1<span style="color:#ae81ff">.7</span>.3_linux_amd64.zip
    - <span style="color:#66d9ef">source_hash</span>: sha256=453814aa5d0c2bc1f8843b7985f2a101976433db3e6c0c81782a3c21dd3f9ac3
    - <span style="color:#66d9ef">enforce_toplevel</span>: <span style="color:#66d9ef">false</span>
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root
    - <span style="color:#66d9ef">if_missing</span>: /usr/local/bin/consul
</code></pre></div><p>This downloads the Consul package (version 1.7.3) and extracts the contents to <code>/usr/local/bin</code>. We use the <code>source_hash</code> to verify the file matches the hash provided by Hashicorp themselves. We set <code>enforce_toplevel</code> to <code>false</code> so that we don&rsquo;t create a <code>/usr/local/bin/consul_1.7.3_linux_amd64</code> directory. The <code>if_missing</code> says that we only do this if <code>/usr/local/bin/consul</code> doesn&rsquo;t exist (i.e. the Consul binary).</p>
<p>If you need to install a new version of Consul at a later date, you can remove the <code>if_missing</code> part during upgrade. I would advise leaving it in until then so that you are not downloading the Consul binary every time you run your Salt states.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul_user</span>:
  <span style="color:#66d9ef">user.present</span>:
    - <span style="color:#66d9ef">name</span>: consul
    - <span style="color:#66d9ef">fullname</span>: Consul
    - <span style="color:#66d9ef">shell</span>: /bin/<span style="color:#66d9ef">false</span>
    - <span style="color:#66d9ef">home</span>: /etc/consul.d

<span style="color:#66d9ef">consul_group</span>:
  <span style="color:#66d9ef">group.present</span>:
    - <span style="color:#66d9ef">name</span>: consul
</code></pre></div><p>The above tasks ensure a <code>consul</code> user exists (rather than running as <code>root</code> or another user) and a group also called <code>consul</code>. In most cases, creating the <code>consul</code> user is enough to also create the <code>consul</code> group, but I found then in OpenSUSE this is not the case. Given the group should exist anyway, checking for it isn&rsquo;t an issue.</p>
<p>The <code>group.present</code> state also ensures it is created, if it doesn&rsquo;t exist already.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">/etc/systemd/system/consul.service</span>:
  <span style="color:#66d9ef">file.managed</span>:
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>] is defined %}
{% if <span style="color:#e6db74">&#39;server&#39;</span> in pillar[<span style="color:#e6db74">&#39;consul&#39;</span>] %}
    - <span style="color:#66d9ef">source</span>: salt://consul/server/files/consul.service.j2
{% else %}
    - <span style="color:#66d9ef">source</span>: salt://consul/client/files/consul.service.j2
{% endif %}
{% endif %}
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0644</span>
    - <span style="color:#66d9ef">template</span>: jinja
</code></pre></div><p>This state creates a SystemD unit file (<code>/etc/systemd/system/consul.service</code>), with different contents if the machine is a Consul <strong>server</strong> or a <strong>client</strong>.</p>
<p>The <code>salt://</code> path is relative to the base directory of your state files (in my case <code>/srv/salt/states</code>. Therefore the files are in <code>/srv/salt/states/consul/server/files/consul.service.j2</code> and <code>/srv/salt/states/consul/client/files/consul.service.j2</code>.</p>
<p>The contents of both files are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[Unit]
Description=&#34;HashiCorp Consul - A service mesh solution&#34;
Documentation=https://www.consul.io/
Requires=network-online.target
After=network-online.target
ConditionFileNotEmpty=/etc/consul.d/consul.hcl

[Service]
Type=notify
User=consul
Group=consul
ExecStart=/usr/local/bin/consul agent -config-dir /etc/consul.d
ExecReload=/usr/local/bin/consul reload
KillMode=process
Restart=on-failure
TimeoutSec=300s
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
</code></pre></div><p>Both files are identical currently, but it does mean that if you need to add server-specific options you can, without also affecting the clients.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">/opt/consul</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/etc/consul.d</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True
</code></pre></div><p>The above states ensures the referenced directories are created. The <code>/opt/consul</code> directory is where Consul stores its data and running state. The <code>/et/consul.d</code> directory is where Consul sources its configuration.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">/etc/consul.d/consul.hcl</span>:
  <span style="color:#66d9ef">file.managed</span>:
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>] is defined %}
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;server&#39;</span>] is defined %}
    - <span style="color:#66d9ef">source</span>: salt://consul/server/files/consul.hcl.j2
{% else %}
    - <span style="color:#66d9ef">source</span>: salt://consul/client/files/consul.hcl.j2
{% endif %}
{% endif %}
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0640</span>
    - <span style="color:#66d9ef">template</span>: jinja
</code></pre></div><p>The above state adds <code>/etc/consul.d/consul.hcl</code> to the machine, with the configuration dependent upon whether it is the <strong>server</strong> or the <strong>client</strong>. The two files look like the below: -</p>
<p><strong>salt://consul/server/files/consul.hcl.j2</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl">{
{<span style="color:#960050;background-color:#1e0010">%-</span> <span style="color:#66d9ef">for</span> <span style="color:#66d9ef">ip</span> <span style="color:#66d9ef">in</span> <span style="color:#66d9ef">grains</span>[<span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">ipv4</span><span style="color:#960050;background-color:#1e0010">&#39;</span>] <span style="color:#960050;background-color:#1e0010">%</span>}
{<span style="color:#960050;background-color:#1e0010">%-</span> <span style="color:#66d9ef">if</span> <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#ae81ff">10</span>.<span style="color:#ae81ff">15</span>.<span style="color:#ae81ff">31</span><span style="color:#960050;background-color:#1e0010">&#39;</span> <span style="color:#66d9ef">in</span> <span style="color:#66d9ef">ip</span> <span style="color:#960050;background-color:#1e0010">%</span>}
  <span style="color:#e6db74">&#34;advertise_addr&#34;: &#34;{{ ip }}&#34;</span>,
  <span style="color:#e6db74">&#34;bind_addr&#34;: &#34;{{ ip }}&#34;</span>,
{<span style="color:#960050;background-color:#1e0010">%-</span> <span style="color:#66d9ef">endif</span> <span style="color:#960050;background-color:#1e0010">%</span>}
{<span style="color:#960050;background-color:#1e0010">%-</span> <span style="color:#66d9ef">endfor</span> <span style="color:#960050;background-color:#1e0010">%</span>}
  <span style="color:#e6db74">&#34;bootstrap_expect&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#ae81ff">1</span>,
  <span style="color:#e6db74">&#34;client_addr&#34;: &#34;0.0.0.0&#34;</span>,
  <span style="color:#e6db74">&#34;data_dir&#34;: &#34;/opt/consul&#34;</span>,
  <span style="color:#e6db74">&#34;datacenter&#34;: &#34;{{ pillar[&#39;consul&#39;][&#39;dc&#39;] }}&#34;</span>,
  <span style="color:#e6db74">&#34;encrypt&#34;: &#34;{{ pillar[&#39;consul&#39;][&#39;enc_key&#39;] }}&#34;</span>,
  <span style="color:#e6db74">&#34;node_name&#34;: &#34;{{ grains[&#39;nodename&#39;] }}&#34;</span>,
  <span style="color:#e6db74">&#34;retry_join&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> [
{<span style="color:#960050;background-color:#1e0010">%-</span> <span style="color:#66d9ef">for</span> <span style="color:#66d9ef">server</span> <span style="color:#66d9ef">in</span> <span style="color:#66d9ef">pillar</span>[<span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">consul</span><span style="color:#960050;background-color:#1e0010">&#39;</span>][<span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">servers</span><span style="color:#960050;background-color:#1e0010">&#39;</span>] <span style="color:#960050;background-color:#1e0010">%</span>}
    <span style="color:#e6db74">&#34;{{ server }}&#34;</span>,
{<span style="color:#960050;background-color:#1e0010">%-</span> <span style="color:#66d9ef">endfor</span> <span style="color:#960050;background-color:#1e0010">%</span>}
  ],
  <span style="color:#e6db74">&#34;server&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">true</span>,
  <span style="color:#e6db74">&#34;autopilot&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> {
    <span style="color:#e6db74">&#34;cleanup_dead_servers&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">true</span>,
    <span style="color:#e6db74">&#34;last_contact_threshold&#34;: &#34;200ms&#34;</span>,
    <span style="color:#e6db74">&#34;max_trailing_logs&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#ae81ff">250</span>,
    <span style="color:#e6db74">&#34;server_stabilization_time&#34;: &#34;10s&#34;</span>,
  },
  <span style="color:#e6db74">&#34;ui&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">true</span>
}
</code></pre></div><p>The first part of this use Salt <strong>grains</strong>, which are detail about the hosts the Salt agents are running on. If you are familiar with Ansible, you would know these as <strong>facts</strong>. Consul tries to autodiscover the IP address to listen on and bind to, but sometimes it can pick the wrong one (it could pick a public facing interface rather than a private one for example). In this case, we go through the IPs configured on the hosts, and use whichever starts with <code>10.15.31</code>. Our management/private range is <code>10.15.31.0/24</code>, so all hosts should have an IP in this range.</p>
<p>The <code>bootstrap_expect</code> parameter is important. If you set this to <code>1</code>, you can run a single <strong>server</strong> Consul cluster (with multiple clients). In production you should never run with a single node. Data loss and state can be lost if your single server encounters problems. It is recommended to run <code>3</code> Consul <strong>servers</strong> or more for <a href="https://en.wikipedia.org/wiki/Quorum">quorum</a>. This is because if you start with two nodes, and there is a network partition or other failure, you have no way of knowing which node has the correct state. If however you have 3 (or more), then the &ldquo;correctness&rdquo; can be determined based upon 2 (or more) of the nodes agreeing (a majority vote).</p>
<p>The <code>datacenter</code> is your Consul cluster. This doesn&rsquo;t dictate the physical location, instead being a logical separation between clusters. You could call your <code>datacenter</code> everything from <code>equinix-ld5</code>, <code>preprod</code>, <code>pod4rack2</code>, or <code>meshuggah</code>, so long as the separation is logical to yourself.</p>
<p>The <code>encrypt</code> field uses an encryption key generated by Consul. You can create these by running <code>consul keygen</code>. This command does not replace the existing Consul key of your running cluster, instead just generating a key in the correct format for any Consul cluster.</p>
<p>The <code>retry_join</code> section dictates what server(s) to form a cluster with when Consul comes up.</p>
<p>The most important part is the <code>server: true</code> field. This dictates that this host(s) will become Consul <strong>servers</strong>. We also enable the <code>ui</code>, so that you can use the Consul web interface to view the state of your cluster.</p>
<p><strong>salt://consul/client/files/consul.hcl.j2</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl">{
{<span style="color:#960050;background-color:#1e0010">%-</span> <span style="color:#66d9ef">for</span> <span style="color:#66d9ef">ip</span> <span style="color:#66d9ef">in</span> <span style="color:#66d9ef">grains</span>[<span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">ipv4</span><span style="color:#960050;background-color:#1e0010">&#39;</span>] <span style="color:#960050;background-color:#1e0010">%</span>}
{<span style="color:#960050;background-color:#1e0010">%-</span> <span style="color:#66d9ef">if</span> <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#ae81ff">10</span>.<span style="color:#ae81ff">15</span>.<span style="color:#ae81ff">31</span><span style="color:#960050;background-color:#1e0010">&#39;</span> <span style="color:#66d9ef">in</span> <span style="color:#66d9ef">ip</span> <span style="color:#960050;background-color:#1e0010">%</span>}
  <span style="color:#e6db74">&#34;advertise_addr&#34;: &#34;{{ ip }}&#34;</span>,
  <span style="color:#e6db74">&#34;bind_addr&#34;: &#34;{{ ip }}&#34;</span>,
{<span style="color:#960050;background-color:#1e0010">%-</span> <span style="color:#66d9ef">endif</span> <span style="color:#960050;background-color:#1e0010">%</span>}
{<span style="color:#960050;background-color:#1e0010">%-</span> <span style="color:#66d9ef">endfor</span> <span style="color:#960050;background-color:#1e0010">%</span>}
  <span style="color:#e6db74">&#34;data_dir&#34;: &#34;{{ pillar[&#39;consul&#39;][&#39;data_dir&#39;] }}&#34;</span>,
  <span style="color:#e6db74">&#34;datacenter&#34;: &#34;{{ pillar[&#39;consul&#39;][&#39;dc&#39;] }}&#34;</span>,
  <span style="color:#e6db74">&#34;encrypt&#34;: &#34;{{ pillar[&#39;consul&#39;][&#39;enc_key&#39;] }}&#34;</span>,
  <span style="color:#e6db74">&#34;node_name&#34;: &#34;{{ grains[&#39;nodename&#39;] }}&#34;</span>,
  <span style="color:#e6db74">&#34;retry_join&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> [
{<span style="color:#960050;background-color:#1e0010">%-</span> <span style="color:#66d9ef">for</span> <span style="color:#66d9ef">server</span> <span style="color:#66d9ef">in</span> <span style="color:#66d9ef">pillar</span>[<span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">consul</span><span style="color:#960050;background-color:#1e0010">&#39;</span>][<span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">servers</span><span style="color:#960050;background-color:#1e0010">&#39;</span>] <span style="color:#960050;background-color:#1e0010">%</span>}
    <span style="color:#e6db74">&#34;{{ server }}&#34;</span>,
{<span style="color:#960050;background-color:#1e0010">%-</span> <span style="color:#66d9ef">endfor</span> <span style="color:#960050;background-color:#1e0010">%</span>}
  ],
  <span style="color:#e6db74">&#34;server&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">false</span>,
}
</code></pre></div><p>The client configuration is similar the <strong>server</strong> configuration, with some parts removed (<code>bootstrap_expect</code>, <code>ui</code> and the <code>autopilot</code> section). We also set <code>&quot;server&quot;: false</code> to ensure this node doesn&rsquo;t become a Consul <strong>server</strong>.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul_service</span>:
  <span style="color:#66d9ef">service.running</span>:
  - <span style="color:#66d9ef">name</span>: consul
  - <span style="color:#66d9ef">enable</span>: True
  - <span style="color:#66d9ef">reload</span>: True
  - <span style="color:#66d9ef">watch</span>:
    - <span style="color:#66d9ef">file</span>: /etc/consul.d/consul.hcl
</code></pre></div><p>The above task makes sure that the <code>consul</code> service is running and enabled, and also triggers a reload of the service. This only happens if there is a change in  the <code>/etc/consul.d/consul.hcl</code> file.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>] is defined %}
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;prometheus_services&#39;</span>] is defined %}
{% for service in pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;prometheus_services&#39;</span>] %}
/etc/consul.d/{{ <span style="color:#66d9ef">service }}.hcl</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://consul/services/files/{{ service }}.hcl
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0640</span>
    - <span style="color:#66d9ef">template</span>: jinja

consul_reload_{{ <span style="color:#66d9ef">service }}</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: consul reload
    - <span style="color:#66d9ef">watch</span>:
      - <span style="color:#66d9ef">file</span>: /etc/consul.d/{{ service }}.hcl
{% endfor %}
{% endif %}
{% endif %}
</code></pre></div><p>This section goes through a list of services. For every service defined, it places the corresponding <code>$SERVICE.hcl</code> file into the <code>/etc/consul.d/</code> directory. It also triggers a <code>consul reload</code> after, trigger Consul to reload its configuration (which picks up any configuration files in the <code>/etc/consul.d</code> directory). This reload is only triggered if the contents of the file <code>/etc/consul.d/$SERVICE.hcl</code> have changed (using the <code>watch</code> directive)</p>
<p>For example, if one of our services was <code>node_exporter</code>, we would look for <code>salt://consul/services/files/node_exporter.hcl</code>. We would place this file on the machine (as <code>/etc/consul.d/node_exporter.hcl</code>), and then reload Consul. The contents of the <code>node_exporter.hcl</code> file are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl">{<span style="color:#e6db74">&#34;service&#34;</span><span style="color:#960050;background-color:#1e0010">:</span>
  {<span style="color:#e6db74">&#34;name&#34;: &#34;node_exporter&#34;</span>,
   <span style="color:#e6db74">&#34;tags&#34;: [&#34;node_exporter&#34;, &#34;prometheus&#34;</span>],
   <span style="color:#e6db74">&#34;port&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#ae81ff">9100</span>
  }
}
</code></pre></div><p>For further information on the contents of this file, see my previous <a href="/posts/prometheus-consul-node_exporter/">post</a> on using Consul to discover services with Prometheus. The tags are used so that Prometheus can discover services specific to it.</p>
<p>Consul is not just for Prometheus, and can be used by many different applications for service discovery. Ensuring services are tagged correctly allows us means we are only monitoring the services that should be monitored, or that expose Prometheus compatible endpoints.</p>
<p>This state is applied as such: -</p>
<p><strong>/srv/salt/states/top.sls</strong></p>
<pre><code>base:
  'G@init:systemd and G@kernel:Linux':
    - match: compound
    - consul
</code></pre><p>If you supply just the name <code>consul</code> as your state, this will tell Salt to the look in the <code>/srv/salt/states/consul/</code> directory for a file named <code>init.sls</code>. If the filename is different (e.g. <code>void.sls</code>) you would need to use <code>consul.void</code> instead.</p>
<p>Usually you would match hosts in your <code>top.sls</code> file with something like <code>'*.yetiops.lab':</code> or <code>'arch-01*':</code>. These match against the nodenames or IDs for hosts (and their agents) registered to the Salt server.</p>
<p>Instead, we are using the <code>G@</code> option to match against <strong>grains</strong>. In this case, we are saying that if the host is a Linux host (i.e. running the Linux kernel) and has the <code>init</code> system of <code>systemd</code>, then run the specified states against it.</p>
<p>If you are only matching against one <strong>grain</strong> (i.e. only kernel, or only the init system) you could use <code>match: grain</code>. The <code>match: compound</code> option allows you match multiple grains, or you can match against grains and the hostname, or hostnames and pillars, and much more (full list <a href="https://docs.saltstack.com/en/latest/topics/targeting/compound.html">here</a>).</p>
<p>Because we only want to apply this state against Linux hosts running SystemD, we match multiple grains. You can test what hosts these grains apply to using: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># Linux and running SystemD</span>
$ salt -C <span style="color:#e6db74">&#39;G@init:systemd and G@kernel:Linux&#39;</span> test.ping
ubuntu-01.yetiops.lab:
    True
arch-01.yetiops.lab:
    True
salt-master.yetiops.lab:
    True
centos-01.yetiops.lab:
    True
suse-01.yetiops.lab:
    True

<span style="color:#75715e"># Linux, any init system</span>
$ salt -C <span style="color:#e6db74">&#39;G@kernel:Linux&#39;</span> test.ping
salt-master.yetiops.lab:
    True
ubuntu-01.yetiops.lab:
    True
arch-01.yetiops.lab:
    True
suse-01.yetiops.lab:
    True
centos-01.yetiops.lab:
    True
void-01.yetiops.lab:
    True
alpine-01.yetiops.lab:
    True
</code></pre></div><p>The first command matches only our SystemD machines, whereas the second also includes Alpine and Void (both of which do not run SystemD).</p>
<h4 id="pillars">Pillars</h4>
<p>The <code>pillars</code> (i.e. the host/group specific variables) are defined as such: -</p>
<p><strong>consul.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul</span>:
  <span style="color:#66d9ef">data_dir</span>: /opt/consul
  <span style="color:#66d9ef">prometheus_services</span>:
  - node_exporter
</code></pre></div><p><strong>consul-dc.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul</span>:
  <span style="color:#66d9ef">dc</span>: yetiops
  <span style="color:#66d9ef">enc_key</span>: <span style="color:#75715e">###CONSUL_KEY### </span>
  <span style="color:#66d9ef">servers</span>:
  - salt-master.yetiops.lab
</code></pre></div><p><strong>consul-server.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul</span>:
  <span style="color:#66d9ef">server</span>: <span style="color:#66d9ef">true</span>
</code></pre></div><p>These are all in the <code>/srv/salt/pillars/consul</code> directory. It is worth noting that if you have two sets of pillars that reference the same base key (i.e. all of these start with <code>consul</code>) the variables are merged (rather than one taking set of variables taking precedence over another). This means that you can refer to all of them in your state files and templates using <code>consul.$VARIABLE</code>.</p>
<p>These pillars are applied to the nodes as per below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  <span style="color:#66d9ef">&#39;*&#39;</span>:
    - consul.consul-dc

  <span style="color:#66d9ef">&#39;G@kernel:Linux&#39;</span>:
    - <span style="color:#66d9ef">match</span>: compound
    - consul.consul

  <span style="color:#66d9ef">&#39;salt-master*&#39;</span>:
    - consul.consul-server
</code></pre></div><p>All hosts receive the <code>consul-dc</code> pillar, as the variables defined here are common across every host we will configure in this lab.</p>
<p>The <code>consul.consul</code> pillar (i.e. <code>/srv/salt/pillars/consul/consul.sls</code>) is applied to all of our Linux hosts, as they all have the same data directory and the same <code>prometheus_services</code>. Currently there is only one <strong>grain</strong> to match against, so we could have used <code>match: grain</code>. However in later posts in this series, we will match against multiple grains (requiring the compound match).</p>
<p>Finally, we set the <code>salt-master*</code> as our Consul server, which changes which SystemD files and the Consul configuration are deployed to it (as seen in the <strong>states</strong> section above).</p>
<p>To view the pillars that a node has, you can run the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ salt <span style="color:#e6db74">&#39;ubuntu-01*&#39;</span> pillar.items
ubuntu-01.yetiops.lab:
    ----------
    consul:
        ----------
        data_dir:
            /opt/consul
        dc:
            yetiops
        enc_key:
            <span style="color:#75715e">###CONSUL_ENCRYPTION_KEY###</span> 
        prometheus_services:
            - node_exporter
        servers:
            - salt-master.yetiops.lab
</code></pre></div><p>Alternatively, if you just want to see a single pillar, you can use: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ salt <span style="color:#e6db74">&#39;salt-master*&#39;</span> pillar.item consul:server
salt-master.yetiops.lab:
    ----------
    consul:server:
        True

$ salt <span style="color:#e6db74">&#39;ubuntu*&#39;</span> pillar.item consul:server
ubuntu-01.yetiops.lab:
    ----------
    consul:server:
</code></pre></div><h4 id="apply-the-states">Apply the states</h4>
<p>To apply all of the states, you can run either: -</p>
<ul>
<li><code>salt '*' state.highstate</code> from the Salt server (to configure every machine and every state)</li>
<li><code>salt 'suse*' state.highstate</code> from the Salt server (to configure all machines with a name beginning with <code>suse*</code>, applying all states)</li>
<li><code>salt 'suse*' state.apply consul</code> from the Salt server (to configure all machines with a name beginning with <code>suse*</code>, applying only the <code>consul</code> state)</li>
<li><code>salt-call state.highstate</code> from a machine running the Salt agent (to configure just one machine with all states)</li>
<li><code>salt-call state.apply consul</code> from a machine running the Salt agent (to configure just one machine with only the <code>consul</code> state)</li>
</ul>
<p>You can also use the <code>salt -C</code> option to apply based upon grains, pillars or other types of matches. For example, to apply to all machines running a Debian-derived operating system, you could run <code>salt -C 'G@os_family:Debian' state.highstate</code> which would cover Debian, Ubuntu, Elementary OS, and others that use Debian as their base.</p>
<h3 id="consul---alpine-linux">Consul - Alpine Linux</h3>
<p>As mentioned already, Alpine does not use SystemD, instead using OpenRC. This is because SystemD has quite a few associated projects, and Alpine tries to keep its base installation as lean as possible. Also, as Alpine is often a base for containers (which typically do not have their own init system), it makes something like SystemD surplus to requirements.</p>
<h4 id="states-1">States</h4>
<p>The state file for Alpine looks like the below: -</p>
<p><strong>/srv/salt/states/consul/alpine.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul_package</span>:
  <span style="color:#66d9ef">pkg.installed</span>:
  - <span style="color:#66d9ef">pkgs</span>:
    - consul
    - consul-openrc

<span style="color:#66d9ef">/etc/init.d/consul</span>:
  <span style="color:#66d9ef">file.managed</span>:
  - <span style="color:#66d9ef">source</span>: salt://consul/client/files/consul-rc
  - <span style="color:#66d9ef">user</span>: root
  - <span style="color:#66d9ef">group</span>: root
  - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>

<span style="color:#66d9ef">/etc/consul/server.json</span>:
  file.absent

<span style="color:#66d9ef">/opt/consul</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/etc/consul.d</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/etc/consul.d/consul.hcl</span>:
  <span style="color:#66d9ef">file.managed</span>:
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>] is defined %}
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;server&#39;</span>] is defined %}
    - <span style="color:#66d9ef">source</span>: salt://consul/server/files/consul.hcl.j2
{% else %}
    - <span style="color:#66d9ef">source</span>: salt://consul/client/files/consul.hcl.j2
{% endif %}
{% endif %}
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0640</span>
    - <span style="color:#66d9ef">template</span>: jinja

<span style="color:#66d9ef">consul_service</span>:
  <span style="color:#66d9ef">service.running</span>:
  - <span style="color:#66d9ef">name</span>: consul
  - <span style="color:#66d9ef">enable</span>: True
  - <span style="color:#66d9ef">reload</span>: True
  - <span style="color:#66d9ef">watch</span>:
    - <span style="color:#66d9ef">file</span>: /etc/consul.d/consul.hcl

{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>] is defined %}
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;prometheus_services&#39;</span>] is defined %}
{% for service in pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;prometheus_services&#39;</span>] %}
/etc/consul.d/{{ <span style="color:#66d9ef">service }}.hcl</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://consul/services/files/{{ service }}.hcl
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0640</span>
    - <span style="color:#66d9ef">template</span>: jinja

consul_reload_{{ <span style="color:#66d9ef">service }}</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: consul reload
    - <span style="color:#66d9ef">watch</span>:
      - <span style="color:#66d9ef">file</span>: /etc/consul.d/{{ service }}.hcl
{% endfor %}
{% endif %}
{% endif %}
</code></pre></div><p>Most of the states in this file are identical to the SystemD version. Below we shall go over the differences: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul_package</span>:
  <span style="color:#66d9ef">pkg.installed</span>:
  - <span style="color:#66d9ef">pkgs</span>:
    - consul
    - consul-openrc
</code></pre></div><p>Rather than retrieving the Consul binary directly from Hashicorp themselves, we install it from the Alpine package repository. Alpine&rsquo;s packages are usually quite close to the latest version, so it makes sense to use them. We also install the <code>consul-openrc</code> package, which puts the correct <code>openrc</code> hooks in place, and installs a basic <code>openrc</code> file for Consul (although we are going to override it).</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">/etc/init.d/consul</span>:
  <span style="color:#66d9ef">file.managed</span>:
  - <span style="color:#66d9ef">source</span>: salt://consul/client/files/consul-rc
  - <span style="color:#66d9ef">user</span>: root
  - <span style="color:#66d9ef">group</span>: root
  - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
</code></pre></div><p>In this, we take replace the existing <code>/etc/init.d/consul</code> file with our own. The contents of this file are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/sbin/openrc-run
</span><span style="color:#75715e"></span>description<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;A tool for service discovery, monitoring and configuration&#34;</span>
description_checkconfig<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Verify configuration files&#34;</span>
description_healthcheck<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Check health status&#34;</span>
description_reload<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Reload configuration&#34;</span>

extra_commands<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;checkconfig&#34;</span>
extra_started_commands<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;healthcheck reload&#34;</span>

command<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/usr/sbin/</span>$RC_SVCNAME<span style="color:#e6db74">&#34;</span>
command_args<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span>$consul_opts<span style="color:#e6db74"> -config-dir=/etc/consul.d&#34;</span>
command_user<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span>$RC_SVCNAME<span style="color:#e6db74">:</span>$RC_SVCNAME<span style="color:#e6db74">&#34;</span>

supervisor<span style="color:#f92672">=</span>supervise-daemon
pidfile<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/run/</span>$RC_SVCNAME<span style="color:#e6db74">.pid&#34;</span>
output_log<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/var/log/</span>$RC_SVCNAME<span style="color:#e6db74">.log&#34;</span>
error_log<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/var/log/</span>$RC_SVCNAME<span style="color:#e6db74">.log&#34;</span>
umask<span style="color:#f92672">=</span><span style="color:#ae81ff">027</span>
respawn_max<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>
respawn_delay<span style="color:#f92672">=</span><span style="color:#ae81ff">10</span>
healthcheck_timer<span style="color:#f92672">=</span><span style="color:#ae81ff">60</span>

depend<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
	need net
	after firewall
<span style="color:#f92672">}</span>

checkconfig<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
	ebegin <span style="color:#e6db74">&#34;Checking /etc/consul.d&#34;</span>
	consul validate /etc/consul.d
	eend $?
<span style="color:#f92672">}</span>

start_pre<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
	checkconfig
	checkpath -f -m <span style="color:#ae81ff">0640</span> -o <span style="color:#e6db74">&#34;</span>$command_user<span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&#34;</span>$output_log<span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&#34;</span>$error_log<span style="color:#e6db74">&#34;</span>
<span style="color:#f92672">}</span>

healthcheck<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
	$command info &gt; /dev/null 2&gt;&amp;<span style="color:#ae81ff">1</span>
<span style="color:#f92672">}</span>

reload<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
	start_pre <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>		<span style="color:#f92672">&amp;&amp;</span> ebegin <span style="color:#e6db74">&#34;Reloading </span>$RC_SVCNAME<span style="color:#e6db74"> configuration&#34;</span> <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>		<span style="color:#f92672">&amp;&amp;</span> supervise-daemon <span style="color:#e6db74">&#34;</span>$RC_SVCNAME<span style="color:#e6db74">&#34;</span> --signal HUP --pidfile <span style="color:#e6db74">&#34;</span>$pidfile<span style="color:#e6db74">&#34;</span>
	eend $?
<span style="color:#f92672">}</span>
</code></pre></div><p>This file makes sure that we are pointing to the correct configuration directory (the original points to <code>/etc/consul</code> and <code>/etc/consul.d</code>).</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">/etc/consul/server.json</span>:
  file.absent
</code></pre></div><p>This state makes sure that <code>/etc/consul/server.json</code> is removed. With this, Alpine would try and start Consul as a server. While we have stopped the reference to <code>/etc/consul</code> in the OpenRC file, this makes sure that if this is ever overridden by a new version of <code>consul-openrc</code>, this node doesn&rsquo;t try and become a Consul server.</p>
<p>After this, all the other states are identical. We do not need to specify the users and groups, as they are automatically created when installing the <code>consul</code> package. You could add the states in that check for the existence of these users and groups, but they will also come back as present anyway.</p>
<p>Our <code>states/top.sls</code> file looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">base</span>:
  <span style="color:#66d9ef">&#39;G@init:systemd and G@kernel:Linux&#39;</span>:
    - <span style="color:#66d9ef">match</span>: compound
    - consul

  <span style="color:#66d9ef">&#39;os:Alpine&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.alpine
</code></pre></div><p>As you can see, we are matching the <code>os</code> grain, and applying the <code>consul.alpine</code> state if the host is running Alpine Linux. We can verify this with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ salt <span style="color:#e6db74">&#39;salt*&#39;</span> grains.item os
salt-master.yetiops.lab:
    ----------
    os:
        Debian

$ salt <span style="color:#e6db74">&#39;alpine*&#39;</span> grains.item os
alpine-01.yetiops.lab:
    ----------
    os:
        Alpine
</code></pre></div><h4 id="pillars-1">Pillars</h4>
<p>The pillars are identical to those used in the SystemD states, as we do not do any matches against the Init system or the name of the OS, only that they are a Linux host.</p>
<h4 id="apply-the-states-1">Apply the states</h4>
<p>You apply the states the same as before, using either matches against the name, or you could match against grains, e.g. <code>salt -C 'G@os:Alpine' state.highstate</code></p>
<h3 id="consul---void-linux">Consul - Void Linux</h3>
<p>Void Linux uses an Init system called <strong>runit</strong>, which has links back to <a href="https://en.wikipedia.org/wiki/Daemontools">daemontools</a> (created by <a href="https://cr.yp.to/djb.html">Daniel J. Bernstein</a>). Void is a popular option among those who do not favour SystemD as an Init system.</p>
<h4 id="states-2">States</h4>
<p>The state file for Void looks like the below: -</p>
<p><strong>/srv/salt/states/consul/void.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul_package</span>:
  <span style="color:#66d9ef">pkg.installed</span>:
  - <span style="color:#66d9ef">pkgs</span>:
    - consul

<span style="color:#66d9ef">consul_user</span>:
  <span style="color:#66d9ef">user.present</span>:
    - <span style="color:#66d9ef">name</span>: consul
    - <span style="color:#66d9ef">fullname</span>: Consul
    - <span style="color:#66d9ef">shell</span>: /bin/<span style="color:#66d9ef">false</span>
    - <span style="color:#66d9ef">home</span>: /etc/consul.d

<span style="color:#66d9ef">consul_group</span>:
  <span style="color:#66d9ef">group.present</span>:
    - <span style="color:#66d9ef">name</span>: consul

<span style="color:#66d9ef">/opt/consul</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/etc/consul.d</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/run/runit/supervise.consul</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0700</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/etc/sv/consul</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0700</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/etc/sv/consul/run</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://consul/client/files/consul-runit
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0755</span>

<span style="color:#66d9ef">/etc/sv/consul/supervise</span>:
  <span style="color:#66d9ef">file.symlink</span>:
   - <span style="color:#66d9ef">target</span>: /run/runit/supervise.consul
   - <span style="color:#66d9ef">user</span>: root
   - <span style="color:#66d9ef">group</span>: root
   - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0777</span>

<span style="color:#66d9ef">/var/service/consul</span>:
  <span style="color:#66d9ef">file.symlink</span>:
   - <span style="color:#66d9ef">target</span>: /etc/sv/consul
   - <span style="color:#66d9ef">user</span>: root
   - <span style="color:#66d9ef">group</span>: root
   - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0777</span>

<span style="color:#66d9ef">/etc/consul.d/consul.hcl</span>:
  <span style="color:#66d9ef">file.managed</span>:
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>] is defined %}
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;server&#39;</span>] is defined %}
    - <span style="color:#66d9ef">source</span>: salt://consul/server/files/consul.hcl.j2
{% else %}
    - <span style="color:#66d9ef">source</span>: salt://consul/client/files/consul.hcl.j2
{% endif %}
{% endif %}
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0640</span>
    - <span style="color:#66d9ef">template</span>: jinja

<span style="color:#66d9ef">consul_service</span>:
  <span style="color:#66d9ef">service.running</span>:
  - <span style="color:#66d9ef">name</span>: consul
  - <span style="color:#66d9ef">enable</span>: True
  - <span style="color:#66d9ef">watch</span>:
    - <span style="color:#66d9ef">file</span>: /etc/consul.d/consul.hcl

{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>] is defined %}
{% if pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;prometheus_services&#39;</span>] is defined %}
{% for service in pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;prometheus_services&#39;</span>] %}
/etc/consul.d/{{ <span style="color:#66d9ef">service }}.hcl</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://consul/services/files/{{ service }}.hcl
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0640</span>
    - <span style="color:#66d9ef">template</span>: jinja

consul_reload_{{ <span style="color:#66d9ef">service }}</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: consul reload
    - <span style="color:#66d9ef">watch</span>:
      - <span style="color:#66d9ef">file</span>: /etc/consul.d/{{ service }}.hcl
{% endfor %}
{% endif %}
{% endif %}
</code></pre></div><p>The first few states in this are similar to Alpine and SystemD (installing the <code>consul</code> package, creating users and directories). After this though, most of the differences <strong>runit</strong>-specific. Rather than using something like SystemD unit files, Sysvinit service files or OpenRC files, <strong>runit</strong> uses directories, shell scripts and symbolic links, along with a <strong>supervise</strong> method to ensure a service is always running.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">/run/runit/supervise.consul</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0700</span>
    - <span style="color:#66d9ef">makedirs</span>: True
</code></pre></div><p>First, we create the <code>/run/runit/supervise.consul</code> directory. When running, Consul uses this directory to show the state of the process: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#f92672">[</span>root@void-01 supervise.consul<span style="color:#f92672">]</span><span style="color:#75715e"># pwd</span>
/run/runit/supervise.consul

<span style="color:#f92672">[</span>root@void-01 supervise.consul<span style="color:#f92672">]</span><span style="color:#75715e"># ls</span>
control  lock  ok  pid  stat  status
</code></pre></div><p>This includes lock files, status, the pid file and more.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">/etc/sv/consul</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0700</span>
    - <span style="color:#66d9ef">makedirs</span>: True
</code></pre></div><p>We create the Consul service directory in the <code>/etc/sv</code> directory, which is where all available (but not necessarily <strong>running</strong>) services reside.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">/etc/sv/consul/run</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://consul/client/files/consul-runit
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0755</span>
</code></pre></div><p>The above adds the file <code>/srv/salt/states/consul/client/files/consul-runit/</code> as <code>/etc/sv/consul/run</code>. The contents of this file are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/bin/sh
</span><span style="color:#75715e"></span>exec consul agent -config-dir /etc/consul.d
</code></pre></div><p>This is little more than a shell script that <strong>runit</strong> supervises (i.e. makes sure it is running).</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">/var/service/consul</span>:
  <span style="color:#66d9ef">file.symlink</span>:
   - <span style="color:#66d9ef">target</span>: /etc/sv/consul
   - <span style="color:#66d9ef">user</span>: root
   - <span style="color:#66d9ef">group</span>: root
   - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0777</span>
</code></pre></div><p>The above creates a symbolic link from <code>/var/service/consul</code> to <code>/etc/sv/consul</code>. This is what tells the Void machine that we want to run this service.</p>
<p>The only other difference we have is that Salt doesn&rsquo;t seem to be able to <strong>reload</strong> the service when on Void, so we remove the <code>reload: True</code> line from the <code>service.running</code> state.</p>
<p>After this, everything else is the same as Alpine and SystemD-based systems. We can verify the service is running using: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sv status consul
run: consul: <span style="color:#f92672">(</span>pid 709<span style="color:#f92672">)</span> 1718s
</code></pre></div><p>Our <code>states/top.sls</code> file looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">base</span>:
  <span style="color:#66d9ef">&#39;G@init:systemd and G@kernel:Linux&#39;</span>:
    - <span style="color:#66d9ef">match</span>: compound
    - consul

  <span style="color:#66d9ef">&#39;os:Alpine&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.alpine

  <span style="color:#66d9ef">&#39;os:Void&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.void
</code></pre></div><p>Again, we&rsquo;re matching the <code>os</code> grain. We can verify this with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ salt <span style="color:#e6db74">&#39;void*&#39;</span> grains.item os
void-01.yetiops.lab:
    ----------
    os:
        Void

$ salt <span style="color:#e6db74">&#39;suse*&#39;</span> grains.item os
suse-01.yetiops.lab:
    ----------
    os:
        SUSE
</code></pre></div><h3 id="consul---verification">Consul - Verification</h3>
<p>After this, we can verify Consul is running on all nodes using the <code>consul</code> command: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ consul members
Node                         Address           Status  Type    Build  Protocol  DC       Segment
salt-master                  10.15.31.5:8301   alive   server  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;all&gt;
alpine-01                    10.15.31.27:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
arch-01                      10.15.31.26:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
centos-01.yetiops.lab        10.15.31.24:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
suse-01                      10.15.31.22:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
ubuntu-01                    10.15.31.33:8301  alive   client  1.7.3  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;
void-01                      10.15.31.31:8301  alive   client  1.7.2  <span style="color:#ae81ff">2</span>         yetiops  &lt;default&gt;

$ consul catalog nodes -service node_exporter
Node                         ID        Address      DC
alpine-01                    e59eb6fc  10.15.31.27  yetiops
arch-01                      97c67201  10.15.31.26  yetiops
centos-01.yetiops.lab        78ac8405  10.15.31.24  yetiops
salt-master                  344fb6f2  10.15.31.5   yetiops
suse-01                      d2fdd88a  10.15.31.22  yetiops
ubuntu-01                    4544c7ff  10.15.31.33  yetiops
void-01                      e99c7e3c  10.15.31.31  yetiops
</code></pre></div><h3 id="node-exporter---systemd">Node Exporter - SystemD</h3>
<p>While we have Consul running, and it has the <code>node_exporter</code> Consul service for all our nodes, we still need to install the Prometheus <code>node_exporter</code> as well.</p>
<h4 id="states-3">States</h4>
<p>The following Salt state is used to deploy the Prometheus Node Exporter onto a SystemD-based host: -</p>
<p><strong>/srv/salt/states/exporters/node_exporter/systemd.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">{% if not salt[<span style="color:#e6db74">&#39;file.file_exists&#39;</span>](<span style="color:#e6db74">&#39;/usr/local/bin/node_exporter&#39;</span>) %}

<span style="color:#66d9ef">retrieve_node_exporter</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: wget -O /tmp/node_exporter.tar.gz https://github.com/prometheus/node_exporter/releases/download/v0<span style="color:#ae81ff">.18.1</span>/node_exporter<span style="color:#ae81ff">-0.18.1</span>.linux-amd64.tar.gz

<span style="color:#66d9ef">extract_node_exporter</span>:
  <span style="color:#66d9ef">archive.extracted</span>:
    - <span style="color:#66d9ef">name</span>: /tmp
    - <span style="color:#66d9ef">enforce_toplevel</span>: <span style="color:#66d9ef">false</span>
    - <span style="color:#66d9ef">source</span>: /tmp/node_exporter.tar.gz
    - <span style="color:#66d9ef">archive_format</span>: tar
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root

<span style="color:#66d9ef">move_node_exporter</span>:
  <span style="color:#66d9ef">file.rename</span>:
    - <span style="color:#66d9ef">name</span>: /usr/local/bin/node_exporter
    - <span style="color:#66d9ef">source</span>: /tmp/node_exporter<span style="color:#ae81ff">-0.18.1</span>.linux-amd64/node_exporter

<span style="color:#66d9ef">delete_node_exporter_dir</span>:
  <span style="color:#66d9ef">file.absent</span>:
    - <span style="color:#66d9ef">name</span>: /tmp/node_exporter<span style="color:#ae81ff">-0.18.1</span>.linux-amd64

<span style="color:#66d9ef">delete_node_exporter_files</span>:
  <span style="color:#66d9ef">file.absent</span>:
    - <span style="color:#66d9ef">name</span>: /tmp/node_exporter.tar.gz
{% endif %}

<span style="color:#66d9ef">node_exporter_user</span>:
  <span style="color:#66d9ef">user.present</span>:
    - <span style="color:#66d9ef">name</span>: node_exporter
    - <span style="color:#66d9ef">fullname</span>: Node Exporter
    - <span style="color:#66d9ef">shell</span>: /bin/<span style="color:#66d9ef">false</span>

<span style="color:#66d9ef">node_exporter_group</span>:
  <span style="color:#66d9ef">group.present</span>:
    - <span style="color:#66d9ef">name</span>: node_exporter

<span style="color:#66d9ef">/opt/prometheus/exporters/dist/textfile</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: node_exporter
    - <span style="color:#66d9ef">group</span>: node_exporter
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/etc/systemd/system/node_exporter.service</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://exporters/node_exporter/files/node_exporter.service.j2
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0644</span>
    - <span style="color:#66d9ef">template</span>: jinja

{% if <span style="color:#e6db74">&#39;CentOS&#39;</span> in grains[<span style="color:#e6db74">&#39;os&#39;</span>] %}
<span style="color:#66d9ef">node_exporter_selinux_fcontext</span>:
  <span style="color:#66d9ef">selinux.fcontext_policy_present</span>:
    - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#39;/usr/local/bin/node_exporter&#39;</span>
    - <span style="color:#66d9ef">sel_type</span>: bin_t

<span style="color:#66d9ef">node_exporter_selinux_fcontext_applied</span>:
  <span style="color:#66d9ef">selinux.fcontext_policy_applied</span>:
    - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#39;/usr/local/bin/node_exporter&#39;</span>
{% endif %}


<span style="color:#66d9ef">node_exporter_service_reload</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: systemctl daemon-reload
    - <span style="color:#66d9ef">watch</span>:
      - <span style="color:#66d9ef">file</span>: /etc/systemd/system/node_exporter.service

<span style="color:#66d9ef">node_exporter_service</span>:
  <span style="color:#66d9ef">service.running</span>:
    - <span style="color:#66d9ef">name</span>: node_exporter
    - <span style="color:#66d9ef">enable</span>: True

{% if (<span style="color:#e6db74">&#39;SUSE&#39;</span> in grains[<span style="color:#e6db74">&#39;os&#39;</span>] or <span style="color:#e6db74">&#39;CentOS&#39;</span> in grains[<span style="color:#e6db74">&#39;os&#39;</span>]) %}
<span style="color:#66d9ef">node_exporter_firewalld_service</span>:
  <span style="color:#66d9ef">firewalld.service</span>:
    - <span style="color:#66d9ef">name</span>: node_exporter
    - <span style="color:#66d9ef">ports</span>:
      - <span style="color:#ae81ff">9100</span>/tcp

<span style="color:#66d9ef">node_exporter_firewalld_rule</span>:
  <span style="color:#66d9ef">firewalld.present</span>:
    - <span style="color:#66d9ef">name</span>: public
    - <span style="color:#66d9ef">services</span>:
      - node_exporter
{% endif %}
</code></pre></div><p>There are a lot of similarities to the Consul state, but some key differences.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">{% if not salt[<span style="color:#e6db74">&#39;file.file_exists&#39;</span>](<span style="color:#e6db74">&#39;/usr/local/bin/node_exporter&#39;</span>) %}

<span style="color:#66d9ef">retrieve_node_exporter</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: wget -O /tmp/node_exporter.tar.gz https://github.com/prometheus/node_exporter/releases/download/v0<span style="color:#ae81ff">.18.1</span>/node_exporter<span style="color:#ae81ff">-0.18.1</span>.linux-amd64.tar.gz

<span style="color:#66d9ef">extract_node_exporter</span>:
  <span style="color:#66d9ef">archive.extracted</span>:
    - <span style="color:#66d9ef">name</span>: /tmp
    - <span style="color:#66d9ef">enforce_toplevel</span>: <span style="color:#66d9ef">false</span>
    - <span style="color:#66d9ef">source</span>: /tmp/node_exporter.tar.gz
    - <span style="color:#66d9ef">archive_format</span>: tar
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root

<span style="color:#66d9ef">move_node_exporter</span>:
  <span style="color:#66d9ef">file.rename</span>:
    - <span style="color:#66d9ef">name</span>: /usr/local/bin/node_exporter
    - <span style="color:#66d9ef">source</span>: /tmp/node_exporter<span style="color:#ae81ff">-0.18.1</span>.linux-amd64/node_exporter

<span style="color:#66d9ef">delete_node_exporter_dir</span>:
  <span style="color:#66d9ef">file.absent</span>:
    - <span style="color:#66d9ef">name</span>: /tmp/node_exporter<span style="color:#ae81ff">-0.18.1</span>.linux-amd64

<span style="color:#66d9ef">delete_node_exporter_files</span>:
  <span style="color:#66d9ef">file.absent</span>:
    - <span style="color:#66d9ef">name</span>: /tmp/node_exporter.tar.gz
{% endif %}
</code></pre></div><p>This state checks to see if the <code>node_exporter</code> binary exists in <code>/usr/local/bin</code>. If it does, all of this is skipped. If it isn&rsquo;t, we first retrieve the file via <code>wget</code> and place it into our <code>/tmp</code> directory on the host. Then, we extract it. After that, we move the file <code>/tmp/node_exporter-0.18.1.linux-amd64/node_exporter</code> (i.e. the binary inside the Tarball we just downloaded) to <code>/usr/local/bin</code>. Finally, we remove the directory created by extracting the tarball, and the tarball itself.</p>
<p>We could have done all of this through the <code>archive.extracted</code> task instead, but we still need to manipulate the contents of the archive so there is very little benefit here. Also, if you use something like a proxy, <code>archive.extracted</code> cannot be told to use it, so this method will work in those cases.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">/opt/prometheus/exporters/dist/textfile</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: node_exporter
    - <span style="color:#66d9ef">group</span>: node_exporter
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True
</code></pre></div><p>After we create the <code>node_exporter</code> user and group, we also create the <code>/opt/prometheus/exporters/dist/textfile</code> directory. This is so that we can make use of the Node Exporter&rsquo;s <a href="https://github.com/prometheus/node_exporter#textfile-collector">textfile collector</a> if we need to.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">/etc/systemd/system/node_exporter.service</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://exporters/node_exporter/files/node_exporter.service.j2
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0644</span>
    - <span style="color:#66d9ef">template</span>: jinja
</code></pre></div><p>The above is similar to what we configure for Consul&rsquo;s SystemD unit file. The contents of the file are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[Unit]
Description=Node Exporter
After=network.target

[Service]
User=node_exporter
Group=node_exporter
Type=simple
ExecStart=/usr/local/bin/node_exporter --collector.systemd --collector.textfile --collector.textfile.directory=/opt/prometheus/exporters/dist/textfile

[Install]
WantedBy=multi-user.target
</code></pre></div><p>The only major parts to point out are that we enable the SystemD collector (disabled by default) and enable the Textfile collector, pointing at the directory we created previously.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">{% if <span style="color:#e6db74">&#39;CentOS&#39;</span> in grains[<span style="color:#e6db74">&#39;os&#39;</span>] %}
<span style="color:#66d9ef">node_exporter_selinux_fcontext</span>:
  <span style="color:#66d9ef">selinux.fcontext_policy_present</span>:
    - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#39;/usr/local/bin/node_exporter&#39;</span>
    - <span style="color:#66d9ef">sel_type</span>: bin_t

<span style="color:#66d9ef">node_exporter_selinux_fcontext_applied</span>:
  <span style="color:#66d9ef">selinux.fcontext_policy_applied</span>:
    - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#39;/usr/local/bin/node_exporter&#39;</span>
{% endif %}
</code></pre></div><p>On CentOS (and RHEL), if you try to run the Node Exporter it will fail, as SELinux is denying it from running. You will see messages like this in the logs for <code>node_exporter</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">centos-01 systemd<span style="color:#f92672">[</span>1<span style="color:#f92672">]</span>: Started /usr/bin/systemctl start node_exporter.service.
centos-01 systemd<span style="color:#f92672">[</span>2101<span style="color:#f92672">]</span>: node_exporter.service: Failed to execute command: Permission denied
centos-01 systemd<span style="color:#f92672">[</span>2101<span style="color:#f92672">]</span>: node_exporter.service: Failed at step EXEC spawning /usr/local/bin/node_exporter: Permission denied
centos-01 systemd<span style="color:#f92672">[</span>1<span style="color:#f92672">]</span>: node_exporter.service: Main process exited, code<span style="color:#f92672">=</span>exited, status<span style="color:#f92672">=</span>203/EXEC
centos-01 systemd<span style="color:#f92672">[</span>1<span style="color:#f92672">]</span>: node_exporter.service: Failed with result <span style="color:#e6db74">&#39;exit-code&#39;</span>.
centos-01 systemd<span style="color:#f92672">[</span>1<span style="color:#f92672">]</span>: Started /usr/bin/systemctl enable node_exporter.service.
centos-01 setroubleshoot<span style="color:#f92672">[</span>2108<span style="color:#f92672">]</span>: SELinux is preventing /usr/lib/systemd/systemd from execute access on the file node_exporter. For complete SELinux messages run: sealert -l 580a42ae-d512-4280-8cca-282716ed59b2
</code></pre></div><p>This is because <code>/usr/local/bin/node_exporter</code> does not have the right SELinux context. You need to set a policy for <code>node_exporter</code> to type <code>bin_t</code> and then apply it. After this, it will run without issues.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">node_exporter_service_reload</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: systemctl daemon-reload
    - <span style="color:#66d9ef">watch</span>:
      - <span style="color:#66d9ef">file</span>: /etc/systemd/system/node_exporter.service
</code></pre></div><p>Salt by itself should attempt <code>daemon-reload</code> when a SystemD unit file changes (i.e. picking up the latest version of the unit), however we run it as a separate task as well. We may want to add additional collectors later, and so we will need a <code>daemon-reload</code> to pick up the latest changes. By the same token, we didn&rsquo;t do this for Consul as it is very unlikely we&rsquo;ll need to change the SystemD unit file often (if at all).</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">{% if (<span style="color:#e6db74">&#39;SUSE&#39;</span> in grains[<span style="color:#e6db74">&#39;os&#39;</span>] or <span style="color:#e6db74">&#39;CentOS&#39;</span> in grains[<span style="color:#e6db74">&#39;os&#39;</span>]) %}
<span style="color:#66d9ef">node_exporter_firewalld_service</span>:
  <span style="color:#66d9ef">firewalld.service</span>:
    - <span style="color:#66d9ef">name</span>: node_exporter
    - <span style="color:#66d9ef">ports</span>:
      - <span style="color:#ae81ff">9100</span>/tcp

<span style="color:#66d9ef">node_exporter_firewalld_rule</span>:
  <span style="color:#66d9ef">firewalld.present</span>:
    - <span style="color:#66d9ef">name</span>: public
    - <span style="color:#66d9ef">services</span>:
      - node_exporter
{% endif %}
</code></pre></div><p>The final section configures the host firewall on CentOS and OpenSUSE. This is because by default, CentOS and OpenSUSE come with <code>firewalld</code> (and may be enabled in your environment), so we need to make sure it is allowed through the firewall on the host. If you use different zones, or tell <code>node_exporter</code> to run on a different port, you would need to change this state. Otherwise, this should cover all you need to allow connections to the <code>node_exporter</code> daemon running on the host.</p>
<p>Our <code>states/top.sls</code> file looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">base</span>:
  <span style="color:#66d9ef">&#39;G@init:systemd and G@kernel:Linux&#39;</span>:
    - <span style="color:#66d9ef">match</span>: compound
    - consul
    - exporters.node_exporter.systemd

  <span style="color:#66d9ef">&#39;os:Alpine&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.alpine

  <span style="color:#66d9ef">&#39;os:Void&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.void
</code></pre></div><h4 id="pillars-2">Pillars</h4>
<p>There are no pillars for the <code>node_exporter</code> state. If you want to specify different ports or listen addresses, you could customize it using pillars, but I am running it mostly at it&rsquo;s defaults.</p>
<h3 id="node-exporter---alpine">Node Exporter - Alpine</h3>
<h4 id="states-4">States</h4>
<p>The state file for Alpine looks like the below: -</p>
<p><strong>/srv/salt/states/exporters/node_exporters/alpine.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">{% if not salt[<span style="color:#e6db74">&#39;file.file_exists&#39;</span>](<span style="color:#e6db74">&#39;/usr/local/bin/node_exporter&#39;</span>) %}

<span style="color:#66d9ef">retrieve_node_exporter</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: wget -O /tmp/node_exporter.tar.gz https://github.com/prometheus/node_exporter/releases/download/v0<span style="color:#ae81ff">.18.1</span>/node_exporter<span style="color:#ae81ff">-0.18.1</span>.linux-amd64.tar.gz

<span style="color:#66d9ef">extract_node_exporter</span>:
  <span style="color:#66d9ef">archive.extracted</span>:
    - <span style="color:#66d9ef">name</span>: /tmp
    - <span style="color:#66d9ef">enforce_toplevel</span>: <span style="color:#66d9ef">false</span>
    - <span style="color:#66d9ef">source</span>: /tmp/node_exporter.tar.gz
    - <span style="color:#66d9ef">archive_format</span>: tar
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root

<span style="color:#66d9ef">move_node_exporter</span>:
  <span style="color:#66d9ef">file.rename</span>:
    - <span style="color:#66d9ef">name</span>: /usr/local/bin/node_exporter
    - <span style="color:#66d9ef">source</span>: /tmp/node_exporter<span style="color:#ae81ff">-0.18.1</span>.linux-amd64/node_exporter

<span style="color:#66d9ef">delete_node_exporter_dir</span>:
  <span style="color:#66d9ef">file.absent</span>:
    - <span style="color:#66d9ef">name</span>: /tmp/node_exporter<span style="color:#ae81ff">-0.18.1</span>.linux-amd64

<span style="color:#66d9ef">delete_node_exporter_files</span>:
  <span style="color:#66d9ef">file.absent</span>:
    - <span style="color:#66d9ef">name</span>: /tmp/node_exporter.tar.gz
{% endif %}

<span style="color:#66d9ef">node_exporter_user</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: adduser -H -D -s /bin/<span style="color:#66d9ef">false</span> node_exporter

<span style="color:#66d9ef">/opt/prometheus/exporters/dist/textfile</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: node_exporter
    - <span style="color:#66d9ef">group</span>: node_exporter
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/var/log/node_exporter</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: node_exporter
    - <span style="color:#66d9ef">group</span>: node_exporter
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/etc/init.d/node_exporter</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://exporters/node_exporter/files/node_exporter.openrc
    - <span style="color:#66d9ef">dest</span>: /etc/init.d/node_exporter
    - <span style="color:#66d9ef">mode</span>: <span style="color:#e6db74">&#39;0755&#39;</span>
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root

<span style="color:#66d9ef">node_exporter_service</span>:
  <span style="color:#66d9ef">service.running</span>:
    - <span style="color:#66d9ef">name</span>: node_exporter
    - <span style="color:#66d9ef">enable</span>: <span style="color:#66d9ef">true</span>
    - <span style="color:#66d9ef">state</span>: restarted
    - <span style="color:#66d9ef">watch</span>:
      - <span style="color:#66d9ef">file</span>: /etc/init.d/node_exporter
</code></pre></div><p>Most of this state is similar to the SystemD state, with a few notable differences.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">node_exporter_user</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: adduser -H -D -s /bin/<span style="color:#66d9ef">false</span> node_exporter
</code></pre></div><p>Unfortunately the <code>user.present</code> state does not work correctly on Alpine. This is because <code>useradd</code> does not exist on Alpine, which is what <code>user.present</code> is using in the background. A GitHub issue exists <a href="https://github.com/saltstack/salt/issues/53121">here</a> for this. Instead, we use the <code>cmd.run</code> module (essentially running a shell command on the host) to add the user instead.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">/var/log/node_exporter</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: node_exporter
    - <span style="color:#66d9ef">group</span>: node_exporter
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True
</code></pre></div><p>Unlike SystemD (which places logs into binary journal files), Alpine uses plain log files in <code>/var/log</code>. We create a <code>node_exporter</code> directory, rather than just having a <code>/var/log/node_exporter.log</code> file.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">/etc/init.d/node_exporter</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://exporters/node_exporter/files/node_exporter.openrc
    - <span style="color:#66d9ef">dest</span>: /etc/init.d/node_exporter
    - <span style="color:#66d9ef">mode</span>: <span style="color:#e6db74">&#39;0755&#39;</span>
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root
</code></pre></div><p>Again, Alpine uses OpenRC for its Init system. We need to use an OpenRC service file for <code>node_exporter</code>. The contents of this file are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">#!/sbin/openrc-run
</span><span style="color:#75715e"></span>
description<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Prometheus machine metrics exporter&#34;</span>
pidfile<span style="color:#f92672">=</span><span style="color:#e6db74">${</span>pidfile<span style="color:#66d9ef">:-</span><span style="color:#e6db74">&#34;/run/</span><span style="color:#e6db74">${</span>RC_SVCNAME<span style="color:#e6db74">}</span><span style="color:#e6db74">.pid&#34;</span><span style="color:#e6db74">}</span>
user<span style="color:#f92672">=</span><span style="color:#e6db74">${</span>user<span style="color:#66d9ef">:-</span><span style="color:#e6db74">${</span>RC_SVCNAME<span style="color:#e6db74">}}</span>
group<span style="color:#f92672">=</span><span style="color:#e6db74">${</span>group<span style="color:#66d9ef">:-</span><span style="color:#e6db74">${</span>RC_SVCNAME<span style="color:#e6db74">}}</span>

command<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/usr/local/bin/node_exporter&#34;</span>
command_args<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;--collector.textfile --collector.textfile.directory=/opt/prometheus/exporters/dist/textfile&#34;</span>
command_background<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;true&#34;</span>
start_stop_daemon_args<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;--user </span><span style="color:#e6db74">${</span>user<span style="color:#e6db74">}</span><span style="color:#e6db74"> --group </span><span style="color:#e6db74">${</span>group<span style="color:#e6db74">}</span><span style="color:#e6db74"> \
</span><span style="color:#e6db74">	--stdout /var/log/node_exporter/</span><span style="color:#e6db74">${</span>RC_SVCNAME<span style="color:#e6db74">}</span><span style="color:#e6db74">.log \
</span><span style="color:#e6db74">	--stderr /var/log/node_exporter/</span><span style="color:#e6db74">${</span>RC_SVCNAME<span style="color:#e6db74">}</span><span style="color:#e6db74">.log&#34;</span>

depend<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
	after net
<span style="color:#f92672">}</span>
</code></pre></div><p>This tells the Node Exporter to run, and we enable the textfile collector as well. We don&rsquo;t enable the SystemD collector (due to no SystemD!).</p>
<p>Besides that, everything else is relatively similar to the SystemD state.</p>
<p>Our <code>states/top.sls</code> file looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">base</span>:
  <span style="color:#66d9ef">&#39;G@init:systemd and G@kernel:Linux&#39;</span>:
    - <span style="color:#66d9ef">match</span>: compound
    - consul
    - exporters.node_exporter.systemd

  <span style="color:#66d9ef">&#39;os:Alpine&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.alpine
    - exporters.node_exporter.alpine

  <span style="color:#66d9ef">&#39;os:Void&#39;</span>:
    - <span style="color:#66d9ef">match</span>: grain
    - consul.void
</code></pre></div><h4 id="pillars-3">Pillars</h4>
<p>Again, there are no pillars for the <code>node_exporter</code>, so we do not need to apply any.</p>
<h3 id="node-exporter---void">Node Exporter - Void</h3>
<h4 id="states-5">States</h4>
<p>The state file for Void looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">node_exporter_package</span>:
  <span style="color:#66d9ef">pkg.installed</span>:
    - <span style="color:#66d9ef">pkgs</span>:
      - node_exporter

<span style="color:#66d9ef">node_exporter_service</span>:
  <span style="color:#66d9ef">service.running</span>:
    - <span style="color:#66d9ef">name</span>: node_exporter
    - <span style="color:#66d9ef">enable</span>: <span style="color:#66d9ef">true</span>
</code></pre></div><p>Compared to the previous states, this is much simpler. This is because there is already a working <code>node_exporter</code> package in the Void Linux repositories, so we just install it and enable it.</p>
<h4 id="pillars-4">Pillars</h4>
<p>Again, there are no pillars for the <code>node_exporter</code>, so we do not need to apply any.</p>
<h3 id="node-exporter---verification">Node Exporter - Verification</h3>
<p>We can verify whether the Node Exporter is available on the hosts by using something like <code>curl</code> or a web browser to check that they are working: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># salt-master (Debian)</span>
$ curl 10.15.31.5:9100/metrics  | grep -Ei <span style="color:#e6db74">&#34;^node_uname_info&#34;</span>
node_uname_info<span style="color:#f92672">{</span>domainname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(none)&#34;</span>,machine<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;x86_64&#34;</span>,nodename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;salt-master&#34;</span>,release<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;4.19.0-9-amd64&#34;</span>,sysname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Linux&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;#1 SMP Debian 4.19.118-2 (2020-04-29)&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>

<span style="color:#75715e"># alpine-01 (Alpine Linux)</span>
$ curl 10.15.31.27:9100/metrics  | grep -Ei <span style="color:#e6db74">&#34;^node_uname_info&#34;</span>
node_uname_info<span style="color:#f92672">{</span>domainname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(none)&#34;</span>,machine<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;x86_64&#34;</span>,nodename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;alpine-01&#34;</span>,release<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;5.4.43-0-lts&#34;</span>,sysname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Linux&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;#1-Alpine SMP Thu, 28 May 2020 09:59:32 UTC&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>

<span style="color:#75715e"># arch-01 (Arch Linux)</span>
$ curl 10.15.31.26:9100/metrics  | grep -Ei <span style="color:#e6db74">&#34;^node_uname_info&#34;</span>
node_uname_info<span style="color:#f92672">{</span>domainname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(none)&#34;</span>,machine<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;x86_64&#34;</span>,nodename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;arch-01&#34;</span>,release<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;5.6.14-arch1-1&#34;</span>,sysname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Linux&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;#1 SMP PREEMPT Wed, 20 May 2020 20:43:19 +0000&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>

<span style="color:#75715e"># centos-01 (CentOS)</span>
$ curl 10.15.31.24:9100/metrics  | grep -Ei <span style="color:#e6db74">&#34;^node_uname_info&#34;</span>
node_uname_info<span style="color:#f92672">{</span>domainname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(none)&#34;</span>,machine<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;x86_64&#34;</span>,nodename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;centos-01.yetiops.lab&#34;</span>,release<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;4.18.0-147.8.1.el8_1.x86_64&#34;</span>,sysname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Linux&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;#1 SMP Thu Apr 9 13:49:54 UTC 2020&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>

<span style="color:#75715e"># suse-01 (OpenSUSE Tumbleweed)</span>
$ curl 10.15.31.22:9100/metrics  | grep -Ei <span style="color:#e6db74">&#34;^node_uname_info&#34;</span>
node_uname_info<span style="color:#f92672">{</span>domainname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&#34;</span>,machine<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;x86_64&#34;</span>,nodename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;suse-01&#34;</span>,release<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;5.6.12-1-default&#34;</span>,sysname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Linux&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;#1 SMP Tue May 12 17:44:12 UTC 2020 (9bff61b)&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>

<span style="color:#75715e"># ubuntu-01 (Ubuntu)</span>
$ curl 10.15.31.33:9100/metrics  | grep -Ei <span style="color:#e6db74">&#34;^node_uname_info&#34;</span>
node_uname_info<span style="color:#f92672">{</span>domainname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(none)&#34;</span>,machine<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;x86_64&#34;</span>,nodename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;ubuntu-01&#34;</span>,release<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;5.4.0-33-generic&#34;</span>,sysname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Linux&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;#37-Ubuntu SMP Thu May 21 12:53:59 UTC 2020&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>

<span style="color:#75715e"># void-01 (Void Linux)</span>
$ curl 10.15.31.31:9100/metrics  | grep -Ei <span style="color:#e6db74">&#34;^node_uname_info&#34;</span>
node_uname_info<span style="color:#f92672">{</span>domainname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(none)&#34;</span>,machine<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;x86_64&#34;</span>,nodename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;void-01&#34;</span>,release<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;5.3.9_1&#34;</span>,sysname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Linux&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;#1 SMP PREEMPT Wed Nov 6 15:01:52 UTC 2019&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>
</code></pre></div><h2 id="configuring-prometheus">Configuring Prometheus</h2>
<p>Configuring Prometheus to use Consul for service discovery is covered in this <a href="/posts/prometheus-consul-node_exporter/">post</a>. You can choose many methods for generating the Prometheus configuration, including config management (like Ansible or SaltStack), embedding it within a Docker container, or using something like the <a href="https://github.com/coreos/prometheus-operator">Kubernetes Prometheus Operator</a>.</p>
<h3 id="using-salt">Using Salt</h3>
<p>Below is a sample state for how to configure it using SaltStack, using Docker to run Prometheus within a container. In this, we generate the configuration on the host (rather than inside the container). This allows us to make changes to the configuration with Salt, without needing to restart the container, or run the Salt agent from within the container itself.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">docker_service</span>:
  <span style="color:#66d9ef">service.running</span>:
    - <span style="color:#66d9ef">name</span>: docker
    - <span style="color:#66d9ef">enable</span>: <span style="color:#66d9ef">true</span>
    - <span style="color:#66d9ef">reload</span>: <span style="color:#66d9ef">true</span>

<span style="color:#66d9ef">/etc/prometheus</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: nobody
    - <span style="color:#66d9ef">group</span>: nogroup
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/etc/alertmanager</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: nobody
    - <span style="color:#66d9ef">group</span>: nogroup
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/etc/prometheus/alerts</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: nobody
    - <span style="color:#66d9ef">group</span>: nogroup
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/etc/prometheus/rules</span>:
  <span style="color:#66d9ef">file.directory</span>:
    - <span style="color:#66d9ef">user</span>: nobody
    - <span style="color:#66d9ef">group</span>: nogroup
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">755</span>
    - <span style="color:#66d9ef">makedirs</span>: True

<span style="color:#66d9ef">/etc/prometheus/prometheus.yml</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://prometheus/files/prometheus.yml.j2
    - <span style="color:#66d9ef">user</span>: nobody
    - <span style="color:#66d9ef">group</span>: nogroup
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0640</span>
    - <span style="color:#66d9ef">template</span>: jinja

<span style="color:#66d9ef">prometheus_docker_volume</span>:
  <span style="color:#66d9ef">docker_volume.present</span>:
    - <span style="color:#66d9ef">name</span>: prometheus
    - <span style="color:#66d9ef">driver</span>: local

<span style="color:#66d9ef">prometheus-container</span>:
  <span style="color:#66d9ef">docker_container.running</span>:
    - <span style="color:#66d9ef">name</span>: prometheus
    - <span style="color:#66d9ef">image</span>: prom/prometheus:latest
    - <span style="color:#66d9ef">restart_policy</span>: always
    - <span style="color:#66d9ef">port_bindings</span>:
      - <span style="color:#ae81ff">9090</span>:<span style="color:#ae81ff">9090</span>
    - <span style="color:#66d9ef">binds</span>:
      - prometheus:/prometheus:rw
      - /etc/prometheus:/etc/prometheus:rw
    - <span style="color:#66d9ef">entrypoint</span>:
      - prometheus
      - --config.file=/etc/prometheus/prometheus.yml
      - --storage.tsdb.path=/prometheus
      - --web.console.libraries=/usr/share/prometheus/console_libraries
      - --web.console.templates=/usr/share/prometheus/consoles
      - --web.enable-lifecycle

<span style="color:#66d9ef">prometheus_config_reload</span>:
  <span style="color:#66d9ef">cmd.run</span>:
    - <span style="color:#66d9ef">name</span>: curl -X POST http://localhost:<span style="color:#ae81ff">9090</span>/-/reload
    - <span style="color:#66d9ef">onchanges</span>:
      - <span style="color:#66d9ef">file</span>: /etc/prometheus/prometheus.yml
</code></pre></div><p>For those familiar with Docker, you might wonder why we are using bind mounts for the containers. Currently, SaltStack&rsquo;s <code>docker_container</code> seems to mount volumes as the root user, at which point the containers cannot write to them. The Prometheus process inside the container runs as the user <code>nobody</code> and the group <code>nogroup</code>, which cannot write to a file or folder owned by root.</p>
<p>When using bind mounts, the permissions from the host operating system are inherited by the container. This means we can set the the permissions of the directories on the host to be <code>nobody:nogroup</code>, and the Prometheus container can write to them.</p>
<p>We also use the flag <code>web.enable-lifecycle</code> so that we can trigger a reload of the configuration from the API. Without this, we would need to send a <code>SIGHUP</code> to the process inside the container or restart the process/container in some way.</p>
<p>The contents of <code>/srv/salt/states/prometheus/files/prometheus.yml.j2</code> file are: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">global:
  scrape_interval:     15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
    - targets:
      - 'localhost:9090'
{%- if pillar['prometheus'] is defined %}
{%- if pillar['prometheus']['consul_clusters'] is defined %}
{%- for cluster in pillar['prometheus']['consul_clusters'] %}
  - job_name: '{{ cluster[&quot;job&quot;] }}'
    consul_sd_configs:
{%- for server in cluster['servers'] %}
      - server: '{{ server }}'
{%- endfor %}
    relabel_configs:
{%- for tag in cluster['tags'] %}
      - source_labels: [__meta_consul_tags]
        regex: .*,{{ tag['name'] }},.*
        action: keep
{%- endfor %}
      - source_labels: [__meta_consul_service]
        target_label: job
{%- endfor %}
{%- endif %}
{%- endif %}
</code></pre><p>To explain what is happening here, we are saying that: -</p>
<ul>
<li>If a <strong>prometheus</strong> pillar is defined then&hellip;</li>
<li>If a <strong>consul_clusters</strong> variable is inside the <strong>prometheus</strong> pillar then&hellip;</li>
<li>For each Consul Cluster defined in <strong>consul_clusters</strong>
<ul>
<li>Create a job named <code>{{ cluster[&quot;job&quot;] }}</code></li>
<li>Add all of the defined Consul servers from that cluster to the job</li>
<li>Keep all targets that match the defined tag(s)</li>
<li>Replace the contents of the label <code>job</code> with the contents of the <code>__meta_consul_service</code> metadata</li>
</ul>
</li>
</ul>
<p>So if we had the below pillars: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">prometheus</span>:
  <span style="color:#66d9ef">consul_clusters</span>:
  - <span style="color:#66d9ef">job</span>: consul-dc
    <span style="color:#66d9ef">servers</span>:
    - consul<span style="color:#ae81ff">-01.</span>yetiops.lab:<span style="color:#ae81ff">8500</span>
    - consul<span style="color:#ae81ff">-02.</span>yetiops.lab:<span style="color:#ae81ff">8500</span>
    - consul<span style="color:#ae81ff">-03.</span>yetiops.lab:<span style="color:#ae81ff">8500</span>
    <span style="color:#66d9ef">tags</span>:
    - <span style="color:#66d9ef">name</span>: prometheus
</code></pre></div><p>This would generate: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">global</span>:
  <span style="color:#66d9ef">scrape_interval</span>:     15s
  <span style="color:#66d9ef">evaluation_interval</span>: 15s

<span style="color:#66d9ef">scrape_configs</span>:
  - <span style="color:#66d9ef">job_name</span>: <span style="color:#e6db74">&#39;prometheus&#39;</span>
    <span style="color:#66d9ef">static_configs</span>:
    - <span style="color:#66d9ef">targets</span>:
      - <span style="color:#e6db74">&#39;localhost:9090&#39;</span>
  - <span style="color:#66d9ef">job_name</span>: <span style="color:#e6db74">&#39;prometheus&#39;</span>
    <span style="color:#66d9ef">consul_sd_configs</span>:
      - <span style="color:#66d9ef">server</span>: <span style="color:#e6db74">&#39;consul-01.yetiops.lab:8500&#39;</span>
      - <span style="color:#66d9ef">server</span>: <span style="color:#e6db74">&#39;consul-02.yetiops.lab:8500&#39;</span>
      - <span style="color:#66d9ef">server</span>: <span style="color:#e6db74">&#39;consul-03.yetiops.lab:8500&#39;</span>
    <span style="color:#66d9ef">relabel_configs</span>:
      - <span style="color:#66d9ef">source_labels</span>: [__meta_consul_tags]
        <span style="color:#66d9ef">regex</span>: .<span style="color:#75715e">*,prometheus,.*</span>
        <span style="color:#66d9ef">action</span>: keep
      - <span style="color:#66d9ef">source_labels</span>: [__meta_consul_service]
        <span style="color:#66d9ef">target_label</span>: job
</code></pre></div><p>You could expand the template to include different metrics paths or different <code>scrape_interval</code> times (i.e. how long between each attempt to pull metrics from a target), as seen below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">%- for cluster in pillar['prometheus']['consul_clusters'] %}
  - job_name: '{{ cluster[&quot;job&quot;] }}'
{%- if cluster['scrape_interval'] is defined %}
    scrape_interval: {{ cluster['scrape_interval'] }}
{%- endif %}
{%- if cluster['metrics_path'] is defined %}
    metrics_path: {{ cluster['metrics_path'] }}
{%- endif %}
</code></pre><p>Some exporters take longer to retrieve metrics than others (<code>snmp_exporter</code> is a good example, depending on the host(s) it targets), so you may want to increase the interval of how often it scrapes metrics.</p>
<p>Other exporters use a different <strong>path</strong> than <code>/metrics</code>. For example, this <a href="https://github.com/znerol/prometheus-pve-exporter">Proxmox PVE exporter</a> uses the path <code>/pve</code> (i.e. <code>http://192.168.0.1:9221/pve</code> rather than <code>http://192.168.0.1:9221/metrics</code>) so the Prometheus job would need to know to try a different path when attempting to gather metrics.</p>
<p>You can then separate out these jobs using different Consul tags (e.g. <code>prometheus-long</code> or <code>prometheus-proxmox</code>) so that the additional jobs target only the hosts and exporters with those tags, and are ignored by the other jobs.</p>
<h2 id="prometheus-targets">Prometheus Targets</h2>
<p>After we have configured all of the above, we should be able to see all of the hosts in Prometheus: -</p>
<p><img src="/img/prometheus/prom-consul-sd.png" alt="Prometheus Consul Service Discovery"></p>
<p><img src="/img/prometheus/prom-consul-targets.png" alt="Prometheus Consul Targets"></p>
<p>There they are!</p>
<p>We can then use Grafana (with Prometheus as a data source) to create dashboards from Prometheus metrics. Alternatively, you can make use of existing Grafana dashboards created by other Grafana users. A good starting point for using with Prometheus&rsquo;s Node Exporter is <a href="https://grafana.com/grafana/dashboards/1860">this one</a> by <a href="https://github.com/rfrail3">rfrail3</a>: -</p>
<p><strong>Alpine Linux</strong>
<img src="/img/prometheus/prom-consul-alpine.png" alt="Alpine Linux Node Exporter Dashboard"></p>
<p><strong>Arch Linux</strong>
<img src="/img/prometheus/prom-consul-arch.png" alt="Arch Linux Node Exporter Dashboard"></p>
<p><strong>OpenSUSE Linux</strong>
<img src="/img/prometheus/prom-consul-suse.png" alt="OpenSUSE Node Exporter Dashboard"></p>
<p><strong>Void Linux</strong>
<img src="/img/prometheus/prom-consul-void.png" alt="Void Linux Node Exporter Dashboard"></p>
<h2 id="git-repository">Git Repository</h2>
<p>All of the states (as well as those for future posts, if you want a quick preview) are available in my <a href="https://gitlab.com/stuh84/salt-lab/">Salt Lab</a> repository.</p>
<h2 id="summary">Summary</h2>
<p>One of the joys of configuration management is that when you add new hosts into your infrastructure, you have states/playbooks/recipes that deploy your base packages, manage users and more. This means you no longer have to do all of this manually every time you add a new host.</p>
<p>If you then also deploy Consul (and the appropriate Consul service declarations) as we have above, these additional hosts also are ingested into your monitoring system automatically too. The benefits of this are massive, saving time and reducing mistakes (forgetting to add hosts into monitoring, deploying the wrong type of monitoring etc).</p>
<p>In the next post in this series, we will cover how you deploy SaltStack on a Windows host, which will then deploy Consul and the Prometheus <a href="https://github.com/prometheus-community/windows_exporter">Windows Exporter</a>.</p>
]]></content>
        </item>
        
        <item>
            <title>Ansible for Networking - Part 6: MikroTik RouterOS</title>
            <link>https://yetiops.net/posts/ansible-for-networking-part-6-mikrotik-routeros/</link>
            <pubDate>Thu, 21 May 2020 20:05:33 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/ansible-for-networking-part-6-mikrotik-routeros/</guid>
            <description>The sixth part of my ongoing series of posts on Ansible for Networking will cover Mikrotik&amp;rsquo;s RouterOS. You can view the other posts in the series below: -
 Part 1 - Start of the series Part 2 - The Lab Environment Part 3 - Cisco IOS Part 4 - Juniper JunOS Part 5 - Arista EOS Part 7 - VyOS  All the playbooks, roles and variables used in this article are available in my Network Automation with Ansible repository.</description>
            <content type="html"><![CDATA[<p>The sixth part of my ongoing series of posts on Ansible for Networking will cover Mikrotik&rsquo;s RouterOS. You can view the other posts in the series below: -</p>
<ul>
<li><a href="/posts/ansible-for-networking-series">Part 1 - Start of the series</a></li>
<li><a href="/posts/ansible-for-networking-part-2-lab">Part 2 - The Lab Environment</a></li>
<li><a href="/posts/ansible-for-networking-part-3-cisco-ios">Part 3 - Cisco IOS</a></li>
<li><a href="/posts/ansible-for-networking-part-4-juniper-junos">Part 4 - Juniper JunOS</a></li>
<li><a href="/posts/ansible-for-networking-part-5-arista-eos">Part 5 - Arista EOS</a></li>
<li><a href="/posts/ansible-for-networking-part-7-vyos">Part 7 - VyOS</a></li>
</ul>
<p>All the playbooks, roles and variables used in this article are available in my <a href="https://gitlab.com/stuh84/network-automation-ansible">Network Automation with Ansible repository</a>.</p>
<h2 id="why-routeros">Why RouterOS?</h2>
<p>MikroTik is a Latvian company who provide routing, switching, wireless and other networking devices. Their operating system (RouterOS) is built upon Linux, but unlike Arista EOS (or the BSD base of JunOS), you don&rsquo;t typically have access to a Linux shell itself.</p>
<p>MikroTik are often significantly lower in price than what you&rsquo;d find from other vendors. For example, I have a MikroTik RB4011 for my home router that has 10 single gigabit ports and 1 ten gigabit port. You&rsquo;d typically be looking in the multiple hundreds or thousands of pounds for a similar offering from other vendors. The price? £180.</p>
<p>MikroTiks have a reputation for being a networking Swiss army knife. Even on their hAP Lite (a £20 access point and router), they support packet captures, BGP, stateful firewalling, IPSec VPNs and more. You are likely to see high resource usage and performance impact when enabling some of these features, but the fact they are available at this price level is astounding.</p>
<p>Because of the price and flexibility of MikroTik devices, they are a very popular option for smaller ISPs and WISPs (Wireless ISPs). I have used the extensively in my career, in everything from VPN concentration to regional layer 2 extensions.</p>
<h3 id="routeros-cli">RouterOS CLI</h3>
<p>RouterOS&rsquo;s command line interface is unique in the networking world. For example, to add an IP address to an interface on a MikroTik, you would do the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-01] &gt; /ip address add interface=test address=192.168.89.1/24
[admin@routeros-01] &gt;
</code></pre></div><p>To remove it, you wouldn&rsquo;t run <code>/ip address remove interface=test address=192.168.89.1/24</code>. Instead, you&rsquo;ll need to do one of the following: -</p>
<p><strong>Find out the ID/number of the interface and remove</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Print IP addressing
[admin@routeros-01] &gt; /ip address print
Flags: X - disabled, I - invalid, D - dynamic
 #   ADDRESS            NETWORK         INTERFACE
 0   ;;; Management
     10.15.30.53/24     10.15.30.0      ether1
 1   ;;; Loopback
     192.0.2.104/32     192.0.2.104     loopback0
 2   ;;; To netsvr
     10.100.104.253/24  10.100.104.0    vlan104
 3   ;;; To routeros-02
     10.100.204.254/24  10.100.204.0    vlan204
 4 D 192.168.122.208/24 192.168.122.0   ether3
 5   192.168.89.1/24    192.168.89.0    test

! Remove the IP address
[admin@routeros-01] &gt; /ip address remove 5

! Check it is removed
[admin@routeros-01] &gt; /ip address print
Flags: X - disabled, I - invalid, D - dynamic
 #   ADDRESS            NETWORK         INTERFACE
 0   ;;; Management
     10.15.30.53/24     10.15.30.0      ether1
 1   ;;; Loopback
     192.0.2.104/32     192.0.2.104     loopback0
 2   ;;; To netsvr
     10.100.104.253/24  10.100.104.0    vlan104
 3   ;;; To routeros-02
     10.100.204.254/24  10.100.204.0    vlan204
 4 D 192.168.122.208/24 192.168.122.0   ether3
</code></pre></div><p><strong>Using <code>find</code></strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Print IP addressing
[admin@routeros-01] &gt; /ip address print
Flags: X - disabled, I - invalid, D - dynamic
 #   ADDRESS            NETWORK         INTERFACE
 0   ;;; Management
     10.15.30.53/24     10.15.30.0      ether1
 1   ;;; Loopback
     192.0.2.104/32     192.0.2.104     loopback0
 2   ;;; To netsvr
     10.100.104.253/24  10.100.104.0    vlan104
 3   ;;; To routeros-02
     10.100.204.254/24  10.100.204.0    vlan204
 4 D 192.168.122.208/24 192.168.122.0   ether3
 5   192.168.89.1/24    192.168.89.0    test

! Using the find command
[admin@routeros-01] &gt; /ip address remove [find where address=&#34;192.168.89.1/24&#34; and interface=&#34;test&#34;]

! Check it is removed
[admin@routeros-01] &gt; /ip address print
Flags: X - disabled, I - invalid, D - dynamic
 #   ADDRESS            NETWORK         INTERFACE
 0   ;;; Management
     10.15.30.53/24     10.15.30.0      ether1
 1   ;;; Loopback
     192.0.2.104/32     192.0.2.104     loopback0
 2   ;;; To netsvr
     10.100.104.253/24  10.100.104.0    vlan104
 3   ;;; To routeros-02
     10.100.204.254/24  10.100.204.0    vlan204
 4 D 192.168.122.208/24 192.168.122.0   ether3
</code></pre></div><p>If you want to change the address rather than deleting it, you would use commands like <code>/ip address set 5 address=X.X.X.X/24</code> or <code>/ip address set [find where address=&quot;192.168.89.1/24&quot; and interface=&quot;test&quot;] address=X.X.X.X/24</code>.</p>
<p>Also, the ID numbers are not always consistent. For example if you deleted the second IP address (<code>192.0.2.104/32</code>) in the list above, this will move the third IP address to second in the list. However if you then tried to run <code>/ip address remove 2</code>, the command line would return &ldquo;no such item&rdquo; (as you have already removed the second item in the list). You need to run <code>/ip address print</code> to &ldquo;regenerate&rdquo; the list, and then you can remove the second address,</p>
<p>Finally, some settings do not have ID numbers. A good example would be the hostname of the device (<code>/system identity set name=$HOSTNAME</code>). With these settings, you cannot <code>add</code> or <code>delete</code> settings, only <code>set</code> (i.e. updating/changing them).</p>
<p>All of this makes automating the configuration with Ansible (and other tools) more complex. Because you cannot reference elements of configuration directly, and IDs can change between commands, it isn&rsquo;t quite as straightforward as something like IOS or JunOS. Also, because some commands use <code>set</code> for configuration and others require <code>add</code> (and then later using <code>set</code> to change them), it requires a lot of trial and error to build a list of commands that produces the same configuration every time they are applied (i.e. idempotence).</p>
<h2 id="objectives">Objectives</h2>
<p>For each vendor, I will be using Ansible to configure two routers/switches/firewalls/appliances.</p>
<p>One will serve as the <em>Edge</em> router, connecting to the Internet and also via BGP to the <strong>Net Server</strong>. The <strong>Net Server</strong> is a CentOS 8 Virtual Machine acting as a route server, syslog collector and TACACS+ server (detailed in <a href="/posts/ansible-for-networking-part-2-lab">The Lab Environment</a>)</p>
<p>The other will be an <em>internal</em> router, performing <strong>core</strong> functions (i.e. internal routing rather than external).</p>
<p>This lab is based upon the MikroTik CHR (Cloud Hosted Router) platform.</p>
<h3 id="edge-router">Edge router</h3>
<p>The edge router will run the following: -</p>
<ul>
<li><strong>External BGP</strong> (eBGP) to the <strong>Net Server</strong>
<ul>
<li>Advertising internal networks</li>
</ul>
</li>
<li><strong>Internal BGP</strong> (iBGP) to the <strong>Internal</strong> router
<ul>
<li>Advertising any routes received from the <strong>Net Server</strong></li>
<li>Advertising a default route (for internet access)</li>
</ul>
</li>
<li><strong>OSPF</strong>
<ul>
<li>Advertising loopbacks and internal networks between both routers</li>
</ul>
</li>
<li><strong>IPv4</strong> and <strong>IPv6</strong> routing
<ul>
<li>Using OSPFv3 (for <strong>IPv6</strong> support)</li>
<li>Using the IPv6 <em>Address Family</em> for BGP</li>
</ul>
</li>
<li><strong>SNMPv3</strong> for monitoring</li>
<li><strong>IPv4 NAT</strong> to allow internet access
<ul>
<li>I cannot run IPv6 for internet access, as my current ISP does not support IPv6</li>
</ul>
</li>
<li><strong>Logging</strong> via Syslog to the <strong>Net Server</strong></li>
<li><strong>Authentication, Authorization and Accounting</strong> (AAA) via <del>TACACS+</del> RADIUS to the <strong>Net Server</strong></li>
<li><strong>Zones</strong> to place interfaces in, for zone-based firewalling</li>
<li><strong>Firewall Rules</strong> to allow traffic to/from the <strong>Net Server</strong></li>
</ul>
<p>Notice in the AAA objective, we are using RADIUS, rather than TACACS+. This is because MikroTik devices do not support TACACS+.</p>
<h3 id="internal-router">Internal router</h3>
<p>The internal router runs a subset of the functions that the edge router does: -</p>
<ul>
<li><strong>Internal BGP</strong> (iBGP) to the <strong>Edge</strong> router
<ul>
<li>Receiving any routes received from the <strong>Net Server</strong></li>
<li>Receiving a default route (for internet access)</li>
</ul>
</li>
<li><strong>OSPF</strong>
<ul>
<li>Advertising loopbacks and internal networks between both routers</li>
</ul>
</li>
<li><strong>IPv4</strong> and <strong>IPv6</strong> routing
<ul>
<li>Using OSPFv3 (for <strong>IPv6</strong> support)</li>
<li>Using the IPv6 <em>Address Family</em> for BGP</li>
</ul>
</li>
<li><strong>SNMPv3</strong> for monitoring</li>
<li><strong>Logging</strong> via Syslog to the <strong>Net Server</strong></li>
<li><strong>Authentication, Authorization and Accounting</strong> (AAA) via <del>TACACS+</del> RADIUS to the <strong>Net Server</strong></li>
<li><strong>Zones</strong> to place interfaces in, for zone-based firewalling</li>
<li><strong>Firewall Rules</strong> to allow traffic to/from the <strong>Net Server</strong></li>
</ul>
<p>Notice in the AAA objective, we are using RADIUS, rather than TACACS+. This is because MikroTik devices do not support TACACS+.</p>
<h2 id="radius">RADIUS</h2>
<p>As RADIUS requires a new role on <strong>netsvr-01</strong>, I will update the original post with the details on how we configure this using Ansible. Support for TACACS+ has been suggested as a feature request on RouterOS, but at the time of writing it is not available.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>To manage a MikroTik device with Ansible, the following steps are required. We also make some changes to the default Ansible connection configuration.</p>
<h3 id="ansible-configuration">Ansible Configuration</h3>
<p>The following defaults are required to use Ansible with MikroTik RouterOS: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">ansible_user</span>: ansible
<span style="color:#66d9ef">ansible_connection</span>: network_cli
<span style="color:#66d9ef">ansible_network_os</span>: routeros
<span style="color:#66d9ef">ansible_ssh_pass</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">            $ANSIBLE_VAULT;1.1;AES256</span>
            <span style="color:#ae81ff">383</span><span style="color:#75715e">###REDACTED###############################################################566</span>
            <span style="color:#ae81ff">363</span><span style="color:#75715e">###REDACTED###############################################################366</span>
            <span style="color:#ae81ff">343</span><span style="color:#75715e">###REDACTED###############################################################565</span>
            <span style="color:#ae81ff">326</span><span style="color:#75715e">###REDACTED###############################################################362</span>
            <span style="color:#ae81ff">3431</span>
</code></pre></div><p>As with JunOS, there is no <strong>enable</strong> or <strong>privileged</strong> mode within RouterOS. Instead, the user privileges  determine whether a user can run commands or make changes.</p>
<h3 id="mikrotik-configuration">MikroTik Configuration</h3>
<p>To allow Ansible access to the MikroTik routers, they need an account creating with <strong>full</strong> privileges (the default RouterOS administrator group). We will also set the IP address and the hostname.</p>
<p>Below shows how to enable all of this: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">## Add the user
[admin@mikrotik] &gt; /user add name=ansible group=full password=###PASSWORD###

## Add an IP to the management interface
[admin@mikrotik] &gt; /ip address add address=10.15.30.53/24 comment=Management interface=ether1

## Verify the IP address is applied
[admin@mikrotik] &gt; /ip address print where comment=Management
Flags: X - disabled, I - invalid, D - dynamic
 #   ADDRESS            NETWORK         INTERFACE
 0   ;;; Management
     10.15.30.53/24     10.15.30.0      ether1

## Set the hostname
[admin@mikrotik] &gt; /system identity set name=routeros-01
[admin@routeros-01] &gt;
</code></pre></div><p>Additionally, you can also set up SSH keys to allow SSH access without using a password. To do this, first you must transfer your public key to the device. I do this with <code>scp ~/.ssh/id_ed25519.pub ansible@$ROUTER-IP</code>. After this, add the key to user like so: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">## Verify the key exists on the router
[admin@routeros-01] &gt; /file print where name ~&#34;id.*pub&#34;
 # NAME                       TYPE             IZE CREATION-TIME
 0 id_ed25519.pub             ssh key           98 apr/12/2020 19:14:11

## Add it to the Ansible user
[admin@routeros-01] &gt; /user ssh-keys import public-key-file=id_ed25519.pub user=ansible
</code></pre></div><p>At this point, you should be able to SSH to the router without using a password.</p>
<p>As you can see above, we already have multiple different ways of applying configuration: -</p>
<ul>
<li><code>/ip address add</code> - Add an IP address</li>
<li><code>/system identity set</code> - Set an identity</li>
<li><code>/user ssh-keys import</code> - Import a key</li>
</ul>
<p>When we start looking at the playbooks, you&rsquo;ll notice this again and again!</p>
<p>Our inventory file looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">[mikrotik]
routeros<span style="color:#ae81ff">-01</span> ansible_host=<span style="color:#ae81ff">10.15.30.53</span>
routeros<span style="color:#ae81ff">-02</span> ansible_host=<span style="color:#ae81ff">10.15.30.54</span>
</code></pre></div><h4 id="verification">Verification</h4>
<p>Can we contact both devices?</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ansible mikrotik -m routeros_facts --ask-vault-pass | grep -i hostname
Vault password:
        <span style="color:#e6db74">&#34;ansible_net_hostname&#34;</span>: <span style="color:#e6db74">&#34;routeros-01&#34;</span>,
        <span style="color:#e6db74">&#34;ansible_net_hostname&#34;</span>: <span style="color:#e6db74">&#34;routeros-02&#34;</span>,
</code></pre></div><h2 id="setup">Setup</h2>
<p>The setup is identical to the IOS and Juniper lab, with a management interface to access the devices, a VLAN bridge for inter-device communication, and an interface on the edge router attached to the KVM NAT bridge for DHCP/Internet access. Unlike the Arista vEOS images, VLANs work correctly in the lab.</p>
<h3 id="vlans-ip-addressing-and-autonomous-system-numbers">VLANs, IP addressing and Autonomous System numbers</h3>
<p>The ID chosen for MikroTik RouterOS is <code>04</code>.</p>
<h4 id="vlans">VLANs</h4>
<p>The VLANs used will be: -</p>
<ul>
<li>VLAN104 between the <em>edge</em> router and <em>netsvr-01</em></li>
<li>VLAN204 between the <em>edge</em> router and <em>internal</em> router</li>
</ul>
<h4 id="ip-addressing">IP Addressing</h4>
<ul>
<li>IPv4 Subnet on VLAN104: <code>10.100.104.0/24</code>
<ul>
<li><em>edge</em> router - <code>10.100.104.253/24</code></li>
<li><em>netsvr-01</em> - <code>10.100.104.254/24</code></li>
</ul>
</li>
<li>IPv4 Subnet on VLAN204: 10.100.204.0/24
<ul>
<li><em>edge</em> router - <code>10.100.204.254/24</code></li>
<li><em>internal</em> router - <code>10.100.204.253/24</code></li>
</ul>
</li>
<li>IPv6 Subnet on VLAN104: <code>2001:db8:104::/64</code>
<ul>
<li><em>edge</em> router - <code>2001:db8:104::f/64</code></li>
<li><em>netsvr-01</em> - <code>2001:db8:104:ffff/64</code></li>
</ul>
</li>
<li>IPv6 Subnet on VLAN204: 2001:db8:204::/64
<ul>
<li><em>edge</em> router - <code>2001:db8:204::a/64</code></li>
<li><em>internal</em> router - <code>2001:db8:204:f/64</code></li>
</ul>
</li>
<li>IPv4 Loopback Addressing
<ul>
<li><em>edge</em> router - <code>192.0.2.104/32</code></li>
<li><em>internal</em> router - <code>192.0.2.204/32</code></li>
</ul>
</li>
<li>IPv6 Loopback Address
<ul>
<li><em>edge</em> router - <code>2001:db8:904:beef::1/128</code></li>
<li><em>internal</em> router - <code>2001:db8:904:beef::2/128</code></li>
</ul>
</li>
</ul>
<h4 id="bgp-autonomous-system">BGP Autonomous System</h4>
<p>The BGP <a href="https://en.wikipedia.org/wiki/Autonomous_system_(Internet)">Autonomous System</a> number will be <code>AS65104</code>.</p>
<h2 id="configuration">Configuration</h2>
<p>Before we start looking at the Ansible playbooks themselves, there is one major point to address when managing RouterOS with Ansible. At the time of writing only two modules exist for RouterOS, <strong>routeros_facts</strong> and <strong>routeros_command</strong>. We can either gather information from the device itself, or we can run RouterOS commands.</p>
<p>Our playbooks consist of raw RouterOS commands, with no inbuilt idempotence or abstraction. To make configuration apply the same way twice, we use RouterOS features like <code>find where</code> (i.e. conditional logic) and in some cases command parsing and regular expressions.</p>
<h3 id="system-tasks">System tasks</h3>
<p>This role sets the hostname, sets the login banner, and enables remote logging to <strong>syslog</strong> on the <strong>netsvr-01</strong> machine.</p>
<h4 id="playbook">Playbook</h4>
<p>The contents of the playbook are as follows: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for system</span>

- <span style="color:#66d9ef">name</span>: Set hostname
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /system identity set name=<span style="color:#e6db74">&#34;{{ inventory_hostname }}&#34;</span>

- <span style="color:#66d9ef">name</span>: Update login banner
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /system note set show-at-login=yes
      - <span style="color:#e6db74">&gt;
</span><span style="color:#e6db74">        /system note set note=&#34;\</span>
        \n      ----------------------------------------\
        \n      |\
        \n      | This banner was generated by Ansible\
        \n      |\
        \n      ----------------------------------------\
        \n      |\
        \n      | You are logged into {{ inventory_hostname }}\
        \n      |\
        \n      ----------------------------------------<span style="color:#e6db74">&#34;
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">- name: Configure syslog
</span><span style="color:#e6db74">  routeros_command:
</span><span style="color:#e6db74">    commands:
</span><span style="color:#e6db74">      - /system logging remove [find where action=netsvr]
</span><span style="color:#e6db74">      - /system logging action set [find where action=remote] remote=&#34;</span>{{ log_host }}<span style="color:#e6db74">&#34;
</span><span style="color:#e6db74">      - /system logging action remove [find where name=netsvr]
</span><span style="color:#e6db74">      - /system logging action add name=&#34;</span>netsvr<span style="color:#e6db74">&#34; remote=&#34;</span>{{ log_host }}&#34; remote-port=<span style="color:#ae81ff">514</span> syslog-facility=local7 syslog-severity=auto target=remote
      - /system logging add action=netsvr disabled=no prefix={{ inventory_hostname }} topics=system,info
      - /system logging add action=netsvr disabled=no prefix={{ inventory_hostname }} topics=warning
      - /system logging add action=netsvr disabled=no prefix={{ inventory_hostname }} topics=critical
      - /system logging add action=netsvr disabled=no prefix={{ inventory_hostname }} topics=error,!ospf,!route
  <span style="color:#66d9ef">tags</span>:
    - logging
</code></pre></div><p>As you can see, every command is a native RouterOS command. We also see our first occurrence of <code>find where</code>.</p>
<h5 id="find-where-and-idempotence"><code>find where</code> and idempotence</h5>
<p>The <code>find where</code> syntax is used to find any configuration items that match a certain value.</p>
<p>For example, in the above we have <code>/system logging remove [find where action=netsvr]</code>. This line looks for any configuration in the <code>/system logging</code> context that has an <code>action</code> that equals <code>netsvr</code>, and removes it. Similarly, the line <code>/system logging action remove [find where name=netsvr]</code> looks for any configuration in the <code>/system logging action</code> context with a name of <code>netsvr</code>.</p>
<p>The reason we cannot just remove configuration lines is due to the aforementioned issue where certain configuration has an <strong>id</strong>. This <strong>id</strong> changes based upon how many other elements of configuration exist within this context: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-01] /system logging action&gt; print
Flags: * - default
 0 * name=&#34;memory&#34; target=memory memory-lines=1000 memory-stop-on-full=no

 1 * name=&#34;disk&#34; target=disk disk-file-name=&#34;log&#34; disk-lines-per-file=1000 disk-file-count=2 disk-stop-on-full=no

 2 * name=&#34;echo&#34; target=echo remember=yes

 3 * name=&#34;remote&#34; target=remote remote=192.0.2.1 remote-port=514 src-address=0.0.0.0 bsd-syslog=no
     syslog-time-format=bsd-syslog syslog-facility=daemon syslog-severity=auto

 4   name=&#34;netsvr&#34; target=remote remote=10.100.104.254 remote-port=514 src-address=0.0.0.0 bsd-syslog=no
     syslog-time-format=bsd-syslog syslog-facility=local7 syslog-severity=auto
</code></pre></div><p>In the above, if we had another line before <strong>netsvr</strong>, the <strong>netsvr</strong> action would have an <strong>id</strong> of 5. This makes the <strong>id</strong> unpredictable, so we cannot build playbooks that make assumptions on what <strong>id</strong> number a line of configuration will have.</p>
<p>Additionally we cannot assume that the configuration already exists, meaning that we may need to use either <code>add</code> (i.e. adding new configuration) or <code>set</code> (updating existing configuration).</p>
<p>As in previous parts of this series, we will sometimes remove some existing configuration and rebuild it. This allows the configuration to reflect the variables we have supplied, without orphaned configuration.</p>
<h4 id="generated-configuration">Generated configuration</h4>
<p>The variables we use for this configuration are <code>inventory_hostname</code> (an in-built Ansible variable) and <code>log_host</code>, which exists in our <code>group_vars</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ cat group_vars/junos | grep -i log
log_host: 10.100.101.254
</code></pre></div><p>The actual generated configuration from the above is: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/system identity
set name=routeros-01

/system note
set show-at-login=yes
set note=&#34;\
    \n      ----------------------------------------\
    \n      |\
    \n      | This banner was generated by Ansible\
    \n      |\
    \n      ----------------------------------------\
    \n      |\
    \n      | You are logged into routeros-01\
    \n      |\
    \n      ----------------------------------------&#34;

/system logging action
set 3 remote=192.0.2.1
add name=netsvr remote=10.100.104.254 syslog-facility=local7 target=remote
/system logging
add action=netsvr prefix=routeros-01 topics=system,info
add action=netsvr prefix=routeros-01 topics=warning
add action=netsvr prefix=routeros-01 topics=critical
add action=netsvr prefix=routeros-01 topics=error,!ospf,!route
</code></pre></div><h4 id="verification-1">Verification</h4>
<p>Can we see log messages getting to <strong>netsvr-01</strong>?</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ tail -n <span style="color:#ae81ff">4</span> /var/log/remote/10.100.104.253
2020-05-05T19:45:16+01:00 10.100.104.253: system,info,account routeros-01: user ansible logged in from 10.15.30.1 via ssh
2020-05-05T19:45:28+01:00 10.100.104.253: system,info,account routeros-01: user ansible logged out from 10.15.30.1 via ssh
2020-05-05T19:45:42+01:00 10.100.104.253: system,info,account routeros-01: user ansible logged in from 10.15.30.1 via ssh
2020-05-05T19:45:53+01:00 10.100.104.253: system,info,account routeros-01: user ansible logged out from 10.15.30.1 via ssh

$ tail -n <span style="color:#ae81ff">4</span> /var/log/remote/10.100.204.253
2020-05-05T19:45:28+01:00 10.100.204.253: system,info,account routeros-02: user ansible logged out from 10.15.30.1 via ssh
2020-05-05T19:45:42+01:00 10.100.204.253: system,info,account routeros-02: user ansible logged in from 10.15.30.1 via ssh
2020-05-05T19:45:53+01:00 10.100.204.253: system,info,account routeros-02: user ansible logged out from 10.15.30.1 via ssh
2020-05-05T19:49:26+01:00 10.100.204.253: system,info,account routeros-02: user admin logged in from 10.15.30.1 via ssh
</code></pre></div><p>Looks good!</p>
<h3 id="interfaces">Interfaces</h3>
<p>This role configures all of the interfaces, including VLANs, creating &ldquo;loopbacks&rdquo; and IP addressing (IPv4 and IPv6).</p>
<h4 id="playbook-1">Playbook</h4>
<p>The contents of the playbook are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for interfaces</span>
<span style="color:#75715e">##</span>
- <span style="color:#66d9ef">name</span>: Configure VLANs
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /interface vlan add name=<span style="color:#e6db74">&#34;vlan{{ item.vlan_id }}&#34;</span> interface=<span style="color:#e6db74">&#34;{{ item.interface }}&#34;</span> comment=<span style="color:#e6db74">&#34;To {{ item.name }}&#34;</span> vlan-id=<span style="color:#e6db74">&#34;{{ item.vlan_id }}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - vlans is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ vlans }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - vlans

- <span style="color:#66d9ef">name</span>: Create Loopbacks
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /interface bridge add name=<span style="color:#e6db74">&#34;{{ item.routeros_if }}&#34;</span> comment=<span style="color:#e6db74">&#34;{{ item.desc }}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - item.routeros_if is match(<span style="color:#e6db74">&#39;loopback.*&#39;</span>)
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - loopback

- <span style="color:#66d9ef">name</span>: Configure interfaces
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /interface set comment=<span style="color:#e6db74">&#34;{{ item.desc }}&#34;</span> [find where name=<span style="color:#e6db74">&#34;{{ item.routeros_if }}&#34;</span>]
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - interfaces

- <span style="color:#66d9ef">name</span>: Configure IPv4 addressing
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /ip address add interface=<span style="color:#e6db74">&#34;{{ item.routeros_if }}&#34;</span> address=<span style="color:#e6db74">&#34;{{ item.ipv4 }}&#34;</span> comment=<span style="color:#e6db74">&#34;{{ item.desc }}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - item.ipv4 is defined
    - <span style="color:#e6db74">&#39;&#34;dhcp&#34; not in item.ipv4&#39;</span>
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ipv4


- <span style="color:#66d9ef">name</span>: Configure IPv4 addressing - DHCP
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /ip dhcp-client add interface=<span style="color:#e6db74">&#34;{{ item.routeros_if }}&#34;</span> disabled=no comment=<span style="color:#e6db74">&#34;{{ item.desc }}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - item.ipv4 is defined
    - <span style="color:#e6db74">&#39;&#34;dhcp&#34; in item.ipv4&#39;</span>
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ipv4

- <span style="color:#66d9ef">name</span>: Configure IPv6 addressing
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /ipv6 address add interface=<span style="color:#e6db74">&#34;{{ item.routeros_if }}&#34;</span> address=<span style="color:#e6db74">&#34;{{ item.ipv6 }}&#34;</span> comment=<span style="color:#e6db74">&#34;{{ item.desc }}&#34;</span>
  <span style="color:#66d9ef">when</span>: item.ipv6 is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ipv6

- <span style="color:#66d9ef">name</span>: Interface cleanup
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /ip address remove [find where invalid]
  <span style="color:#66d9ef">tags</span>:
    - ipv4
    - ipv6
</code></pre></div><p>Again, no Ansible RouterOS Ansible modules exist outside of <code>routeros_command</code> (for configuration changes). We are using parametrized RouterOS commands, the variables being sourced from our <code>host_vars</code> or <code>group_vars</code>.</p>
<h5 id="configuring-vlans">Configuring VLANs</h5>
<p>There are many different ways to configure VLANs in RouterOS, in part due to the different switch chips MikroTik have used across their devices (historically and currently). Some benefit from VLAN tags being switched/evaluated in hardware, whereas others require software bridging.</p>
<p>The approach I am using is the <code>/interface vlan add</code> command, as we are using the RouterOS images for routing rather than switching. If we were switching (rather than routing) traffic, we would probably need to use one of the <code>bridge vlan</code> options (see <a href="https://wiki.mikrotik.com/wiki/Manual:Basic_VLAN_switching">here</a>).</p>
<p>This task checks to see if any VLANs are defined. If any are, it loops through and adds them (configuring the name, ID and interface they are tagged on).</p>
<p>The relevant <code>host_vars</code> that we create VLANs with are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">vlans</span>:
  - <span style="color:#66d9ef">name</span>: netsvr<span style="color:#ae81ff">-01</span>
    <span style="color:#66d9ef">vlan_id</span>: <span style="color:#ae81ff">104</span>
    <span style="color:#66d9ef">interface</span>: ether2
  - <span style="color:#66d9ef">name</span>: routeros<span style="color:#ae81ff">-02</span>
    <span style="color:#66d9ef">vlan_id</span>: <span style="color:#ae81ff">204</span>
    <span style="color:#66d9ef">interface</span>: ether2
</code></pre></div><p>This generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/interface vlan add comment=&#34;To netsvr&#34; interface=ether2 name=vlan104 vlan-id=104
/interface vlan add comment=&#34;To routeros-02&#34; interface=ether2 name=vlan204 vlan-id=204
</code></pre></div><p>We can verify this with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-01] &gt; /interface vlan print
Flags: X - disabled, R - running
 #   NAME                        MTU ARP             VLAN-ID INTERFACE
 0 R ;;; To netsvr
     vlan104                    1500 enabled             104 ether2
 1 R ;;; To routeros-02
     vlan204                    1500 enabled             204 ether2
</code></pre></div><h5 id="creating-loopbacks">Creating Loopbacks</h5>
<p>RouterOS does not have any pre-defined loopback interfaces (like in JunOS), or an interface type of &ldquo;Loopback&rdquo;  (e.g. <code>LoopbackX</code> in IOS). To have something like a loopback within MikroTik, we create a bridge interface with no physical ports bridged to it. The interface still comes up, and is routable, without the need to bind it to any other interface.</p>
<p>We need to create these interfaces, rather than running something like <code>interface Loopback0</code> (as we would in IOS). To do so, we loop through our interfaces defined in <code>host_vars</code> and use a small regular expression to match against the names of our defined interfaces. If any have &ldquo;loopback&rdquo; in the name, we create a bridge with that name.</p>
<p>For example, in our <code>host_vars</code> we have: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;ether1&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;ether2&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;vlan104&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;vlan204&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;ether3&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;loopback0&#34;</span>
</code></pre></div><p>When evaluated against our Ansible <strong>match</strong> function (<code>item.routeros_if is match('loopback.*')</code>, the only one that matches our expression is <code>loopback0</code>.</p>
<p>The generated configuration is therefore: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/interface bridge add comment=Loopback name=loopback0
</code></pre></div><p>We can verify this has been created with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-01] /interface bridge&gt; print brief
Flags: X - disabled, R - running
 #   NAME                   MTU ACTUAL-MTU L2MTU
 0 R ;;; Loopback
     loopback0             auto       1500 65535
</code></pre></div><h5 id="configure-ipv4-addressing">Configure IPv4 Addressing</h5>
<p>This task loops through our interface <code>host_vars</code>, and when it has an IPv4 address (that is not <strong>dhcp</strong>), it adds them to the device. Unlike IOS, JunOS or EOS, DHCP is configured in a different configuration context (rather than something like <code>set interface fxp0 unit 0 family inet address dhcp</code>).</p>
<p>The relevant <code>host_vars</code> are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;ether1&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Management&#34;</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.15.30.53/24&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;ether2&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;VLAN Bridge&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;vlan104&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To netsvr&#34;</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.100.104.253/24&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;vlan204&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To routeros-02&#34;</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.100.204.254/24&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;ether3&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To the Internet&#34;</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;dhcp&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;loopback0&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;192.0.2.104/32&#34;</span>
</code></pre></div><p>This generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/ip address add address=10.15.30.53/24 comment=Management interface=ether1 network=10.15.30.0
/ip address add address=192.0.2.104 comment=Loopback interface=loopback0 network=192.0.2.104
/ip address add address=10.100.104.253/24 comment=&#34;To netsvr&#34; interface=vlan104 network=10.100.104.0
/ip address add address=10.100.204.254/24 comment=&#34;To routeros-02&#34; interface=vlan204 network=10.100.204.0
</code></pre></div><p>As noted, no configuration is generated for the DHCP interface.</p>
<p>We can verify this with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-01] &gt; /ip address print
Flags: X - disabled, I - invalid, D - dynamic
 #   ADDRESS            NETWORK         INTERFACE
 0   ;;; Management
     10.15.30.53/24     10.15.30.0      ether1
 1   ;;; Loopback
     192.0.2.104/32     192.0.2.104     loopback0
 2   ;;; To netsvr
     10.100.104.253/24  10.100.104.0    vlan104
 3   ;;; To routeros-02
     10.100.204.254/24  10.100.204.0    vlan204
</code></pre></div><h5 id="configure-ipv4-dhcp">Configure IPv4 DHCP</h5>
<p>This task loops through our interface <code>host_vars</code>. If the interface has an IPv4 address field that contains the word <code>dhcp</code>, then we will configure that interface as a DHCP client.</p>
<p>The same <code>host_vars</code> from the previous task are relevant here. This generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/ip dhcp-client add comment=&#34;To the Internet&#34; dhcp-options=hostname,clientid disabled=no interface=ether3
</code></pre></div><p>The <code>dhcp-options</code> field is a default value. You can add or remove options from this if you choose.</p>
<p>We can verify this with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-01] /ip dhcp-client&gt; print
Flags: X - disabled, I - invalid, D - dynamic
 #   INTERFACE                USE-PEER-DNS ADD-DEFAULT-ROUTE STATUS        ADDRESS
 0   ;;; To the Internet
     ether3                   yes          yes               bound         192.168.122.208/24

[admin@routeros-01] &gt; /ip dns print
                      servers:
              dynamic-servers: 192.168.122.1
        allow-remote-requests: no
          max-udp-packet-size: 4096
         query-server-timeout: 2s
          query-total-timeout: 10s
       max-concurrent-queries: 100
  max-concurrent-tcp-sessions: 20
                   cache-size: 2048KiB
                cache-max-ttl: 1w
                   cache-used: 17KiB

[admin@routeros-01] &gt; /ip route print where dst-address=0.0.0.0/0
Flags: X - disabled, A - active, D - dynamic, C - connect, S - static, r - rip, b - bgp, o - ospf, m - mme, B - blackhole, U - unreachable, P - prohibit
 #      DST-ADDRESS        PREF-SRC        GATEWAY            DISTANCE
 0 ADS  0.0.0.0/0                          192.168.122.1             1
</code></pre></div><h5 id="configure-ipv6-addressing">Configure IPv6 addressing</h5>
<p>This task is identical to the IPv4 addressing task, except that it applies IPv6 addresses instead. The relevant <code>host_vars</code> are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;vlan104&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To netsvr&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:104::f/64&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;vlan204&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To routeros-02&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:204::a/64&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;loopback0&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:904:beef::1/128&#34;</span>
</code></pre></div><p>This generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/ipv6 address add address=2001:db8:104::f comment=&#34;To netsvr&#34; interface=vlan104
/ipv6 address add address=2001:db8:204::a comment=&#34;To routeros-02&#34; interface=vlan204
/ipv6 address add address=2001:db8:904:beef::1/128 advertise=no comment=Loopback interface=loopback0
</code></pre></div><p>We can verify this with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-01] &gt; /ipv6 address print
Flags: X - disabled, I - invalid, D - dynamic, G - global, L - link-local
 #    ADDRESS                        FROM-POOL INTERFACE       ADVERTISE
 0  G ;;; To netsvr
      2001:db8:104::f/64                       vlan104         yes
 1  G ;;; To routeros-02
      2001:db8:204::a/64                       vlan204         yes
 2  G ;;; Loopback
      2001:db8:904:beef::1/128                 loopback0       no
</code></pre></div><p>The <code>advertise=no</code> option is because <code>loopback0</code> is not a physical interface. This means that we do not require IPv6 Neighbour Discovery.</p>
<h4 id="verification-2">Verification</h4>
<p><strong>routeros-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show IPs (IPv4 and IPv6)
[admin@routeros-01] &gt; /ip address print
Flags: X - disabled, I - invalid, D - dynamic
 #   ADDRESS            NETWORK         INTERFACE
 0   ;;; Management
     10.15.30.53/24     10.15.30.0      ether1
 1   ;;; Loopback
     192.0.2.104/32     192.0.2.104     loopback0
 2   ;;; To netsvr
     10.100.104.253/24  10.100.104.0    vlan104
 3   ;;; To routeros-02
     10.100.204.254/24  10.100.204.0    vlan204
 4 D 192.168.122.208/24 192.168.122.0   ether3

[admin@routeros-01] &gt; /ipv6 address print
Flags: X - disabled, I - invalid, D - dynamic, G - global, L - link-local
 #    ADDRESS                        FROM-POOL INTERFACE       ADVERTISE
 0  G ;;; To netsvr
      2001:db8:104::f/64                       vlan104         yes
 1  G ;;; To routeros-02
      2001:db8:204::a/64                       vlan204         yes
 2  G ;;; Loopback
      2001:db8:904:beef::1/128                 loopback0       no

! Show interface statuses and descriptions
[admin@routeros-01] &gt; /interface print
Flags: D - dynamic, X - disabled, R - running, S - slave
 #     NAME                                TYPE       ACTUAL-MTU L2MTU  MAX-L2MTU MAC-ADDRESS
 0  R  ;;; Management
       ether1                              ether            1500                  52:54:00:CA:DA:15
 1  R  ;;; VLAN Bridge
       ether2                              ether            1500                  52:54:00:C0:5D:55
 2  R  ;;; To the Internet
       ether3                              ether            1500                  52:54:00:30:EC:65
 3  R  ;;; Loopback
       loopback0                           bridge           1500 65535            42:E5:10:16:E9:17
 4  R  ;;; To netsvr
       vlan104                             vlan             1500                  52:54:00:C0:5D:55
 5  R  ;;; To routeros-02
       vlan204                             vlan             1500                  52:54:00:C0:5D:55

! Ping to netsvr-01 on IPv4 and IPv6
[admin@routeros-01] &gt; /ping 10.100.104.254
  SEQ HOST                                     SIZE TTL TIME  STATUS
    0 10.100.104.254                             56  64 0ms
    1 10.100.104.254                             56  64 0ms
    sent=2 received=2 packet-loss=0% min-rtt=0ms avg-rtt=0ms max-rtt=0ms

[admin@routeros-01] &gt; /ping 2001:db8:104::ffff
  SEQ HOST                                     SIZE TTL TIME  STATUS
    0 2001:db8:104::ffff                         56  64 0ms   echo reply
    1 2001:db8:104::ffff                         56  64 0ms   echo reply
    sent=2 received=2 packet-loss=0% min-rtt=0ms avg-rtt=0ms max-rtt=0ms

! Ping to routeros-02 on IPv4 and IPv6
[admin@routeros-01] &gt; /ping 10.100.204.253
  SEQ HOST                                     SIZE TTL TIME  STATUS
    0 10.100.204.253                             56  64 0ms
    1 10.100.204.253                             56  64 0ms
    2 10.100.204.253                             56  64 0ms
    sent=3 received=3 packet-loss=0% min-rtt=0ms avg-rtt=0ms max-rtt=0ms

[admin@routeros-01] &gt; /ping 2001:db8:204::f
  SEQ HOST                                     SIZE TTL TIME  STATUS
    0 2001:db8:204::f                            56  64 0ms   echo reply
    1 2001:db8:204::f                            56  64 0ms   echo reply
    2 2001:db8:204::f                            56  64 0ms   echo reply
    sent=3 received=3 packet-loss=0% min-rtt=0ms avg-rtt=0ms max-rtt=0ms
</code></pre></div><p><strong>routeros-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show IPs (IPv4 and IPv6)
[admin@routeros-02] &gt; /ip address print
Flags: X - disabled, I - invalid, D - dynamic
 #   ADDRESS            NETWORK         INTERFACE
 0   ;;; Management
     10.15.30.54/24     10.15.30.0      ether1
 1   ;;; Loopback
     192.0.2.204/32     192.0.2.204     loopback0
 2   ;;; To routeros-01
     10.100.204.253/24  10.100.204.0    vlan204

[admin@routeros-02] &gt; /ipv6 address print
Flags: X - disabled, I - invalid, D - dynamic, G - global, L - link-local
 #    ADDRESS                   FROM-POOL INTERFACE    ADVERTISE
 0  G ;;; Loopback
      2001:db8:904:beef::2/128            loopback0    no
 1  G ;;; To routeros-01
      2001:db8:204::f/64                  vlan204      yes


! Show interface statuses and descriptions
[admin@routeros-01] &gt; /interface print
Flags: D - dynamic, X - disabled, R - running, S - slave
 #     NAME                                TYPE       ACTUAL-MTU L2MTU  MAX-L2MTU MAC-ADDRESS
 0  R  ;;; Management
       ether1                              ether            1500                  52:54:00:CA:DA:15
 1  R  ;;; VLAN Bridge
       ether2                              ether            1500                  52:54:00:C0:5D:55
 2  R  ;;; To the Internet
       ether3                              ether            1500                  52:54:00:30:EC:65
 3  R  ;;; Loopback
       loopback0                           bridge           1500 65535            42:E5:10:16:E9:17
 4  R  ;;; To netsvr
       vlan104                             vlan             1500                  52:54:00:C0:5D:55
 5  R  ;;; To routeros-02
       vlan204                             vlan             1500                  52:54:00:C0:5D:55

! Ping to routeros-01 on IPv4 and IPv6
[admin@routeros-01] &gt; /ping 10.100.104.254
  SEQ HOST                                     SIZE TTL TIME  STATUS
    0 10.100.104.254                             56  64 0ms
    1 10.100.104.254                             56  64 0ms
    sent=2 received=2 packet-loss=0% min-rtt=0ms avg-rtt=0ms max-rtt=0ms

[admin@routeros-01] &gt; /ping 2001:db8:204::a
  SEQ HOST                                     SIZE TTL TIME  STATUS
    0 2001:db8:204::a                            56  64 0ms   echo reply
    1 2001:db8:204::a                            56  64 0ms   echo reply
    2 2001:db8:204::a                            56  64 0ms   echo reply
    sent=3 received=3 packet-loss=0% min-rtt=0ms avg-rtt=0ms max-rtt=0ms
</code></pre></div><h3 id="firewall">Firewall</h3>
<p>The next role goes through applying firewall filters between the <strong>edge</strong> router and the <strong>netsvr-01</strong> machine. RouterOS are well known for being capable firewall devices, with a lot of flexibility.</p>
<p>The firewall within RouterOS is stateful, so if you apply a rule in one direction, the return flow of traffic should also be matched by the same rule.</p>
<h4 id="playbook-2">Playbook</h4>
<p>The contents of the playbook are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for acl</span>
- <span style="color:#66d9ef">name</span>: Firewall Address Lists - BGP
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /ip firewall address-list add address=<span style="color:#e6db74">&#34;{{ item.peer }}&#34;</span> comment=<span style="color:#e6db74">&#34;{{ item.name }}&#34;</span> list=<span style="color:#e6db74">&#34;{{ item.acl }}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - rtr_role is search(<span style="color:#e6db74">&#34;edge&#34;</span>)
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv4 is defined
    - item.acl is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv4 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv4

- <span style="color:#66d9ef">name</span>: Firewall Address Lists - BGPv6
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /ipv6 firewall address-list add address=<span style="color:#e6db74">&#34;{{ item.peer }}&#34;</span> comment=<span style="color:#e6db74">&#34;{{ item.name }}&#34;</span> list=<span style="color:#e6db74">&#34;{{ item.acl }}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - rtr_role is search(<span style="color:#e6db74">&#34;edge&#34;</span>)
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv6 is defined
    - item.acl is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv6 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv6

- <span style="color:#66d9ef">name</span>: Firewall Address Lists - Services
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /ip firewall address-list add address=<span style="color:#e6db74">&#34;{{ log_host }}&#34;</span> comment=<span style="color:#e6db74">&#34;netsvr-01-syslog&#34;</span> list=<span style="color:#e6db74">&#34;syslog&#34;</span>
      - /ip firewall address-list add address=<span style="color:#e6db74">&#34;{{ tacacs[&#39;ipv4&#39;] }}&#34;</span> comment=<span style="color:#e6db74">&#34;netsvr-01-aaa&#34;</span> list=<span style="color:#e6db74">&#34;aaa&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - rtr_role is search(<span style="color:#e6db74">&#34;edge&#34;</span>)
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv4

- <span style="color:#66d9ef">name</span>: Firewall Filters - IPv4
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /ip firewall filter remove [find where comment~<span style="color:#e6db74">&#34;ICMP&#34;</span>]
      - /ip firewall filter add action=accept chain=input protocol=icmp comment=<span style="color:#e6db74">&#34;ICMP In&#34;</span>
      - /ip firewall filter add action=accept chain=output protocol=icmp comment=<span style="color:#e6db74">&#34;ICMP Out&#34;</span>
      - /ip firewall filter add action=accept chain=forward protocol=icmp comment=<span style="color:#e6db74">&#34;ICMP Forward&#34;</span>
      - /ip firewall filter remove [find where comment=<span style="color:#e6db74">&#34;BGP IPv4 Peers Inbound&#34;</span>]
      - /ip firewall filter remove [find where comment=<span style="color:#e6db74">&#34;BGP IPv4 Peers Outbound&#34;</span>]
      - /ip firewall filter add action=accept chain=input in-interface=<span style="color:#e6db74">&#34;{{ item.routeros_if }}&#34;</span> src-address-list=<span style="color:#e6db74">&#34;bgp-ipv4-peers&#34;</span> port=<span style="color:#e6db74">&#34;179&#34;</span> protocol=<span style="color:#e6db74">&#34;tcp&#34;</span> comment=<span style="color:#e6db74">&#34;BGP IPv4 Peers Inbound&#34;</span>
      - /ip firewall filter add action=accept chain=output out-interface=<span style="color:#e6db74">&#34;{{ item.routeros_if }}&#34;</span> dst-address-list=<span style="color:#e6db74">&#34;bgp-ipv4-peers&#34;</span> port=<span style="color:#e6db74">&#34;179&#34;</span> protocol=<span style="color:#e6db74">&#34;tcp&#34;</span> comment=<span style="color:#e6db74">&#34;BGP IPv4 Peers Outbound&#34;</span>
      - /ip firewall filter remove [find where comment~<span style="color:#e6db74">&#34;RADIUS&#34;</span>]
      - /ip firewall filter remove [find where comment~<span style="color:#e6db74">&#34;Syslog&#34;</span>]
      - /ip firewall filter add action=accept chain=input in-interface=<span style="color:#e6db74">&#34;{{ item.routeros_if }}&#34;</span> src-address-list=<span style="color:#e6db74">&#34;aaa&#34;</span> port=<span style="color:#e6db74">&#34;1812-1813&#34;</span> protocol=<span style="color:#e6db74">&#34;udp&#34;</span> comment=<span style="color:#e6db74">&#34;RADIUS Inbound&#34;</span>
      - /ip firewall filter add action=accept chain=output out-interface=<span style="color:#e6db74">&#34;{{ item.routeros_if }}&#34;</span> dst-address-list=<span style="color:#e6db74">&#34;syslog&#34;</span> port=<span style="color:#e6db74">&#34;514&#34;</span> protocol=<span style="color:#e6db74">&#34;udp&#34;</span> comment=<span style="color:#e6db74">&#34;Syslog Outbound&#34;</span>
      - /ip firewall filter add action=accept chain=output out-interface=<span style="color:#e6db74">&#34;{{ item.routeros_if }}&#34;</span> src-address-list=<span style="color:#e6db74">&#34;aaa&#34;</span> port=<span style="color:#e6db74">&#34;1812-1813&#34;</span> protocol=<span style="color:#e6db74">&#34;udp&#34;</span> comment=<span style="color:#e6db74">&#34;RADIUS Outbound&#34;</span>
      - /ip firewall filter add action=accept chain=forward in-interface=<span style="color:#e6db74">&#34;{{ item.routeros_if }}&#34;</span> src-address-list=<span style="color:#e6db74">&#34;aaa&#34;</span> port=<span style="color:#e6db74">&#34;1812-1813&#34;</span> protocol=<span style="color:#e6db74">&#34;udp&#34;</span> comment=<span style="color:#e6db74">&#34;RADIUS Forward Inbound&#34;</span>
      - /ip firewall filter add action=accept chain=forward out-interface=<span style="color:#e6db74">&#34;{{ item.routeros_if }}&#34;</span> dst-address-list=<span style="color:#e6db74">&#34;syslog&#34;</span> port=<span style="color:#e6db74">&#34;514&#34;</span> protocol=<span style="color:#e6db74">&#34;udp&#34;</span> comment=<span style="color:#e6db74">&#34;Syslog Forward Outbound&#34;</span>
      - /ip firewall filter add action=accept chain=forward out-interface=<span style="color:#e6db74">&#34;{{ item.routeros_if }}&#34;</span> dst-address-list=<span style="color:#e6db74">&#34;aaa&#34;</span> port=<span style="color:#e6db74">&#34;1812-1813&#34;</span> protocol=<span style="color:#e6db74">&#34;udp&#34;</span> comment=<span style="color:#e6db74">&#34;RADIUS Forward Outbound&#34;</span>
      - /ip firewall filter remove [find where comment~<span style="color:#e6db74">&#34;Drop all&#34;</span>]
      - /ip firewall filter add action drop in-interface=<span style="color:#e6db74">&#34;{{ item.routeros_if }}&#34;</span> chain=input comment=<span style="color:#e6db74">&#34;Drop all inbound&#34;</span>
      - /ip firewall filter add action drop out-interface=<span style="color:#e6db74">&#34;{{ item.routeros_if }}&#34;</span> chain=output comment=<span style="color:#e6db74">&#34;Drop all outbound&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - rtr_role is search(<span style="color:#e6db74">&#34;edge&#34;</span>)
    - item.acl is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv4

- <span style="color:#66d9ef">name</span>: Firewall Filters - IPv6
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /ipv6 firewall filter remove [find where comment~<span style="color:#e6db74">&#34;ICMP&#34;</span>]
      - /ipv6 firewall filter add action=accept chain=input protocol=icmpv6 comment=<span style="color:#e6db74">&#34;ICMPv6 In&#34;</span>
      - /ipv6 firewall filter add action=accept chain=output protocol=icmpv6 comment=<span style="color:#e6db74">&#34;ICMPv6 Out&#34;</span>
      - /ipv6 firewall filter add action=accept chain=forward protocol=icmpv6 comment=<span style="color:#e6db74">&#34;ICMPv6 Forward&#34;</span>
      - /ipv6 firewall filter remove [find where comment=<span style="color:#e6db74">&#34;BGP IPv6 Peers Inbound&#34;</span>]
      - /ipv6 firewall filter remove [find where comment=<span style="color:#e6db74">&#34;BGP IPv6 Peers Outbound&#34;</span>]
      - /ipv6 firewall filter add action=accept chain=input in-interface=<span style="color:#e6db74">&#34;{{ item.routeros_if }}&#34;</span> src-address-list=<span style="color:#e6db74">&#34;bgp-ipv6-peers&#34;</span> port=<span style="color:#e6db74">&#34;179&#34;</span> protocol=<span style="color:#e6db74">&#34;tcp&#34;</span> comment=<span style="color:#e6db74">&#34;BGP IPv6 Peers Inbound&#34;</span>
      - /ipv6 firewall filter add action=accept chain=output out-interface=<span style="color:#e6db74">&#34;{{ item.routeros_if }}&#34;</span> dst-address-list=<span style="color:#e6db74">&#34;bgp-ipv6-peers&#34;</span> port=<span style="color:#e6db74">&#34;179&#34;</span> protocol=<span style="color:#e6db74">&#34;tcp&#34;</span> comment=<span style="color:#e6db74">&#34;BGP IPv6 Peers Outbound&#34;</span>
      - /ipv6 firewall filter remove [find where comment~<span style="color:#e6db74">&#34;Drop all&#34;</span>]
      - /ipv6 firewall filter add action drop in-interface=<span style="color:#e6db74">&#34;{{ item.routeros_if }}&#34;</span> chain=input comment=<span style="color:#e6db74">&#34;Drop all inbound&#34;</span>
      - /ipv6 firewall filter add action drop out-interface=<span style="color:#e6db74">&#34;{{ item.routeros_if }}&#34;</span> chain=output comment=<span style="color:#e6db74">&#34;Drop all outbound&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - rtr_role is search(<span style="color:#e6db74">&#34;edge&#34;</span>)
    - item.acl is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv6
</code></pre></div><p>This role is the a good example  of running a set of commands from the playbook. Other than defining a few variables, most of the commands are as you would apply them yourself to RouterOS.</p>
<h5 id="firewall-address-lists---bgp">Firewall Address Lists - BGP</h5>
<p>A firewall address list in RouterOS is a list of addresses (or subnets) that are logically grouped together. This allows you to apply rules to a group of addresses, negating the need to duplicate them for every host/range.</p>
<p>This task goes through our BGP peers. For every IPv4 BGP peer we have defined that has the <code>acl</code> field, it will add them to the address list named in the <code>acl</code> field. The relevant <code>host_vars</code> are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">bgp</span>:
    <span style="color:#66d9ef">ipv4</span>:
     - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">10.100.104.254</span>
       <span style="color:#66d9ef">acl</span>: bgp-ipv4-peers
       <span style="color:#66d9ef">name</span>: netsvr<span style="color:#ae81ff">-01</span>-v4
     - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">192.0.2.204</span>
       <span style="color:#66d9ef">name</span>: routeros<span style="color:#ae81ff">-02</span>-v4
</code></pre></div><p>As we can see above, only one peer has the <code>acl</code> field defined. The generated configuration is therefore: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/ip firewall address-list add address=10.100.104.254 comment=netsvr-01-v4 list=bgp-ipv4-peers
</code></pre></div><p>We can verify this with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-01] &gt; /ip firewall address-list print
Flags: X - disabled, D - dynamic
 #   LIST               ADDRESS             CREATION-TIME        TIMEOUT
 0   ;;; netsvr-01-v4
     bgp-ipv4-peers     10.100.104.254      apr/26/2020 22:54:50
</code></pre></div><p>We only have one peer in here, but if you had multiple peers (say, on an Internet Exchange port) using lists makes firewall rules much simpler and easier to maintain.</p>
<h5 id="firewall-address-lists---bgp-ipv6">Firewall Address Lists - BGP IPv6</h5>
<p>This task is identical to the above, except it is for IPv6 BGP peers.</p>
<p>The relevant <code>host_vars</code> are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">bgp</span>:
    <span style="color:#66d9ef">ipv6</span>:
     - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:104::ffff&#34;</span>
       <span style="color:#66d9ef">acl</span>: bgp-ipv6-peers
       <span style="color:#66d9ef">name</span>: netsvr<span style="color:#ae81ff">-01</span>-v6
     - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:904:beef::2&#34;</span>
       <span style="color:#66d9ef">name</span>: routeros<span style="color:#ae81ff">-02</span>-v6
</code></pre></div><p>Again, only one peer has the <code>acl</code> field defined. The generated configuration is therefore: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/ipv6 firewall address-list add address=2001:db8:104::ffff/128 comment=netsvr-01-v6 list=bgp-ipv6-peers
</code></pre></div><p>We can verify this with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-01] &gt; /ipv6 firewall address-list print
Flags: X - disabled, D - dynamic
 #   LIST                  ADDRESS                       TIMEOUT
 0   ;;; netsvr-01-v6
     bgp-ipv6-peers        2001:db8:104::ffff/128
</code></pre></div><h5 id="firewall-address-lists---services">Firewall Address Lists - Services</h5>
<p>This address list configures the Syslog and RADIUS/AAA address lists. In our scenario, RADIUS and Syslog are on the same virtual machine, but in most networks they would typically be separated.</p>
<p>We retrieve the Syslog and RADIUS host from our <code>group_vars</code>, which is: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">log_host</span>: <span style="color:#ae81ff">10.100.104.254</span>
<span style="color:#66d9ef">tacacs</span>:
  <span style="color:#66d9ef">ipv4</span>: <span style="color:#ae81ff">192.0.2.1</span>
</code></pre></div><p>Why are we using the <code>tacacs</code> variable for our RADIUS host? Because if and when RouterOS supports TACACS+, this would be the preferred method. Rather than changing our variables, we will keep them as generic as possible, and change the roles at a later date to accommodate TACACS+.</p>
<p>This generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/ip firewall address-list add address=10.100.104.254 comment=netsvr-01-syslog list=syslog
/ip firewall address-list add address=192.0.2.1 comment=netsvr-01-aaa list=aaa
</code></pre></div><p>We can verify this with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-01] &gt; /ip firewall address-list print
Flags: X - disabled, D - dynamic
 #   LIST                       ADDRESS               CREATION-TIME        TIMEOUT
 0   ;;; netsvr-01-v4
     bgp-ipv4-peers             10.100.104.254        apr/26/2020 22:54:50
 1   ;;; netsvr-01-syslog
     syslog                     10.100.104.254        apr/26/2020 23:00:52
 2   ;;; netsvr-01-aaa
     aaa                        192.0.2.1             apr/26/2020 23:00:52
</code></pre></div><h5 id="firewall-filters---ipv4">Firewall Filters - IPv4</h5>
<p>This task builds the firewall filters for the IPv4 traffic between the <strong>edge</strong> router and the <strong>netsvr-01</strong> machine. To do this, it loops through our interfaces, and if they have the <code>acl</code> field defined, rules are created bound to that interface (as well as some general rules).</p>
<p>The relevant <code>host_vars</code> are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;ether1&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;ether2&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;vlan104&#34;</span>
    <span style="color:#66d9ef">acl</span>:
      <span style="color:#66d9ef">ipv4</span>:
        - bgp-ipv4-peers
        - syslog
        - aaa
      <span style="color:#66d9ef">ipv6</span>:
        - bgp-ipv6-peers
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;vlan204&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;ether3&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;loopback0&#34;</span>
</code></pre></div><p>The <code>host_vars</code> do reference the names of filters being applied, but I did not build the logic to build the filters per interface. This is something which I will improve upon at a later date.</p>
<p>Also notice that we are removing rules and then reapplying them. This is so that the rules are up to date, and also so that rules do not get placed after the &ldquo;drop all&rdquo; rule and never take effect.</p>
<p>The generated configuration looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/ip firewall filter add action=accept chain=input comment=&#34;ICMP In&#34; protocol=icmp
/ip firewall filter add action=accept chain=output comment=&#34;ICMP Out&#34; protocol=icmp
/ip firewall filter add action=accept chain=forward comment=&#34;ICMP Forward&#34; protocol=icmp
/ip firewall filter add action=accept chain=input comment=&#34;BGP IPv4 Peers Inbound&#34; in-interface=vlan104 port=179 protocol=tcp src-address-list=bgp-ipv4-peers
/ip firewall filter add action=accept chain=output comment=&#34;BGP IPv4 Peers Outbound&#34; dst-address-list=bgp-ipv4-peers out-interface=vlan104 port=179 protocol=tcp
/ip firewall filter add action=accept chain=input comment=&#34;RADIUS Inbound&#34; in-interface=vlan104 port=1812-1813 protocol=udp src-address-list=aaa
/ip firewall filter add action=accept chain=output comment=&#34;Syslog Outbound&#34; dst-address-list=syslog out-interface=vlan104 port=514 protocol=udp
/ip firewall filter add action=accept chain=output comment=&#34;RADIUS Outbound&#34; out-interface=vlan104 port=1812-1813 protocol=udp src-address-list=aaa
/ip firewall filter add action=accept chain=forward comment=&#34;RADIUS Forward Inbound&#34; in-interface=vlan104 port=1812-1813 protocol=udp src-address-list=aaa
/ip firewall filter add action=accept chain=forward comment=&#34;Syslog Forward Outbound&#34; dst-address-list=syslog out-interface=vlan104 port=514 protocol=udp
/ip firewall filter add action=accept chain=forward comment=&#34;RADIUS Forward Outbound&#34; dst-address-list=aaa out-interface=vlan104 port=1812-1813 protocol=udp
/ip firewall filter add action=drop chain=input comment=&#34;Drop all inbound&#34; in-interface=vlan104
/ip firewall filter add action=drop chain=output comment=&#34;Drop all outbound&#34; out-interface=vlan104
</code></pre></div><p>We can verify this with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-01] &gt; /ip firewall filter print
Flags: X - disabled, I - invalid, D - dynamic
 0    ;;; ICMP In
      chain=input action=accept protocol=icmp

 1    ;;; ICMP Out
      chain=output action=accept protocol=icmp

 2    ;;; ICMP Forward
      chain=forward action=accept protocol=icmp

 3    ;;; BGP IPv4 Peers Inbound
      chain=input action=accept protocol=tcp src-address-list=bgp-ipv4-peers in-interface=vlan104 port=179

 4    ;;; BGP IPv4 Peers Outbound
      chain=output action=accept protocol=tcp dst-address-list=bgp-ipv4-peers out-interface=vlan104 port=179

 5    ;;; RADIUS Inbound
      chain=input action=accept protocol=udp src-address-list=aaa in-interface=vlan104 port=1812-1813

 6    ;;; Syslog Outbound
      chain=output action=accept protocol=udp dst-address-list=syslog out-interface=vlan104 port=514

 7    ;;; RADIUS Outbound
      chain=output action=accept protocol=udp src-address-list=aaa out-interface=vlan104 port=1812-1813

 8    ;;; RADIUS Forward Inbound
      chain=forward action=accept protocol=udp src-address-list=aaa in-interface=vlan104 port=1812-1813

 9    ;;; Syslog Forward Outbound
      chain=forward action=accept protocol=udp dst-address-list=syslog out-interface=vlan104 port=514

10    ;;; RADIUS Forward Outbound
      chain=forward action=accept protocol=udp dst-address-list=aaa out-interface=vlan104 port=1812-1813

11    ;;; Drop all inbound
      chain=input action=drop in-interface=vlan104

12    ;;; Drop all outbound
      chain=output action=drop out-interface=vlan104
</code></pre></div><h5 id="firewall-filters---ipv6">Firewall Filters - IPv6</h5>
<p>This task is identical to the above task, except it is for IPv6 firewall filters. The same <code>host_vars</code> are used too. The major difference is that we do not talk to RADIUS and Syslog over IPv6, so we do not create filters for them. The generated configuration looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/ipv6 firewall filter add action=accept chain=input comment=&#34;ICMPv6 In&#34; protocol=icmpv6
/ipv6 firewall filter add action=accept chain=output comment=&#34;ICMPv6 Out&#34; protocol=icmpv6
/ipv6 firewall filter add action=accept chain=forward comment=&#34;ICMPv6 Forward&#34; protocol=icmpv6
/ipv6 firewall filter add action=accept chain=input comment=&#34;BGP IPv6 Peers Inbound&#34; in-interface=vlan104 port=179 protocol=tcp src-address-list=bgp-ipv6-peers
/ipv6 firewall filter add action=accept chain=output comment=&#34;BGP IPv6 Peers Outbound&#34; dst-address-list=bgp-ipv6-peers out-interface=vlan104 port=179 protocol=tcp
/ipv6 firewall filter add action=drop chain=input comment=&#34;Drop all inbound&#34; in-interface=vlan104
/ipv6 firewall filter add action=drop chain=output comment=&#34;Drop all outbound&#34; out-interface=vlan104
</code></pre></div><p>We can verify this with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-01] &gt; /ipv6 firewall filter print
Flags: X - disabled, I - invalid, D - dynamic
 0    ;;; ICMPv6 In
      chain=input action=accept protocol=icmpv6

 1    ;;; ICMPv6 Out
      chain=output action=accept protocol=icmpv6

 2    ;;; ICMPv6 Forward
      chain=forward action=accept protocol=icmpv6

 3    ;;; BGP IPv6 Peers Inbound
      chain=input action=accept protocol=tcp src-address-list=bgp-ipv6-peers in-interface=vlan104 port=179

 4    ;;; BGP IPv6 Peers Outbound
      chain=output action=accept protocol=tcp dst-address-list=bgp-ipv6-peers out-interface=vlan104 port=179

 5    ;;; Drop all inbound
      chain=input action=drop in-interface=vlan104

 6    ;;; Drop all outbound
      chain=output action=drop out-interface=vlan104
</code></pre></div><h4 id="verification-3">Verification</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show the hit count on the policies
[admin@routeros-01] &gt; /ip firewall filter print stats
Flags: X - disabled, I - invalid, D - dynamic
 #    CHAIN                            ACTION             BYTES         PACKETS
 0    ;;; ICMP In
      input                            accept                 0               0
 1    ;;; ICMP Out
      output                           accept                 0               0
 2    ;;; ICMP Forward
      forward                          accept               159               1
 3    ;;; BGP IPv4 Peers Inbound
      input                            accept               790              13
 4    ;;; BGP IPv4 Peers Outbound
      output                           accept               809              13
 5    ;;; RADIUS Inbound
      input                            accept                 0               0
 6    ;;; Syslog Outbound
      output                           accept             1 270              15
 7    ;;; RADIUS Outbound
      output                           accept                 0               0
 8    ;;; RADIUS Forward Inbound
      forward                          accept               386               3
 9    ;;; Syslog Forward Outbound
      forward                          accept               424               4
10    ;;; RADIUS Forward Outbound
      forward                          accept               820               5
11    ;;; Drop all inbound
      input                            drop                   0               0
12    ;;; Drop all outbound
      output                           drop                   0               0

[admin@routeros-01] &gt; /ipv6 firewall filter print stats
Flags: X - disabled, I - invalid, D - dynamic
 #    CHAIN                            ACTION             BYTES         PACKETS
 0    ;;; ICMPv6 In
      input                            accept             2 504              40
 1    ;;; ICMPv6 Out
      output                           accept             2 080              29
 2    ;;; ICMPv6 Forward
      forward                          accept                 0               0
 3    ;;; BGP IPv6 Peers Inbound
      input                            accept             1 122              14
 4    ;;; BGP IPv6 Peers Outbound
      output                           accept             1 160              14
 5    ;;; Drop all inbound
      input                            drop                   0               0
 6    ;;; Drop all outbound
      output                           drop                   0               0

! What happens if we try to SSH from the netsvr?
[stuh84@netsvr-01 ~] $ ssh admin@10.100.104.253

! Can we still ping it?
[stuh84@netsvr-01 ~] $ ping 10.100.104.253
PING 10.100.104.253 (10.100.104.253) 56(84) bytes of data.
64 bytes from 10.100.104.253: icmp_seq=1 ttl=64 time=0.591 ms
64 bytes from 10.100.104.253: icmp_seq=2 ttl=64 time=0.670 ms
64 bytes from 10.100.104.253: icmp_seq=3 ttl=64 time=0.675 ms
^C
--- 10.100.104.253 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 41ms
rtt min/avg/max/mdev = 0.591/0.645/0.675/0.043 ms
</code></pre></div><p>All looks good!</p>
<h3 id="routing">Routing</h3>
<p>For routing, we are using BGP for external network connectivity, as well as OSPF for IPv4 and OSPFv3 for IPv6. Unlike other vendors that have IPv4 support for OSPFv3, MikroTik only support IPv6 routes and addressing in OSPFv3. This is why I have chosen to configure both in each article in this series, so that those using mixed-vendor networks can integrate them together without needing to reconfigure their core routing protocols.</p>
<h4 id="main-playbook">Main Playbook</h4>
<p>As with the others in this series, the main playbook is used to include other playbooks: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for routing</span>
<span style="color:#75715e">##</span>
- <span style="color:#66d9ef">name</span>: Include OSPF routing
  <span style="color:#66d9ef">include</span>: ospf.yml

- <span style="color:#66d9ef">name</span>: Include OSPFv3 routing
  <span style="color:#66d9ef">include</span>: ospfv3.yml

- <span style="color:#66d9ef">name</span>: Include BGP routing
  <span style="color:#66d9ef">include</span>: bgp.yml
</code></pre></div><h4 id="ospf-playbook">OSPF Playbook</h4>
<p>The contents of the OSPF Playbook are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for routing</span>
<span style="color:#75715e">##</span>
- <span style="color:#66d9ef">name</span>: OSPF Process - Router ID
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /routing ospf instance set <span style="color:#ae81ff">0</span> router-id=<span style="color:#e6db74">&#34;{{ router_id }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf

- <span style="color:#66d9ef">name</span>: OSPF Interfaces - Networks
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /routing ospf network add network=<span style="color:#e6db74">&#34;{{ item.ipv4 | ipaddr(&#39;network/prefix&#39;) }}&#34;</span> area=backbone
  <span style="color:#66d9ef">when</span>: item.ospf is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf

- <span style="color:#66d9ef">name</span>: OSPF Interfaces - Passive
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /routing ospf interface add passive=yes interface=<span style="color:#e6db74">&#34;{{ item.routeros_if }}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - item.ospf is defined
    - item.ospf.passive is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf

- <span style="color:#66d9ef">name</span>: OSPF Interfaces - Non-passive
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /routing ospf interface set [find where interface=<span style="color:#e6db74">&#34;{{ item.routeros_if }}&#34;</span>] passive=no
  <span style="color:#66d9ef">when</span>:
    - item.ospf is defined
    - item.ospf.passive is not defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf

<span style="color:#75715e">## Remove undefined networks</span>

- <span style="color:#66d9ef">name</span>: Retrieve configured OSPF networks
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /routing ospf network print
  <span style="color:#66d9ef">register</span>: ospf_network_result
  <span style="color:#66d9ef">tags</span>:
    - ospf

- <span style="color:#66d9ef">name</span>: Generate list of configured OSPF networks
  <span style="color:#66d9ef">set_fact</span>:
    <span style="color:#66d9ef">ospf_configured_networks</span>: <span style="color:#e6db74">&#34;{{ ospf_configured_networks|default([]) + [ item | regex_findall(&#39;\\b(?:[0-9]{1,3}\\.){3}[0-9]{1,3}/[1-3]{0,1}[0-9]\\b&#39;) ] }}&#34;</span>
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ ospf_network_result.stdout_lines[0] | reject(&#39;search&#39;, &#39;Flags&#39;) | reject(&#39;search&#39;, &#39;AREA&#39;) | list }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf

- <span style="color:#66d9ef">name</span>: Generate list of defined OSPF networks
  <span style="color:#66d9ef">set_fact</span>:
    <span style="color:#66d9ef">ospf_defined_networks</span>: <span style="color:#e6db74">&#34;{{ ospf_defined_networks|default([]) + [ item.ipv4 | ipaddr(&#39;network/prefix&#39;) ] }}&#34;</span>
  <span style="color:#66d9ef">when</span>: item.ospf is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf

- <span style="color:#66d9ef">name</span>: Create list of networks to delete
  <span style="color:#66d9ef">set_fact</span>:
    <span style="color:#66d9ef">ospf_delete_networks</span>: <span style="color:#e6db74">&#34;{{ ospf_delete_networks|default([]) + [ item ] }}&#34;</span>
  <span style="color:#66d9ef">when</span>: ospf_defined_networks is not search(item)
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ ospf_configured_networks | flatten }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf

- <span style="color:#66d9ef">name</span>: Delete undefined networks
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /routing ospf network remove [find where network=<span style="color:#e6db74">&#34;{{ item }}&#34;</span>]
  <span style="color:#66d9ef">when</span>: ospf_delete_networks is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ ospf_delete_networks }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf

<span style="color:#75715e">## Remove undefined interfaces</span>

- <span style="color:#66d9ef">name</span>: Gather facts from Router
  <span style="color:#66d9ef">routeros_facts</span>:
    <span style="color:#66d9ef">gather_subset</span>:
      - interfaces
  <span style="color:#66d9ef">register</span>: mikro_facts
  <span style="color:#66d9ef">tags</span>:
    - ospf

- <span style="color:#66d9ef">name</span>: Find configured interfaces
  <span style="color:#66d9ef">set_fact</span>:
    <span style="color:#66d9ef">configured_interfaces</span>: <span style="color:#e6db74">&#34;{{ configured_interfaces| default([]) + [ item.key ] }}&#34;</span>
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ lookup(&#39;dict&#39;, mikro_facts.ansible_facts.ansible_net_interfaces) }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf

- <span style="color:#66d9ef">name</span>: List of defined OSPF interfaces
  <span style="color:#66d9ef">set_fact</span>:
    <span style="color:#66d9ef">ospf_interfaces</span>: <span style="color:#e6db74">&#34;{{ ospf_interfaces| default([]) + [ item.routeros_if ]}}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - item.ospf is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf

- <span style="color:#66d9ef">name</span>: List of non-OSPF interfaces
  <span style="color:#66d9ef">set_fact</span>:
    <span style="color:#66d9ef">non_ospf_interfaces</span>: <span style="color:#e6db74">&#34;{{ non_ospf_interfaces|default([]) + [ item ] }}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - ospf_interfaces is not search(item)
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ configured_interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf

- <span style="color:#66d9ef">name</span>: Remove OSPF from non-OSPF interfaces
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /routing ospf interface remove [find where interface=<span style="color:#e6db74">&#34;{{ item }}&#34;</span>]
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ non_ospf_interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf
</code></pre></div><p>Compared to some of the other vendors, there are significantly more tasks in this playbook. Again, because no modules other than <strong>routeros_command</strong> exist for RouterOS, any idempotence (i.e. repeatable configuration) is achieved through RouterOS CLI features and commands.</p>
<h5 id="ospf-router-id">OSPF Router ID</h5>
<p>In this task, we set the OSPF Router ID. This is so that the router has a static (and predictable) Router ID. The importance of this is that changing the Router ID of an OSPF process can, in some cases, reset OSPF neighbourships. Router IDs are usually determined by the highest number IP address on an active interface. If the interface with that IP becomes inactive, the Router ID could change.</p>
<p>The <code>host_vars</code> we use for this are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">router_id</span>: <span style="color:#ae81ff">192.0.2.104</span>
</code></pre></div><p>This generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/routing ospf instance set [ find default=yes ] router-id=192.0.2.104
</code></pre></div><p>We can verify this with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-01] &gt; /routing ospf instance print value-list
                     name: default
                     router-id: 192.0.2.104
</code></pre></div><h5 id="ospf-interfaces---networks">OSPF Interfaces - Networks</h5>
<p>This task goes through our list of interfaces, and for any that have the OSPF field, we take their IPv4 address, convert it into a <strong>network</strong> address (i.e. the first address in the subnet) and then add it to OSPF. Unlike BGP, networks in OSPF are used to include interfaces in your OSPF routing domain, rather than generating routes for advertisement. For example, you could have a network statement of <code>0.0.0.0/0</code> which would include all of your interfaces (because this covers every subnet), but it would not advertise out <code>0.0.0.0/0</code> as a route.</p>
<p>The relevant <code>host_vars</code> for this task are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;ether1&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;ether2&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;vlan104&#34;</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.100.104.253/24&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;vlan204&#34;</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.100.204.254/24&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;ether3&#34;</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;dhcp&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;loopback0&#34;</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;192.0.2.104/32&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
</code></pre></div><p>In the above, the interfaces that have an OSPF field are <code>vlan104</code>, <code>vlan204</code> and <code>loopback0</code>. Therefore the configuration that is generated is: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/routing ospf network add area=backbone network=10.100.104.0/24
/routing ospf network add area=backbone network=10.100.204.0/24
/routing ospf network add area=backbone network=192.0.2.104/32
</code></pre></div><p>We can verify this with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-01] &gt; /routing ospf network print
Flags: X - disabled, I - invalid
 #   NETWORK            AREA
 0   10.100.104.0/24    backbone
 1   10.100.204.0/24    backbone
 2   192.0.2.104/32     backbone
</code></pre></div><p>The <code>backbone</code> area is the default OSPF area (known as either <code>area 0</code> in IOS or <code>area 0.0.0.0</code> on most other vendors). This task does not configure interfaces outside of the backbone area, as for our purposes we are not using them.</p>
<h5 id="ospf-interfaces---passive">OSPF Interfaces - Passive</h5>
<p>This task configures which interfaces will run in passive mode (i.e. the subnet they have is advertised, but no OSPF neighbours will be negotiated over this interface). The relevant <code>host_vars</code> for this task are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;vlan104&#34;</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.100.104.253/24&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;vlan204&#34;</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.100.204.254/24&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;loopback0&#34;</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;192.0.2.104/32&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
</code></pre></div><p>This will generated the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/routing ospf interface add interface=loopback0 passive=yes
/routing ospf interface add interface=vlan104 passive=yes
</code></pre></div><p>We can verify this with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-01] &gt; /routing ospf interface print
Flags: X - disabled, I - inactive, D - dynamic, P - passive
 #    INTERFACE         COST PRIORITY NETWORK-TYPE   AUTHENTICATION AUTHENTICATION-KEY
 0  P loopback0           10        1 default        none
 1  P vlan104             10        1 default        none
</code></pre></div><h5 id="ospf-interfaces---non-passive">OSPF Interfaces - Non-Passive</h5>
<p>This task does the same as the above, except that it configures all interfaces that are not passive. This is done because if we change an interface from <strong>passive</strong> to <strong>non-passive</strong>, we cannot just remove the <code>passive</code> declaration. Instead it must be set explicitly.</p>
<p>This does mean that all interfaces running OSPF will have a declaration in <code>/routing ospf interfaces</code>, even though technically not all are required. Without this, we cannot change the <code>passive</code> state of an interface.</p>
<p>The same <code>host_vars</code> from the previous task apply, which generate the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/routing ospf interface add interface=vlan204 passive=no
</code></pre></div><p>We can verify this with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-01] &gt; /routing ospf interface print where passive=no
Flags: X - disabled, I - inactive, D - dynamic, P - passive
 #    INTERFACE       COST PRIORITY NETWORK-TYPE   AUTHENTICATION AUTHENTICATION-KEY
 0    vlan204           10        1 default        none
</code></pre></div><h5 id="removing-undefined-networks">Removing Undefined Networks</h5>
<p>To remove undefined networks from MikroTiks, we use a series of tasks to gather information from our <code>host_vars</code> and the running configuration. The two are then compared. Any network statements that are configured on the router but not defined in our <code>host_vars</code> are removed.</p>
<h6 id="retrieve-configured-ospf-networks">Retrieve configured OSPF networks</h6>
<p>The first task runs <code>/routing ospf network print</code> and stores it in a variable called <code>ospf_network_result</code>. The output of <code>/routing ospf network print</code> looks something like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-01] &gt; /routing ospf network print
Flags: X - disabled, I - invalid
 #   NETWORK            AREA
 0   10.100.104.0/24    backbone
 1   10.100.204.0/24    backbone
 2   192.0.2.104/32     backbone
 3   169.254.1.1/32     backbone
</code></pre></div><p>For demonstration purposes I have added the <code>169.254.1.1/32</code> network, which does not exist in our <code>host_vars</code>.</p>
<h6 id="generate-list-of-configured-ospf-networks">Generate list of configured OSPF networks</h6>
<p>Firstly, this task generates a list for us to loop through, by doing the following: -</p>
<ul>
<li>Looks at the output of <code>ospf_network_result</code>, looking for the field <code>stdout_lines</code>, retrieving only the first value in the list</li>
<li>If any line contains the word <strong>flags</strong>, we ignore it (i.e. the <code>Flags: X - disabled, I - invalid</code> line)</li>
<li>If any line contains the word <strong>area</strong>, we also ignore it (i.e. the <code> #   NETWORK            AREA</code> line)</li>
<li>Converts the results to a list</li>
</ul>
<p>Once the above is complete, we loop through the results, and do the following on each iteration: -</p>
<ul>
<li>Create the list <code>ospf_configured_networks</code> with a default value of <code>[]</code> (i.e. an empty list), if <code>ospf_configured_networks</code> does not already exist</li>
<li>If it does already exist, then retrieve the list from the previous iteration of the loop</li>
<li>Run a regular expression to find an IPv4 network address in each item in our loop, and add it to the <code>ospf_configured_networks</code> list</li>
</ul>
<p>If we do not supply a default value for the <code>ospf_configured_networks</code>, it will not exist on the first iteration of the loop. This will cause the task will fail. Subsequent iterations will not need a default value, as the list already exists.</p>
<p>All of the above doesn&rsquo;t make a lot of sense without seeing it in action, so the below shows what happens at each state: -</p>
<p><strong>Contents the ospf_network_result variable</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ok: [routeros-01] =&gt; {
    &#34;ospf_network_result&#34;: {
        &#34;changed&#34;: false,
        &#34;failed&#34;: false,
        &#34;stdout&#34;: [
            &#34;Flags: X - disabled, I - invalid \n #   NETWORK            AREA                                                   \n 0   10.100.104.0/24    backbone                                               \n 1   10.100.204.0/24    backbone                                               \n 2   192.0.2.104/32     backbone                                               \n 3   169.254.1.1/32     backbone&#34;
        ],
        &#34;stdout_lines&#34;: [
            [
                &#34;Flags: X - disabled, I - invalid &#34;,
                &#34; #   NETWORK            AREA                                                   &#34;,
                &#34; 0   10.100.104.0/24    backbone                                               &#34;,
                &#34; 1   10.100.204.0/24    backbone                                               &#34;,
                &#34; 2   192.0.2.104/32     backbone                                               &#34;,
                &#34; 3   169.254.1.1/32     backbone&#34;
            ]
        ]
    }
}
</code></pre></div><p><strong>Retrieving the stdout_lines section</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ok: [routeros-01] =&gt; {
    &#34;ospf_network_result.stdout_lines&#34;: [
        [
            &#34;Flags: X - disabled, I - invalid &#34;,
            &#34; #   NETWORK            AREA                                                   &#34;,
            &#34; 0   10.100.104.0/24    backbone                                               &#34;,
            &#34; 1   10.100.204.0/24    backbone                                               &#34;,
            &#34; 2   192.0.2.104/32     backbone                                               &#34;,
            &#34; 3   169.254.1.1/32     backbone&#34;
        ]
    ]
}
</code></pre></div><p>Notice in the above we have a list within a list. We do not require a nested list here, so we retrieve the first element of it</p>
<p><strong>Retrieve first element in the list</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ok: [routeros-01] =&gt; {
    &#34;ospf_network_result.stdout_lines[0]&#34;: [
        &#34;Flags: X - disabled, I - invalid &#34;,
        &#34; #   NETWORK            AREA                                                   &#34;,
        &#34; 0   10.100.104.0/24    backbone                                               &#34;,
        &#34; 1   10.100.204.0/24    backbone                                               &#34;,
        &#34; 2   192.0.2.104/32     backbone                                               &#34;,
        &#34; 3   169.254.1.1/32     backbone&#34;
    ]
}
</code></pre></div><p><strong>Ignore lines with the word Flags</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ok: [routeros-01] =&gt; {
    &#34;ospf_network_result.stdout_lines[0] | reject(&#39;search&#39;, &#39;Flags&#39;) | list&#34;: [
        &#34; #   NETWORK            AREA                                                   &#34;,
        &#34; 0   10.100.104.0/24    backbone                                               &#34;,
        &#34; 1   10.100.204.0/24    backbone                                               &#34;,
        &#34; 2   192.0.2.104/32     backbone                                               &#34;,
        &#34; 3   169.254.1.1/32     backbone&#34;
    ]
}
</code></pre></div><p><strong>Ignore lines with the word AREA</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ok: [routeros-01] =&gt; {
    &#34;ospf_network_result.stdout_lines[0] | reject(&#39;search&#39;, &#39;Flags&#39;) | reject(&#39;search&#39;, &#39;AREA&#39;) | list&#34;: [
        &#34; 0   10.100.104.0/24    backbone                                               &#34;,
        &#34; 1   10.100.204.0/24    backbone                                               &#34;,
        &#34; 2   192.0.2.104/32     backbone                                               &#34;,
        &#34; 3   169.254.1.1/32     backbone&#34;
    ]
}
</code></pre></div><p>Our regular expression that matches the networks (in CIDR format) is <code>\\b(?:[0-9]{1,3}\\.){3}[0-9]{1,3}/[1-3]{0,1}[0-9]\\b</code>. To explain what this does: -</p>
<ul>
<li><code>\\b</code> - Whole words/strings only, not a match within a word</li>
<li>The <code>(?:</code> and <code>)</code> section creates a <strong>non-capturing</strong> group, allowing us to repeat the expression inside it without needing to necessarily &ldquo;save&rdquo; the results
<ul>
<li>A capturing group could be used to replace sections later if required, but we do not need this behaviour</li>
</ul>
</li>
<li>The <code>[0-9]{1,3}</code> section says that we are looking for numbers between 0-9 between 1 and 3 times. This will match up to 255</li>
<li>Technically this could also match up to 999, but IP addresses do not go that high!</li>
<li>The <code>{3}</code> matches the previous expression exactly 3 times</li>
<li>The second use of <code>[0-9]{1,3}</code> matches the last octet of an IP address</li>
<li>The <code>/[1-3]{0,1}[0-9]</code> matches the subnet mask, saying that the we could have a number from 1 to 3, either zero or once, and any number from 0 to 9
<ul>
<li>This is because subnet masks can be anything from <code>/0</code> to <code>/32</code></li>
</ul>
</li>
</ul>
<p>Thankfully, this expression was already available within the <a href="https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html#regular-expression-filters">Ansible Regular Expression Filters</a> documentation. Without it, I would have probably used something much simpler (and potentially less likely to match the values!).</p>
<p>To sum up, this regular expression extracts <strong>only</strong> the subnet addresses that are in the list generated when we loop through our <code>ospf_network_result</code>. While we could run this without ignoring the lines containing <strong>AREA</strong> or <strong>Flags</strong>, this would add empty values to our list, which are unnecessary and could lead to commands failing later with empty values.</p>
<p>The list generated by this task is: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ok: [routeros-01] =&gt; {
    &#34;ospf_configured_networks&#34;: [
        [
            &#34;10.100.104.0/24&#34;
        ],
        [
            &#34;10.100.204.0/24&#34;
        ],
        [
            &#34;192.0.2.104/32&#34;
        ],
        [
            &#34;169.254.1.1/32&#34;
        ]
    ]
}
</code></pre></div><h6 id="gathering-list-of-defined-networks">Gathering list of defined networks</h6>
<p>This task loops through our <code>host_vars</code> and generates a list of the OSPF networks we have defined. This would then generate: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ok: [routeros-01] =&gt; {
    &#34;ospf_defined_networks&#34;: [
        &#34;10.100.104.0/24&#34;,
        &#34;10.100.204.0/24&#34;,
        &#34;192.0.2.104/32&#34;
    ]
}
</code></pre></div><h6 id="create-list-of-networks-to-delete">Create list of networks to delete</h6>
<p>First, this task takes our list of networks that are configured on the router and <strong>flattens</strong> it. Flattening a list means that rather than having lists inside of lists (nested list), every value is part one combined list instead. This is required because the task to generate the <code>ospf_configured_networks</code> variable created a list of lists.</p>
<p>We then loop through the results of the flattened list. For each entry, we check to see if it is in the <code>ospf_defined_networks</code> list. If it isn&rsquo;t, we add it to a variable called <code>ospf_delete_networks</code>.</p>
<p>The output of this is therefore: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ok: [routeros-01] =&gt; {
    &#34;ospf_delete_networks&#34;: [
        &#34;169.254.1.1/32&#34;
    ]
}
</code></pre></div><h6 id="delete-the-networks">Delete the networks</h6>
<p>Our final part of this section is removing the non-defined networks. This takes our <code>ospf_delete_networks</code> list, loops through it, and runs <code>/routing ospf network remove [find where network=&quot;{{ item }}&quot;]</code>, <code>{{ item }}</code> being the OSPF networks found in our <code>ospf_delete_networks</code> list.</p>
<p>Before running this, we see this in our OSPF networks list: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-01] &gt; /routing ospf network print
Flags: X - disabled, I - invalid
 #   NETWORK            AREA
 0   10.100.104.0/24    backbone
 1   10.100.204.0/24    backbone
 2   192.0.2.104/32     backbone
 3   169.254.1.1/32     backbone
</code></pre></div><p>After this task, we see: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-01] &gt; /routing ospf network print
Flags: X - disabled, I - invalid
 #   NETWORK            AREA
 0   10.100.104.0/24    backbone
 1   10.100.204.0/24    backbone
 2   192.0.2.104/32     backbone
</code></pre></div><p>This task will only run if the <code>ospf_delete_networks</code> variable exists. If we tried to run it regardless, the task would fail if no undefined networks were configured.</p>
<h5 id="removing-undefined-interfaces">Removing undefined interfaces</h5>
<p>Similar to removing undefined OSPF networks, this requires a set of tasks, rather than a single task.</p>
<h6 id="gathering-facts-from-the-router">Gathering facts from the router</h6>
<p>The first task gathers <strong>facts</strong> (i.e. details about the devices) from the routers. You could use this to discover the version of RouterOS running, the hostname, or more usefully in our case, the list of interfaces (physical and logical) that exist on the router.</p>
<p>The below shows what we retrieve from a RouterOS device when using <code>routeros_facts</code>. We use the <code>gather_subset</code> option to limit to the interface facts: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ok: [routeros-01] =&gt; {
    &#34;mikro_facts&#34;: {
        &#34;ansible_facts&#34;: {
            &#34;ansible_net_all_ipv4_addresses&#34;: [
                &#34;10.15.30.53&#34;,
                &#34;192.0.2.104&#34;,
                &#34;10.100.104.253&#34;,
                &#34;10.100.204.254&#34;,
                &#34;192.168.122.208&#34;
            ],
            &#34;ansible_net_all_ipv6_addresses&#34;: [
                &#34;fe80::5054:ff:fec0:5d55&#34;,
                &#34;fe80::5054:ff:fec0:5d55&#34;,
                &#34;fe80::308a:89ff:fe50:7b25&#34;,
                &#34;fe80::5054:ff:feca:da15&#34;,
                &#34;fe80::5054:ff:fec0:5d55&#34;,
                &#34;fe80::5054:ff:fe30:ec65&#34;
            ],
            &#34;ansible_net_gather_subset&#34;: [
                &#34;default&#34;,
                &#34;interfaces&#34;
            ],
            &#34;ansible_net_hostname&#34;: &#34;routeros-01&#34;,
            &#34;ansible_net_interfaces&#34;: {
                &#34;ether1&#34;: {
                    &#34;actual-mtu&#34;: &#34;1500&#34;,
                    &#34;default-name&#34;: &#34;ether1&#34;,
                    &#34;ipv4&#34;: [
                        {
                            &#34;address&#34;: &#34;10.15.30.53&#34;,
                            &#34;subnet&#34;: &#34;24&#34;
                        }
                    ],
                    &#34;ipv6&#34;: [
                        {
                            &#34;address&#34;: &#34;fe80::5054:ff:feca:da15&#34;,
                            &#34;subnet&#34;: &#34;64&#34;
                        }
                    ],
                    &#34;last-link-up-time&#34;: &#34;may/17/2020 06:03:35&#34;,
                    &#34;link-downs&#34;: &#34;0&#34;,
                    &#34;mac-address&#34;: &#34;52:54:00:CA:DA:15&#34;,
                    &#34;mtu&#34;: &#34;1500&#34;,
                    &#34;name&#34;: &#34;ether1&#34;,
                    &#34;type&#34;: &#34;ether&#34;
                },
                &#34;ether2&#34;: {
                    &#34;actual-mtu&#34;: &#34;1500&#34;,
                    &#34;default-name&#34;: &#34;ether2&#34;,
                    &#34;ipv6&#34;: [
                        {
                            &#34;address&#34;: &#34;fe80::5054:ff:fec0:5d55&#34;,
                            &#34;subnet&#34;: &#34;64&#34;
                        }
                    ],
                    &#34;last-link-up-time&#34;: &#34;may/17/2020 06:03:35&#34;,
                    &#34;link-downs&#34;: &#34;0&#34;,
                    &#34;mac-address&#34;: &#34;52:54:00:C0:5D:55&#34;,
                    &#34;mtu&#34;: &#34;1500&#34;,
                    &#34;name&#34;: &#34;ether2&#34;,
                    &#34;type&#34;: &#34;ether&#34;
                },
                &#34;ether3&#34;: {
                    &#34;actual-mtu&#34;: &#34;1500&#34;,
                    &#34;default-name&#34;: &#34;ether3&#34;,
                    &#34;ipv4&#34;: [
                        {
                            &#34;address&#34;: &#34;192.168.122.208&#34;,
                            &#34;subnet&#34;: &#34;24&#34;
                        }
                    ],
                    &#34;ipv6&#34;: [
                        {
                            &#34;address&#34;: &#34;fe80::5054:ff:fe30:ec65&#34;,
                            &#34;subnet&#34;: &#34;64&#34;
                        }
                    ],
                    &#34;last-link-up-time&#34;: &#34;may/17/2020 06:03:35&#34;,
                    &#34;link-downs&#34;: &#34;0&#34;,
                    &#34;mac-address&#34;: &#34;52:54:00:30:EC:65&#34;,
                    &#34;mtu&#34;: &#34;1500&#34;,
                    &#34;name&#34;: &#34;ether3&#34;,
                    &#34;type&#34;: &#34;ether&#34;
                },
                &#34;loopback0&#34;: {
                    &#34;actual-mtu&#34;: &#34;1500&#34;,
                    &#34;ipv4&#34;: [
                        {
                            &#34;address&#34;: &#34;192.0.2.104&#34;,
                            &#34;subnet&#34;: &#34;32&#34;
                        }
                    ],
                    &#34;ipv6&#34;: [
                        {
                            &#34;address&#34;: &#34;fe80::308a:89ff:fe50:7b25&#34;,
                            &#34;subnet&#34;: &#34;64&#34;
                        }
                    ],
                    &#34;l2mtu&#34;: &#34;65535&#34;,
                    &#34;last-link-up-time&#34;: &#34;may/17/2020 06:03:25&#34;,
                    &#34;link-downs&#34;: &#34;0&#34;,
                    &#34;mac-address&#34;: &#34;32:8A:89:50:7B:25&#34;,
                    &#34;mtu&#34;: &#34;auto&#34;,
                    &#34;name&#34;: &#34;loopback0&#34;,
                    &#34;type&#34;: &#34;bridge&#34;
                },
                &#34;vlan104&#34;: {
                    &#34;actual-mtu&#34;: &#34;1500&#34;,
                    &#34;ipv4&#34;: [
                        {
                            &#34;address&#34;: &#34;10.100.104.253&#34;,
                            &#34;subnet&#34;: &#34;24&#34;
                        }
                    ],
                    &#34;ipv6&#34;: [
                        {
                            &#34;address&#34;: &#34;fe80::5054:ff:fec0:5d55&#34;,
                            &#34;subnet&#34;: &#34;64&#34;
                        }
                    ],
                    &#34;last-link-up-time&#34;: &#34;may/17/2020 06:03:35&#34;,
                    &#34;link-downs&#34;: &#34;0&#34;,
                    &#34;mac-address&#34;: &#34;52:54:00:C0:5D:55&#34;,
                    &#34;mtu&#34;: &#34;1500&#34;,
                    &#34;name&#34;: &#34;vlan104&#34;,
                    &#34;type&#34;: &#34;vlan&#34;
                },
                &#34;vlan204&#34;: {
                    &#34;actual-mtu&#34;: &#34;1500&#34;,
                    &#34;ipv4&#34;: [
                        {
                            &#34;address&#34;: &#34;10.100.204.254&#34;,
                            &#34;subnet&#34;: &#34;24&#34;
                        }
                    ],
                    &#34;ipv6&#34;: [
                        {
                            &#34;address&#34;: &#34;fe80::5054:ff:fec0:5d55&#34;,
                            &#34;subnet&#34;: &#34;64&#34;
                        }
                    ],
                    &#34;last-link-up-time&#34;: &#34;may/17/2020 06:03:35&#34;,
                    &#34;link-downs&#34;: &#34;0&#34;,
                    &#34;mac-address&#34;: &#34;52:54:00:C0:5D:55&#34;,
                    &#34;mtu&#34;: &#34;1500&#34;,
                    &#34;name&#34;: &#34;vlan204&#34;,
                    &#34;type&#34;: &#34;vlan&#34;
                }
            },
            &#34;ansible_net_model&#34;: null,
            &#34;ansible_net_neighbors&#34;: {
                &#34;ether1&#34;: {
                    &#34;address&#34;: &#34;10.15.30.54&#34;,
                    &#34;address4&#34;: &#34;10.15.30.54&#34;,
                    &#34;address6&#34;: &#34;fe80::5054:ff:fe09:3b0&#34;,
                    &#34;age&#34;: &#34;5s&#34;,
                    &#34;board&#34;: &#34;CHR&#34;,
                    &#34;identity&#34;: &#34;routeros-02&#34;,
                    &#34;interface&#34;: &#34;ether1&#34;,
                    &#34;interface-name&#34;: &#34;ether1&#34;,
                    &#34;ipv6&#34;: &#34;yes&#34;,
                    &#34;mac-address&#34;: &#34;52:54:00:09:03:B0&#34;,
                    &#34;platform&#34;: &#34;MikroTik&#34;,
                    &#34;software-id&#34;: &#34;OKX&#34;,
                    &#34;unpack&#34;: &#34;none&#34;,
                    &#34;uptime&#34;: &#34;24m8s&#34;,
                    &#34;version&#34;: &#34;6.45.8&#34;
                },
                &#34;ether2&#34;: {
                    &#34;address&#34;: &#34;fe80::5054:ff:fefc:cd86&#34;,
                    &#34;address6&#34;: &#34;fe80::5054:ff:fefc:cd86&#34;,
                    &#34;age&#34;: &#34;5s&#34;,
                    &#34;board&#34;: &#34;CHR&#34;,
                    &#34;identity&#34;: &#34;routeros-02&#34;,
                    &#34;interface&#34;: &#34;ether2&#34;,
                    &#34;interface-name&#34;: &#34;ether2&#34;,
                    &#34;ipv6&#34;: &#34;yes&#34;,
                    &#34;mac-address&#34;: &#34;52:54:00:FC:CD:86&#34;,
                    &#34;platform&#34;: &#34;MikroTik&#34;,
                    &#34;software-id&#34;: &#34;OKX&#34;,
                    &#34;unpack&#34;: &#34;none&#34;,
                    &#34;uptime&#34;: &#34;24m8s&#34;,
                    &#34;version&#34;: &#34;6.45.8&#34;
                },
                &#34;vlan204&#34;: {
                    &#34;address&#34;: &#34;10.100.204.253&#34;,
                    &#34;address4&#34;: &#34;10.100.204.253&#34;,
                    &#34;address6&#34;: &#34;2001:db8:204::f&#34;,
                    &#34;age&#34;: &#34;5s&#34;,
                    &#34;board&#34;: &#34;CHR&#34;,
                    &#34;identity&#34;: &#34;routeros-02&#34;,
                    &#34;interface&#34;: &#34;vlan204&#34;,
                    &#34;interface-name&#34;: &#34;vlan204&#34;,
                    &#34;ipv6&#34;: &#34;yes&#34;,
                    &#34;mac-address&#34;: &#34;52:54:00:FC:CD:86&#34;,
                    &#34;platform&#34;: &#34;MikroTik&#34;,
                    &#34;software-id&#34;: &#34;OKX&#34;,
                    &#34;unpack&#34;: &#34;none&#34;,
                    &#34;uptime&#34;: &#34;24m8s&#34;,
                    &#34;version&#34;: &#34;6.45.8&#34;
                }
            },
            &#34;ansible_net_serialnum&#34;: null,
            &#34;ansible_net_version&#34;: &#34;6.45.8 (long-term)&#34;
        },
        &#34;changed&#34;: false,
        &#34;failed&#34;: false
    }
}
</code></pre></div><p>As you can see, we have a lot of information returned. We do not need all of this information, but it does mean we do not need to do any regular expressions or parsing to discover what interfaces exist.</p>
<h6 id="find-configured-interfaces">Find configured interfaces</h6>
<p>This task goes through the facts we just retrieved. Rather than looping through a list, we use it as a dictionary (i.e. key/values). We can then retrieve all the <strong>keys</strong> from the <code>ansible_net_interfaces</code> section. We then generate a list of the keys, which looks like: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ok: [routeros-01] =&gt; {
    &#34;configured_interfaces&#34;: [
        &#34;ether1&#34;,
        &#34;ether2&#34;,
        &#34;ether3&#34;,
        &#34;loopback0&#34;,
        &#34;vlan104&#34;,
        &#34;vlan204&#34;
    ]
}
</code></pre></div><h6 id="defined-ospf-interfaces">Defined OSPF interfaces</h6>
<p>This task goes through our <code>host_vars</code>, and generates a list of interfaces that have the <code>ospf</code> field. This results in: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ok: [routeros-01] =&gt; {
    &#34;ospf_interfaces&#34;: [
        &#34;vlan104&#34;,
        &#34;vlan204&#34;,
        &#34;loopback0&#34;
    ]
}
</code></pre></div><h6 id="list-of-non-ospf-interfaces">List of non-ospf interfaces</h6>
<p>To generate the list of interfaces that OSPF should not be running on, we do similar to what we did in the OSPF network tasks. We loop through our list of interfaces gathered from the <code>routeros_facts</code> module and check if each item appears in the <code>ospf_interfaces</code> variable. If it doesn&rsquo;t, we add it to <code>non_ospf_interfaces</code>.</p>
<p>This generates the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ok: [routeros-01] =&gt; {
    &#34;non_ospf_interfaces&#34;: [
        &#34;ether1&#34;,
        &#34;ether2&#34;,
        &#34;ether3&#34;
    ]
}
</code></pre></div><h6 id="remove-ospf-from-non-ospf-interfaces">Remove OSPF from non-OSPF interfaces</h6>
<p>This task loops through our <code>non_ospf_interfaces</code> variable. For each item, it runs <code>/routing ospf interface remove [find where interface=&quot;{{ item }}&quot;]</code>.</p>
<p>In the above, we would run the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/routing ospf interface remove [find where interface=&#34;ether1&#34;]
/routing ospf interface remove [find where interface=&#34;ether2&#34;]
/routing ospf interface remove [find where interface=&#34;ether3&#34;]
</code></pre></div><h5 id="verification-4">Verification</h5>
<p>After this, we should be able to see OSPF routes on both the <strong>edge</strong> router and the <strong>internal</strong> router: -</p>
<p><strong>routeros-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show OSPF interfaces
[admin@routeros-01] &gt; /routing ospf interface print
Flags: X - disabled, I - inactive, D - dynamic, P - passive
 #    INTERFACE     COST PRIORITY NETWORK-TYPE   AUTHENTICATION AUTHENTICATION-KEY
 0  P loopback0       10        1 default        none
 1  P vlan104         10        1 default        none
 2    vlan204         10        1 default        none

! Show OSPF neighbours
[admin@routeros-01] &gt; /routing ospf neighbor print brief
 # ROUTER-ID       ADDRESS         STATE       STATE-CHANGES
 0 192.0.2.204     10.100.204.253  Full                    6

! Show routing table
[admin@routeros-01] &gt; /ip route print where ospf
Flags: X - disabled, A - active, D - dynamic, C - connect, S - static, r - rip, b - bgp, o - ospf, m - mme,
B - blackhole, U - unreachable, P - prohibit
 #      DST-ADDRESS        PREF-SRC        GATEWAY            DISTANCE
 0 ADo  192.0.2.204/32                     10.100.204.253          110

! Can we ping?
[admin@routeros-01] &gt; /ping 192.0.2.204
  SEQ HOST                                     SIZE TTL TIME  STATUS
    0 192.0.2.204                                56  64 0ms
    1 192.0.2.204                                56  64 0ms
    sent=2 received=2 packet-loss=0% min-rtt=0ms avg-rtt=0ms max-rtt=0ms
</code></pre></div><p><strong>routeros-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show OSPF interfaces
[admin@routeros-02] &gt; /routing ospf interface print
Flags: X - disabled, I - inactive, D - dynamic, P - passive
 #    INTERFACE     COST PRIORITY NETWORK-TYPE   AUTHENTICATION AUTHENTICATION-KEY
 0  P loopback0       10        1 default        none
 1    vlan204         10        1 default        none

! Show OSPF neighbours
[admin@routeros-02] &gt; /routing ospf neighbor print brief
 # ROUTER-ID       ADDRESS         STATE     STATE-CHANGES
 0 192.0.2.104     10.100.204.254  Full                  5

! Show routing table
[admin@routeros-02] &gt; /ip route print where ospf
Flags: X - disabled, A - active, D - dynamic, C - connect, S - static, r - rip, b - bgp, o - ospf, m - mme,
B - blackhole, U - unreachable, P - prohibit
 #      DST-ADDRESS        PREF-SRC        GATEWAY            DISTANCE
 0 ADo  10.100.104.0/24                    10.100.204.254          110
 1 ADo  192.0.2.104/32                     10.100.204.254          110

! Can we ping
[admin@routeros-02] &gt; /ping 192.0.2.104
  SEQ HOST                                     SIZE TTL TIME  STATUS
    0 192.0.2.104                                56  64 0ms
    1 192.0.2.104                                56  64 0ms
    sent=2 received=2 packet-loss=0% min-rtt=0ms avg-rtt=0ms max-rtt=0ms

[admin@routeros-02] &gt; /ping 10.100.104.253
  SEQ HOST                                     SIZE TTL TIME  STATUS
    0 10.100.104.253                             56  64 0ms
    sent=1 received=1 packet-loss=0% min-rtt=0ms avg-rtt=0ms max-rtt=0ms
</code></pre></div><p>All looking good!</p>
<h4 id="ospfv3-playbook">OSPFv3 Playbook</h4>
<p>The contents of the OSPFv3 Playbook are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for routing</span>
<span style="color:#75715e">##</span>
- <span style="color:#66d9ef">name</span>: OSPFv3 Process - Router ID
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /routing ospf-v3 instance set <span style="color:#ae81ff">0</span> router-id=<span style="color:#e6db74">&#34;{{ router_id }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospfv3

- <span style="color:#66d9ef">name</span>: OSPFv3 Interfaces
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /routing ospf-v3 interface add interface=<span style="color:#e6db74">&#34;{{ item.routeros_if }}&#34;</span> area=backbone
  <span style="color:#66d9ef">when</span>: item.ospfv3 is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospfv3

- <span style="color:#66d9ef">name</span>: OSPFv3 Interfaces - Passive
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /routing ospf-v3 interface set passive=yes [find where interface=<span style="color:#e6db74">&#34;{{ item.routeros_if }}&#34;</span>]
  <span style="color:#66d9ef">when</span>:
    - item.ospfv3 is defined
    - item.ospfv3.passive is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospfv3

- <span style="color:#66d9ef">name</span>: OSPFv3 Interfaces - Non-Passive
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /routing ospf-v3 interface set passive=no [find where interface=<span style="color:#e6db74">&#34;{{ item.routeros_if }}&#34;</span>]
  <span style="color:#66d9ef">when</span>:
    - item.ospfv3 is defined
    - item.ospfv3.passive is not defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospfv3

- <span style="color:#66d9ef">name</span>: Remove Inactive Interface
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /router ospf-v3 interface remove [find where inactive]
  <span style="color:#66d9ef">tags</span>:
  - ospfv3

<span style="color:#75715e">## Remove undefined interfaces</span>

- <span style="color:#66d9ef">name</span>: Gather facts from Router
  <span style="color:#66d9ef">routeros_facts</span>:
    <span style="color:#66d9ef">gather_subset</span>:
      - interfaces
  <span style="color:#66d9ef">register</span>: mikro_facts
  <span style="color:#66d9ef">tags</span>:
    - ospfv3

- <span style="color:#66d9ef">name</span>: Find configured interfaces
  <span style="color:#66d9ef">set_fact</span>:
    <span style="color:#66d9ef">configured_interfaces</span>: <span style="color:#e6db74">&#34;{{ configured_interfaces| default([]) + [ item.key ] }}&#34;</span>
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ lookup(&#39;dict&#39;, mikro_facts.ansible_facts.ansible_net_interfaces) }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospfv3

- <span style="color:#66d9ef">name</span>: List of defined OSPFv3 interfaces
  <span style="color:#66d9ef">set_fact</span>:
    <span style="color:#66d9ef">ospfv3_interfaces</span>: <span style="color:#e6db74">&#34;{{ ospfv3_interfaces| default([]) + [ item.routeros_if ]}}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - item.ospfv3 is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospfv3

- <span style="color:#66d9ef">name</span>: List of non-OSPFv3 interfaces
  <span style="color:#66d9ef">set_fact</span>:
    <span style="color:#66d9ef">non_ospfv3_interfaces</span>: <span style="color:#e6db74">&#34;{{ non_ospfv3_interfaces|default([]) + [ item ] }}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - ospfv3_interfaces is not search(item)
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ configured_interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospfv3

- <span style="color:#66d9ef">name</span>: Remove OSPFv3 from non-OSPFv3 interfaces
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /routing ospf-v3 interface remove [find where interface=<span style="color:#e6db74">&#34;{{ item }}&#34;</span>]
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ non_ospfv3_interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospfv3
</code></pre></div><p>This task is very similar to the OSPF playbook, except: -</p>
<ul>
<li>OSPFv3 does not use network statements in RouterOS</li>
<li>We need to remove inactive interfaces</li>
<li>The word <code>ospf-v3</code> is used instead of <code>ospf</code></li>
</ul>
<p>Removing inactive interfaces is used because sometimes when adding an <code>ospf-v3</code> interface, it can add configuration identical to what already exists, and then remove the interface from the existing configuration. This task removes any inactive (i.e. potentially broken) configuration.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-02] /routing ospf-v3 interface&gt; print
Flags: X - disabled, I - inactive, D - dynamic, P - passive
 #    INTERFACE                                           AREA                                           COST PRIORITY NETWORK-TYPE
 0 I  *E                                                  backbone                                         10        1 default
 1  P loopback0                                           backbone                                         10        1 default
 2 I  *F                                                  backbone                                         10        1 default
 3    vlan204                                             backbone                                         10        1 default
</code></pre></div><p>As we can see in the above, two of the interfaces are inactive. The task above then removes them, so that we are only left with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-02] /routing ospf-v3 interface&gt; print
Flags: X - disabled, I - inactive, D - dynamic, P - passive
 #    INTERFACE                                           AREA                                           COST PRIORITY NETWORK-TYPE
 0  P loopback0                                           backbone                                         10        1 default
 1    vlan204                                             backbone                                         10        1 default
</code></pre></div><p>Other than that, this set of tasks takes the same approach as we would for OSPF.</p>
<p>##### Verification</p>
<p>We&rsquo;ll follow the same steps as we did for OSPF: -</p>
<p><strong>routeros-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show OSPFv3 interfaces
[admin@routeros-01] &gt; /routing ospf-v3 interface print
Flags: X - disabled, I - inactive, D - dynamic, P - passive
 #    INTERFACE                                           AREA                                           COST PRIORITY NETWORK-TYPE
 0  P loopback0                                           backbone                                         10        1 default
 1  P vlan104                                             backbone                                         10        1 default
 2    vlan204                                             backbone                                         10        1 default

! Show OSPFv3 neighbours
[admin@routeros-01] &gt; /routing ospf-v3 neighbor print brief
 # ROUTER-ID       ADDRESS                                 STATE                                                        STATE-CHANGES
 0 192.0.2.204     fe80::5054:ff:fefc:cd86                 Full                                                                     6

! Show routing table
Flags: X - disabled, A - active, D - dynamic, C - connect, S - static, r - rip, o - ospf, b - bgp, U - unreachable
 #      DST-ADDRESS              GATEWAY                  DISTANCE
 0 ADo  2001:db8:904:beef::2/128 fe80::5054:ff:fefc:cd...      110

! Ping!
[admin@routeros-01] &gt; /ping 2001:db8:904:beef::2
  SEQ HOST                                     SIZE TTL TIME  STATUS
    0 2001:db8:904:beef::2                       56  64 1ms   echo reply
    1 2001:db8:904:beef::2                       56  64 0ms   echo reply
    sent=2 received=2 packet-loss=0% min-rtt=0ms avg-rtt=0ms max-rtt=1ms
</code></pre></div><p><strong>routeros-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show OSPFv3 interfaces
[admin@routeros-02] /routing ospf-v3 interface&gt; print
Flags: X - disabled, I - inactive, D - dynamic, P - passive
 #    INTERFACE                                           AREA                                           COST PRIORITY NETWORK-TYPE
 0  P loopback0                                           backbone                                         10        1 default
 1    vlan204                                             backbone                                         10        1 default

! Show OSPFv3 neighbours
[admin@routeros-02] &gt; /routing ospf-v3 neighbor print brief
 # ROUTER-ID       ADDRESS                                 STATE                                                        STATE-CHANGES
 0 192.0.2.104     fe80::5054:ff:fec0:5d55                 Full                                                                     5

! Show routing table
[admin@routeros-02] &gt; /ipv6 route print where ospf
Flags: X - disabled, A - active, D - dynamic, C - connect, S - static, r - rip, o - ospf, b - bgp, U - unreachable
 #      DST-ADDRESS              GATEWAY                  DISTANCE
 0 ADo  2001:db8:104::/64        fe80::5054:ff:fec0:5d...      110
 1 ADo  2001:db8:904:beef::1/128 fe80::5054:ff:fec0:5d...      110

! Ping!
[admin@routeros-02] &gt; /ping 2001:db8:904:beef::1
  SEQ HOST                                     SIZE TTL TIME  STATUS
    0 2001:db8:904:beef::1                       56  64 1ms   echo reply
    1 2001:db8:904:beef::1                       56  64 0ms   echo reply
    sent=2 received=2 packet-loss=0% min-rtt=0ms avg-rtt=0ms max-rtt=1ms

[admin@routeros-02] &gt; /ping 2001:db8:104::f
  SEQ HOST                                     SIZE TTL TIME  STATUS
    0 2001:db8:104::f                            56  64 0ms   echo reply
    1 2001:db8:104::f                            56  64 0ms   echo reply
    2 2001:db8:104::f                            56  64 0ms   echo reply
    sent=3 received=3 packet-loss=0% min-rtt=0ms avg-rtt=0ms max-rtt=0ms
</code></pre></div><p>All looks good!</p>
<h4 id="bgp-playbook">BGP Playbook</h4>
<p>The BGP playbook is where we configure our internal and external BGP peers. This playbook is potentially the most complex playbook in the series so far. We are not only adding peers, but also checking for the existence of peers, updating them if they exist, creating them if they do not, and also using regular expressions and command parsing to remove peers that are not defined any longer.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for routing</span>
<span style="color:#75715e">##</span>
<span style="color:#75715e">##</span>
- <span style="color:#66d9ef">name</span>: Configure BGP - Instance details
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /routing bgp instance set <span style="color:#ae81ff">0</span> as=<span style="color:#e6db74">&#34;{{ bgp.local_as }}&#34;</span> router-id=<span style="color:#e6db74">&#34;{{ router_id }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - bgp

- <span style="color:#66d9ef">name</span>: Retrieve current BGP peers
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /routing bgp peer print
  <span style="color:#66d9ef">register</span>: bgp_peer_result
  <span style="color:#66d9ef">no_log</span>: True
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v4
  - bgp_v6

- <span style="color:#66d9ef">name</span>: Clean the command output - BGP Peers
  <span style="color:#66d9ef">set_fact</span>:
    <span style="color:#66d9ef">bgp_peer_result_output</span>: <span style="color:#e6db74">&#34;{{ bgp_peer_result_output|default([]) + [ item ] }}&#34;</span>
    <span style="color:#66d9ef">no_log</span>: True
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp_peer_result.stdout_lines }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v4
  - bgp_v6

- <span style="color:#66d9ef">name</span>: Check if the peer exists - IPv4
  <span style="color:#66d9ef">set_fact</span>:
    <span style="color:#66d9ef">peer_exists</span>: <span style="color:#e6db74">&#34;{{ peer_exists|default([]) + [ item.peer ] }}&#34;</span>
  <span style="color:#66d9ef">when</span>: bgp_peer_result_output is search(item.peer)
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv4 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v4

- <span style="color:#66d9ef">name</span>: Check if the peer exists - IPv6
  <span style="color:#66d9ef">set_fact</span>:
    <span style="color:#66d9ef">peer_exists</span>: <span style="color:#e6db74">&#34;{{ peer_exists|default([]) + [ item.peer ] }}&#34;</span>
  <span style="color:#66d9ef">when</span>: bgp_peer_result_output is search(item.peer)
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv6 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v6

- <span style="color:#66d9ef">name</span>: Configure BGP - Add eBGP v4 peers if they are not configured
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /routing bgp peer add remote-as=<span style="color:#e6db74">&#34;{{ item.remote_as }}&#34;</span> remote-address=<span style="color:#e6db74">&#34;{{ item.peer }}&#34;</span> name=<span style="color:#e6db74">&#34;{{ item.name }}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv4 is defined
    - item.ebgp is defined
    - peer_exists is not defined or peer_exists is not search(item.peer)
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv4 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v4

- <span style="color:#66d9ef">name</span>: Configure BGP - Update eBGP v4 peers if they are configured
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /routing bgp peer set [find where remote-address=<span style="color:#e6db74">&#34;{{ item.peer }}&#34;</span>] remote-as=<span style="color:#e6db74">&#34;{{ item.remote_as }}&#34;</span> name=<span style="color:#e6db74">&#34;{{ item.name }}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv4 is defined
    - item.ebgp is defined
    - peer_exists is defined
    - peer_exists is search(item.peer)
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv4 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v4

- <span style="color:#66d9ef">name</span>: Configure BGP - Add eBGP v6 peers if they are not configured
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /routing bgp peer add remote-as=<span style="color:#e6db74">&#34;{{ item.remote_as }}&#34;</span> remote-address=<span style="color:#e6db74">&#34;{{ item.peer }}&#34;</span> address-families=<span style="color:#e6db74">&#34;ipv6&#34;</span> name=<span style="color:#e6db74">&#34;{{ item.name }}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv6 is defined
    - item.ebgp is defined
    - peer_exists is not defined or peer_exists is not search(item.peer)
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv6 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v6

- <span style="color:#66d9ef">name</span>: Configure BGP - Update eBGP v6 peers if they are configured
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /routing bgp peer set [find where remote-address=<span style="color:#e6db74">&#34;{{ item.peer }}&#34;</span>] remote-as=<span style="color:#e6db74">&#34;{{ item.remote_as }}&#34;</span> address-families=<span style="color:#e6db74">&#34;ipv6&#34;</span> name=<span style="color:#e6db74">&#34;{{ item.name }}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv6 is defined
    - item.ebgp is defined
    - peer_exists is defined
    - peer_exists is search(item.peer)
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv6 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v6

- <span style="color:#66d9ef">name</span>: Configure BGP - Add iBGP v4 peers if they are not configured
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /routing bgp peer add remote-as=<span style="color:#e6db74">&#34;{{ item.remote_as }}&#34;</span> remote-address=<span style="color:#e6db74">&#34;{{ item.peer }}&#34;</span> update-source=<span style="color:#e6db74">&#34;{{ item.update_source }}&#34;</span> name=<span style="color:#e6db74">&#34;{{ item.name }}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv4 is defined
    - item.ibgp is defined
    - peer_exists is not defined or peer_exists is not search(item.peer)
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv4 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v4

- <span style="color:#66d9ef">name</span>: Configure BGP - Update iBGP v4 peers if they are configured
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /routing bgp peer set [find where remote-address=<span style="color:#e6db74">&#34;{{ item.peer }}&#34;</span>] remote-as=<span style="color:#e6db74">&#34;{{ item.remote_as }}&#34;</span> update-source=<span style="color:#e6db74">&#34;{{ item.update_source }}&#34;</span> name=<span style="color:#e6db74">&#34;{{ item.name }}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv4 is defined
    - item.ibgp is defined
    - peer_exists is defined
    - peer_exists is search(item.peer)
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv4 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v4

- <span style="color:#66d9ef">name</span>: Configure BGP - Add iBGP v6 peers if they are not configured
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /routing bgp peer add remote-as=<span style="color:#e6db74">&#34;{{ item.remote_as }}&#34;</span> remote-address=<span style="color:#e6db74">&#34;{{ item.peer }}&#34;</span> update-source=<span style="color:#e6db74">&#34;{{ item.update_source }}&#34;</span> address-families=<span style="color:#e6db74">&#34;ipv6&#34;</span> name=<span style="color:#e6db74">&#34;{{ item.name }}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv6 is defined
    - item.ibgp is defined
    - peer_exists is not defined or peer_exists is not search(item.peer)
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv6 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v6

- <span style="color:#66d9ef">name</span>: Configure BGP - Update iBGP v6 peers if they are configured
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /routing bgp peer set [find where remote-address=<span style="color:#e6db74">&#34;{{ item.peer }}&#34;</span>] remote-as=<span style="color:#e6db74">&#34;{{ item.remote_as }}&#34;</span> update-source=<span style="color:#e6db74">&#34;{{ item.update_source }}&#34;</span> address-families=<span style="color:#e6db74">&#34;ipv6&#34;</span> name=<span style="color:#e6db74">&#34;{{ item.name }}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv6 is defined
    - item.ibgp is defined
    - peer_exists is defined
    - peer_exists is search(item.peer)
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv6 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v6

- <span style="color:#66d9ef">name</span>: Configure BGP - iBGP v4 Default Originate
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /routing bgp peer set [find where remote-address=<span style="color:#e6db74">&#34;{{ item.peer }}&#34;</span>] default-originate=if-installed
  <span style="color:#66d9ef">when</span>:
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv4 is defined
    - item.ibgp is defined
    - item.default_originate is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv4 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v4

- <span style="color:#66d9ef">name</span>: Configure BGP - Redistribute OSPF, connected and static
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /routing bgp instance set <span style="color:#ae81ff">0</span> redistribute-ospf=yes redistribute-connected=yes redistribute-static=yes
  <span style="color:#66d9ef">when</span>:
    - bgp is defined
    - bgp.redist is defined
    - bgp.redist.ospf is defined
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v4
  - bgp_v6

<span style="color:#75715e">#### Remove undefined peers</span>

- <span style="color:#66d9ef">name</span>: Retrieve IPs of configured peers - IPv4
  <span style="color:#66d9ef">set_fact</span>:
    <span style="color:#66d9ef">v4_configured_peers</span>: <span style="color:#e6db74">&#34;{{ v4_configured_peers|default([]) + [ item | regex_findall(&#39;\\b(?:[0-9]{1,3}\\.){3}[0-9]{1,3}\\b&#39;) ] }}&#34;</span>
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp_peer_result.stdout_lines[0] | reject(&#39;search&#39;, &#39;Flags&#39;) | reject(&#39;search&#39;, &#39;INSTANCE&#39;) | list }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v4

- <span style="color:#66d9ef">name</span>: Retrieve IPs of configured peers - IPv6
  <span style="color:#66d9ef">set_fact</span>:
    <span style="color:#66d9ef">v6_configured_peers</span>: <span style="color:#e6db74">&#34;{{ v6_configured_peers|default([]) + [ item | regex_findall(&#39;[0-9a-fA-F]{1,4}:.*:[0-9a-fA-F]{1,4}&#39;) ]  }}&#34;</span>
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp_peer_result.stdout_lines[0] | reject(&#39;search&#39;, &#39;Flags&#39;) | reject(&#39;search&#39;, &#39;INSTANCE&#39;) | list }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v6

- <span style="color:#66d9ef">name</span>: Combine the configured peer lists
  <span style="color:#66d9ef">set_fact</span>:
    <span style="color:#66d9ef">configured_peers</span>: <span style="color:#e6db74">&#39;{{ v4_configured_peers|flatten|default([]) + v6_configured_peers|flatten|default([])}}&#39;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v4
  - bgp_v6


- <span style="color:#66d9ef">name</span>: Create list of defined peers
  <span style="color:#66d9ef">set_fact</span>:
    <span style="color:#66d9ef">defined_peers</span>: <span style="color:#e6db74">&#34;{{ defined_peers|default([]) + [ item.peer ] }}&#34;</span>
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv4 + bgp.neighbors.ipv6 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v4
  - bgp_v6

- <span style="color:#66d9ef">name</span>: Create list of peers to delete
  <span style="color:#66d9ef">set_fact</span>:
    <span style="color:#66d9ef">delete_peers</span>: <span style="color:#e6db74">&#34;{{ delete_peers|default([]) + [ item ] }}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - defined_peers is not search(item)
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ configured_peers }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v4
  - bgp_v6

- <span style="color:#66d9ef">name</span>: Delete the peers
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /routing bgp peer remove [find where remote-address=<span style="color:#e6db74">&#34;{{ item }}&#34;</span>]
  <span style="color:#66d9ef">when</span>: delete_peers is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ delete_peers }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v4
  - bgp_v6
</code></pre></div><h5 id="setting-bgp-instance-details">Setting BGP instance details</h5>
<p>The first task updates the main BGP instance, setting the router ID and our local autonomous system number. These variables are sourced from our <code>host_vars</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">router_id</span>: <span style="color:#ae81ff">192.0.2.104</span>
<span style="color:#66d9ef">bgp</span>:
  <span style="color:#66d9ef">local_as</span>: <span style="color:#ae81ff">65104</span>
</code></pre></div><p>This generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/routing bgp instance set default as=65104 router-id=192.0.2.104
</code></pre></div><p>RouterOS automatically translates the instance with an ID of <code>0</code> to <code>default</code>.</p>
<p>We can verify this with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-01] &gt; /routing bgp instance print
Flags: * - default, X - disabled
 0 *  name=&#34;default&#34; as=65104 router-id=192.0.2.104 redistribute-connected=no redistribute-static=no redistribute-rip=no redistribute-ospf=no redistribute-other-bgp=no out-filter=&#34;&#34; client-to-client-reflection=no
      ignore-as-path-len=no routing-table=&#34;&#34;
</code></pre></div><h5 id="building-peers">Building peers</h5>
<p>The following set of tasks are used to determine whether a BGP peer is already defined. If it isn&rsquo;t, it will create a new peer. If it is, it will update the existing peer. Because the syntax used to add a peer and configure an existing peer are not consistent, this is a good workaround to maintain idempotence.</p>
<h6 id="get-the-list-of-peers">Get the list of peers</h6>
<p>First, we get our list of BGP peers. This includes both IPv4 and IPv6 peers. We also use the <code>no_log: True</code> option, so that we are not presented with pages of information on every run through of the playbook. If you need to enable it for debugging, you would change this to <code>false</code>, or remove it entirely.</p>
<p>Once we have the list of peers, we store them in the <code>bgp_peer_result</code> variable: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ok: [routeros-01] =&gt; {
    &#34;bgp_peer_result&#34;: {
        &#34;changed&#34;: false,
        &#34;failed&#34;: false,
        &#34;stdout&#34;: [
            &#34;Flags: X - disabled, E - established \n #   INSTANCE        REMOTE-ADDRESS                                 REMOTE-AS  \n 0 E default         2001:db8:904:beef::2                           65104      \n 1 E default         10.100.104.254                                 65430      \n 2 E default         2001:db8:104::ffff                             65430      \n 3 E default         192.0.2.204                                    65104&#34;
        ],
        &#34;stdout_lines&#34;: [
            [
                &#34;Flags: X - disabled, E - established &#34;,
                &#34; #   INSTANCE        REMOTE-ADDRESS                                 REMOTE-AS  &#34;,
                &#34; 0 E default         2001:db8:904:beef::2                           65104      &#34;,
                &#34; 1 E default         10.100.104.254                                 65430      &#34;,
                &#34; 2 E default         2001:db8:104::ffff                             65430      &#34;,
                &#34; 3 E default         192.0.2.204                                    65104&#34;
            ]
        ]
    }
}
</code></pre></div><h6 id="cleaning-the-output">Cleaning the output</h6>
<p>The next task takes the output above, and creates a list from the entries in <code>stdout_lines</code>.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ok: [routeros-01] =&gt; {
    &#34;bgp_peer_result_output&#34;: [
        [
            &#34;Flags: X - disabled, E - established &#34;,
            &#34; #   INSTANCE        REMOTE-ADDRESS                                 REMOTE-AS  &#34;,
            &#34; 0 E default         2001:db8:904:beef::2                           65104      &#34;,
            &#34; 1 E default         10.100.104.254                                 65430      &#34;,
            &#34; 2 E default         2001:db8:104::ffff                             65430      &#34;,
            &#34; 3 E default         192.0.2.204                                    65104&#34;
        ]
    ]
}
</code></pre></div><p>While this doesn&rsquo;t do much, it does make the output cleaner and easier to work with.</p>
<h6 id="checking-if-the-peer-exists---ipv4">Checking if the peer exists - IPv4</h6>
<p>This task goes through the list of peers defined in our <code>host_vars</code>. If the peer exists in the <code>bgp_peer_result_output</code> variable, it adds it to <code>peer_exists</code> variable. If <code>peer_exists</code> isn&rsquo;t already defined, it creates an empty list.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ok: [routeros-01] =&gt; {
    &#34;peer_exists&#34;: [
        &#34;10.100.104.254&#34;,
        &#34;192.0.2.204&#34;
     ]
}
</code></pre></div><h6 id="checking-if-the-peer-exists---ipv6">Checking if the peer exists - IPv6</h6>
<p>This task does the same as the above, except for IPv6: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ok: [routeros-01] =&gt; {
    &#34;peer_exists&#34;: [
        &#34;10.100.104.254&#34;,
        &#34;192.0.2.204&#34;,
        &#34;2001:db8:104::ffff&#34;,
        &#34;2001:db8:904:beef::2&#34;
    ]
}
</code></pre></div><p>For simplicity, we do not create a new list for IPv6.</p>
<h6 id="adding-ebgp-peers-if-they-do-not-exist---ipv4">Adding eBGP peers if they do not exist - IPv4</h6>
<p>This task will go through our list of peers in our <code>host_vars</code>. If they are IPv4, and if they are not already configured (i.e. they are not in the <code>peer_exists</code> variable, or no peers are configured), it will attempt to add them.</p>
<p>The <code>host_vars</code> that are relevant are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">bgp</span>:
  <span style="color:#66d9ef">local_as</span>: <span style="color:#ae81ff">65104</span>
  <span style="color:#66d9ef">neighbors</span>:
    <span style="color:#66d9ef">ipv4</span>:
     - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">10.100.104.254</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65430</span>
       <span style="color:#66d9ef">ebgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">name</span>: netsvr<span style="color:#ae81ff">-01</span>-v4
     - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">192.0.2.204</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65104</span>
       <span style="color:#66d9ef">ibgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">update_source</span>: loopback0
       <span style="color:#66d9ef">name</span>: routeros<span style="color:#ae81ff">-02</span>-v4
</code></pre></div><p>As we can see, only one peer here is eBGP, so the configuration we would generate is: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/routing bgp peer add name=netsvr-01-v4 remote-address=10.100.104.254 remote-as=65430
</code></pre></div><h6 id="update-existing-ebgp-peers-if-they-do-not-exist---ipv4">Update existing eBGP peers if they do not exist - IPv4</h6>
<p>This task is like the previous, except rather than attempting to add peers, it updates existing peer configuration. This task is dependent on the peer being defined as an eBGP peer in our <code>host_vars</code>, and being in the <code>peer_exists</code> variable.</p>
<p>The same <code>host_vars</code> are used, but this time we generate the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/routing bgp peer set [find where remote-address=&#34;10.100.104.254&#34;] remote-as=&#34;65430&#34; name=&#34;netsvr-01-v4&#34;
</code></pre></div><h6 id="adding-ebgp-peers-if-they-do-not-exist---ipv6">Adding eBGP peers if they do not exist - IPv6</h6>
<p>This task is the same as the IPv4 version, except it is for IPv6 peers.</p>
<p>The <code>host_vars</code> that are relevant are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">bgp</span>:
  <span style="color:#66d9ef">local_as</span>: <span style="color:#ae81ff">65104</span>
  <span style="color:#66d9ef">neighbors</span>:
    <span style="color:#66d9ef">ipv6</span>:
     - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:104::ffff&#34;</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65430</span>
       <span style="color:#66d9ef">ebgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">name</span>: netsvr<span style="color:#ae81ff">-01</span>-v6
     - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:904:beef::2&#34;</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65104</span>
       <span style="color:#66d9ef">ibgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">update_source</span>: loopback0
       <span style="color:#66d9ef">name</span>: routeros<span style="color:#ae81ff">-02</span>-v6
</code></pre></div><p>This then generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/routing bgp peer add address-families=ipv6 name=netsvr-01-v6 remote-address=2001:db8:104::ffff remote-as=65430
</code></pre></div><p>In addition, we restrict this to the IPv6 address family, to ensure it does not attempt to advertise/receive IPv4 routes over this session. This is supported on RouterOS, but not all vendors supports this.</p>
<h6 id="update-existing-ebgp-peers-if-they-do-exist---ipv6">Update existing eBGP peers if they do exist - IPv6</h6>
<p>Again, this is like the IPv4 task, except it runs only if the peer is in the <code>peer_exists</code> variable.</p>
<p>The same <code>host_vars</code> are used, but this time we generate the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/routing bgp peer set [find where remote-address=&#34;2001:db8:104::ffff&#34;] remote-as=&#34;65430&#34; address-families=&#34;ipv6&#34; name=&#34;netsvr-01-v6&#34;
</code></pre></div><h6 id="ibgp-tasks-for-ipv4-and-ipv6">iBGP tasks for IPv4 and IPv6</h6>
<p>We have an almost identical set of tasks, except for iBGP rather than eBGP. The difference between eBGP and iBGP is that for eBGP, we do not set a source interface for the BGP peer. For iBGP we do, because this peer may be available over multiple interfaces.</p>
<p>If the peers do not exist (i.e. not already configured), this configuration is generated: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/routing bgp peer add address-families=ipv6 name=routeros-02-v6 remote-address=2001:db8:904:beef::2 remote-as=65104 update-source=loopback0
/routing bgp peer add name=routeros-02-v4 remote-address=192.0.2.204 remote-as=65104 update-source=loopback0
</code></pre></div><p>If the peers do not exist, this is applied instead: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/routing bgp peer set [find where remote-address=&#34;2001:db8:904:beef::2&#34;] address-families=ipv6 name=routeros-02-v6 remote-as=65104 update-source=loopback0
/routing bgp peer set [find where remote-address=&#34;192.0.2.204&#34;] name=routeros-02-v4 remote-as=65104 update-source=loopback0
</code></pre></div><h5 id="default-originate-on-the-ibgp-ipv4-session">Default Originate on the iBGP IPv4 session</h5>
<p>This task will go through the BGP neighbours in our <code>host_vars</code>. If the neighbour has the <code>default_originate</code> option, it will update that peer. This will enable it to advertise a default route.</p>
<p>The <code>host_vars</code> that are relevant are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">bgp</span>:
  <span style="color:#66d9ef">local_as</span>: <span style="color:#ae81ff">65104</span>
  <span style="color:#66d9ef">neighbors</span>:
    <span style="color:#66d9ef">ipv4</span>:
     - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">192.0.2.204</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65104</span>
       <span style="color:#66d9ef">ibgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">update_source</span>: loopback0
       <span style="color:#66d9ef">default_originate</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">name</span>: routeros<span style="color:#ae81ff">-02</span>-v4
</code></pre></div><p>This will generate the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/routing bgp peer set [find where remote-address=&#34;{{ item.peer }}&#34;] default-originate=if-installed
</code></pre></div><p>We can verify this with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-01] &gt; /routing bgp peer print detail where remote-address=192.0.2.204
Flags: X - disabled, E - established
 0 E name=&#34;routeros-02-v4&#34; instance=default remote-address=192.0.2.204 remote-as=65104 tcp-md5-key=&#34;&#34; nexthop-choice=default multihop=no route-reflect=no hold-time=3m ttl=255 in-filter=&#34;&#34; out-filter=&#34;&#34; address-families=ip
     update-source=loopback0 default-originate=if-installed remove-private-as=no as-override=no passive=no use-bfd=no
</code></pre></div><h5 id="redistributing-ospf-connected-and-static-routes">Redistributing OSPF, connected and static routes</h5>
<p>This task is used to advertise our OSPF routes, connected routes and also static routes (including our DHCP-received default route). The latter is required because the <code>if-installed</code> option of BGP&rsquo;s <code>default-originate</code> requires the route to be part of the BGP process (even if through redistribution). This task will be applied if we have the <code>redist</code> option set in the <code>bgp</code> section of our <code>host_vars</code>.</p>
<p>This generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/routing bgp instance set 0 redistribute-ospf=yes redistribute-connected=yes redistribute-static=yes
</code></pre></div><p>We can verify this with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-01] &gt; /routing bgp instance print
Flags: * - default, X - disabled
 0 *  name=&#34;default&#34; as=65104 router-id=192.0.2.104 redistribute-connected=yes redistribute-static=yes redistribute-rip=no redistribute-ospf=yes redistribute-other-bgp=no out-filter=&#34;&#34; client-to-client-reflection=yes
      ignore-as-path-len=no routing-table=&#34;&#34;
</code></pre></div><h5 id="removing-undefined-peers">Removing undefined peers</h5>
<p>This set of tasks retrieves the list of peers configured on the routers. If they do not exist in our <code>host_vars</code>, they are removed.</p>
<p>This means that peers removed from our <code>host_vars</code> will also be removed from the routers too.</p>
<p>Similar to the OSPF playbook, we use regular expressions to parse commands. We then use variables (dynamically generated/discovered, and our <code>host_vars</code>) to remove the undefined peers.</p>
<h6 id="retrieve-ips-of-configured-peers---ipv4">Retrieve IPs of configured peers - IPv4</h6>
<p>This task takes the output of <code>bgp_peer_result</code> (created earlier in the playbook), and gathers the first entry in the <code>stdout_lines</code> section. This takes the below output: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ok: [routeros-01] =&gt; {
    &#34;bgp_peer_result&#34;: {
        &#34;changed&#34;: false,
        &#34;failed&#34;: false,
        &#34;stdout&#34;: [
            &#34;Flags: X - disabled, E - established \n #   INSTANCE        REMOTE-ADDRESS                                 REMOTE-AS  \n 0 E default         2001:db8:904:beef::2                           65104      \n 1 E default         10.100.104.254                                 65430      \n 2 E default         2001:db8:104::ffff                             65430      \n 3 E default         192.0.2.204                                    65104      \n 4   default         192.0.2.255                                    65001&#34;
        ],
        &#34;stdout_lines&#34;: [
            [
                &#34;Flags: X - disabled, E - established &#34;,
                &#34; #   INSTANCE        REMOTE-ADDRESS                                 REMOTE-AS  &#34;,
                &#34; 0 E default         2001:db8:904:beef::2                           65104      &#34;,
                &#34; 1 E default         10.100.104.254                                 65430      &#34;,
                &#34; 2 E default         2001:db8:104::ffff                             65430      &#34;,
                &#34; 3 E default         192.0.2.204                                    65104      &#34;,
                &#34; 4   default         192.0.2.255                                    65001&#34;
            ]
        ]
    }
}
</code></pre></div><p>and turns it into: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ok: [routeros-01] =&gt; {
    &#34;bgp_peer_result_output&#34;: [
        [
            &#34;Flags: X - disabled, E - established &#34;,
            &#34; #   INSTANCE        REMOTE-ADDRESS                                 REMOTE-AS  &#34;,
            &#34; 0 E default         2001:db8:904:beef::2                           65104      &#34;,
            &#34; 1 E default         10.100.104.254                                 65430      &#34;,
            &#34; 2 E default         2001:db8:104::ffff                             65430      &#34;,
            &#34; 3 E default         192.0.2.204                                    65104      &#34;,
            &#34; 4   default         192.0.2.255                                    65001&#34;
        ]
    ]
}
</code></pre></div><p>After this, we use <code>reject</code> filters to remove the lines <code>Flags: X - disabled, E - established</code> and <code> #   INSTANCE        REMOTE-ADDRESS                                 REMOTE-AS</code>. This leaves us with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[
    &#34; 0 E default         2001:db8:904:beef::2                           65104      &#34;,
    &#34; 1 E default         10.100.104.254                                 65430      &#34;,
    &#34; 2 E default         2001:db8:104::ffff                             65430      &#34;,
    &#34; 3 E default         192.0.2.204                                    65104      &#34;,
    &#34; 4   default         192.0.2.255                                    65001&#34;
]
</code></pre></div><p>Once this is done, we convert it to a list again. This is because  <code>reject</code> filters turn the list into something called a <strong>generator</strong> object. A <strong>generator</strong> is an object that can be iterated over (like a list) but is not human readable. If you are familiar with Python, you will likely be familiar with <strong>generators</strong>. For our purposes, a <strong>generator</strong> is not useful, so we transform it back into a list.</p>
<p>We then loop through the output and extract the IPv4 peer IPs using regular expressions.</p>
<p>The regular expression is <code>\\b(?:[0-9]{1,3}\\.){3}[0-9]{1,3}\\b</code>. This does the following: -</p>
<ul>
<li><code>\\b</code> - Whole words/strings only, not a match within a word</li>
<li>The <code>(?:</code> and <code>)</code> section creates a <strong>non-capturing</strong> group, allowing us to repeat the expression inside it without needing to necessarily &ldquo;save&rdquo; the results</li>
<li>The <code>[0-9]{1,3}</code> says that we are looking for numbers between 0-9 between 1 and 3 times. This will match up to 255</li>
<li>Technically this could also match up to 999, but IP addresses do not go that high!</li>
<li>The <code>{3}</code> matches the previous expression exactly 3 times</li>
<li>The second use of <code>[0-9]{1,3}</code> matches the last octet of an IP address</li>
</ul>
<p>This is similar to what we used for OSPF networks, except there are no network masks to match.</p>
<p>We then create a list called <code>v4_configured_peers</code>. Every IPv4 peer we find using the above regular expression is added to it: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ok: [routeros-01] =&gt; {
    &#34;v4_configured_peers&#34;: [
        [],
        [
            &#34;10.100.104.254&#34;
        ],
        [],
        [
            &#34;192.0.2.204&#34;
        ],
        [
            &#34;192.0.2.255&#34;
        ]
    ]
}
</code></pre></div><h6 id="retrieve-ips-of-configured-peers---ipv6">Retrieve IPs of configured peers - IPv6</h6>
<p>We do the same as above for IPv6. The only difference is the regular expression. The regular expression is <code>[0-9a-fA-F]{1,4}:.*:[0-9a-fA-F]{1,4}</code>. This is not as comprehensive as the IPv4 regular expression, and could miss certain kinds of IPv6 addresses. It will match any BGP peer we&rsquo;d configure using IPv6, so this is good enough.</p>
<p>This generates the <code>v6_configured_peers</code> list, which looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ok: [routeros-01] =&gt; {
    &#34;v6_configured_peers&#34;: [
        [
            &#34;2001:db8:904:beef::2&#34;
        ],
        [],
        [
            &#34;2001:db8:104::ffff&#34;
        ],
        [],
        []
    ]
}
</code></pre></div><h6 id="combining-the-configured-peer-lists">Combining the configured peer lists</h6>
<p>This task takes the two lists generated (<code>v4_configured_peers</code> and <code>v6_configured_peers</code>) and combines them. It also <strong>flattens</strong> the lists. Rather than lists within lists, all elements are part of one list instead.</p>
<p>The output of this is: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ok: [routeros-01] =&gt; {
    &#34;configured_peers&#34;: [
        &#34;10.100.104.254&#34;,
        &#34;192.0.2.204&#34;,
        &#34;192.0.2.255&#34;,
        &#34;2001:db8:904:beef::2&#34;,
        &#34;2001:db8:104::ffff&#34;
    ]
}
</code></pre></div><h6 id="creating-list-of-defined-peers">Creating list of defined peers</h6>
<p>This task goes through our <code>host_vars</code>, and generates a list of the BGP peers we have defined. Our <code>host_vars</code> are our source of truth, meaning the only peers that should be configured are those in our <code>host_vars</code>.</p>
<p>The below <code>host_vars</code> are relevant: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">bgp</span>:
  <span style="color:#66d9ef">neighbors</span>:
    <span style="color:#66d9ef">ipv4</span>:
     - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">10.100.104.254</span>
     - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">192.0.2.204</span>
    <span style="color:#66d9ef">ipv6</span>:
     - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:104::ffff&#34;</span>
     - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:904:beef::2&#34;</span>
</code></pre></div><p>This generates the following list: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ok: [routeros-01] =&gt; {
    &#34;defined_peers&#34;: [
        &#34;10.100.104.254&#34;,
        &#34;192.0.2.204&#34;,
        &#34;2001:db8:104::ffff&#34;,
        &#34;2001:db8:904:beef::2&#34;
    ]
}
</code></pre></div><h6 id="creating-list-of-peers-to-delete">Creating list of peers to delete</h6>
<p>This task loops through the <code>configured_peers</code> variable, and checks if each peer exists in the <code>defined_peers</code> variable. If it doesn&rsquo;t, it is added to the variable <code>delete_peers</code>.</p>
<p>This generates something like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ok: [routeros-01] =&gt; {
    &#34;delete_peers&#34;: [
        &#34;192.0.2.255&#34;
    ]
}
</code></pre></div><h6 id="deleting-the-undefined-peers">Deleting the undefined peers</h6>
<p>This task loops through the <code>delete_peers</code> variable. For every peer, it will remove them from the configuration (IPv4 or IPv6). Based upon the last task, this would then generate the following command: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/routing bgp peer remove [find where remote-address=&#34;192.0.2.255&#34;]
</code></pre></div><p>Before this, our peers looked like: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-01] /routing bgp peer&gt; print brief
Flags: X - disabled, E - established
 #   INSTANCE        REMOTE-ADDRESS           REMOTE-AS
 0 E default         2001:db8:904:beef::2     65104
 1 E default         10.100.104.254           65430
 2 E default         2001:db8:104::ffff       65430
 3 E default         192.0.2.204              65104
 4   default         192.0.2.255              65001
</code></pre></div><p>After this, they look like: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-01] /routing bgp peer&gt; print brief
Flags: X - disabled, E - established
 #   INSTANCE        REMOTE-ADDRESS           REMOTE-AS
 0 E default         2001:db8:904:beef::2     65104
 1 E default         10.100.104.254           65430
 2 E default         2001:db8:104::ffff       65430
 3 E default         192.0.2.204              65104
</code></pre></div><h5 id="summary">Summary</h5>
<p>As mentioned, this is probably the most complex playbook in the series so far. As we have no modules to rely on, we achieve idempotence using RouterOS CLI features, regular expressions and complex list operations.</p>
<p>This does go some way to showing the power of Ansible, and in many ways the flexibility of RouterOS as well. You may not be benefiting from having any abstraction away from RouterOS, but it does allow you to make use of list comparisons, variables, regular expressions, looping and more.</p>
<h5 id="verification-5">Verification</h5>
<p>At the end of this playbook, we should have BGP sessions established over IPv4 and IPv6, as well as routes received and sent to the <strong>netsvr-01</strong> BGP route server.</p>
<p><strong>routeros-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show BGP neighbours on IPv4 and IPv6 
[admin@routeros-01] &gt; /routing bgp peer print
Flags: X - disabled, E - established
 #   INSTANCE                              REMOTE-ADDRESS                                                       REMOTE-AS
 0 E default                               2001:db8:904:beef::2                                                 65104
 1 E default                               10.100.104.254                                                       65430
 2 E default                               2001:db8:104::ffff                                                   65430
 3 E default                               192.0.2.204                                                          65104

! Show BGP routes
[admin@routeros-01] &gt; /ip route print where bgp
Flags: X - disabled, A - active, D - dynamic, C - connect, S - static, r - rip, b - bgp, o - ospf, m - mme,
B - blackhole, U - unreachable, P - prohibit
 #      DST-ADDRESS        PREF-SRC        GATEWAY            DISTANCE
 0 ADb  192.0.2.1/32                       10.100.104.254           20

[admin@routeros-01] &gt; /ipv6 route print where bgp
Flags: X - disabled, A - active, D - dynamic, C - connect, S - static, r - rip, o - ospf, b - bgp, U - unreachable
 #      DST-ADDRESS              GATEWAY                  DISTANCE
 0 ADb  2001:db8:999:beef::1/128 fe80::33a6:26b8:1b0e:...       20

! Ping the netsvr Loopback (192.0.2.1 and 2001:DB8:999:BEEF::1)
[admin@routeros-01] &gt; /ping 192.0.2.1
  SEQ HOST                                     SIZE TTL TIME  STATUS
    0 192.0.2.1                                  56  64 0ms
    sent=1 received=1 packet-loss=0% min-rtt=0ms avg-rtt=0ms max-rtt=0ms

[admin@routeros-01] &gt; /ping 2001:db8:999:beef::1
  SEQ HOST                                     SIZE TTL TIME  STATUS
    0 2001:db8:999:beef::1                       56  64 1ms   echo reply
    1 2001:db8:999:beef::1                       56  64 0ms   echo reply
    sent=2 received=2 packet-loss=0% min-rtt=0ms avg-rtt=0ms max-rtt=1ms
</code></pre></div><p><strong>routeros-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show BGP neighbours on IPv4 and IPv6 
[admin@routeros-02] &gt; /routing bgp peer print
Flags: X - disabled, E - established
 #   INSTANCE                              REMOTE-ADDRESS                                                       REMOTE-AS
 0 E default                               2001:db8:904:beef::1                                                 65104
 1 E default                               192.0.2.104                                                          65104

! Show BGP routes
[admin@routeros-02] &gt; /ip route print where bgp
Flags: X - disabled, A - active, D - dynamic, C - connect, S - static, r - rip, b - bgp, o - ospf, m - mme,
B - blackhole, U - unreachable, P - prohibit
 #      DST-ADDRESS        PREF-SRC        GATEWAY            DISTANCE
 0 ADb  0.0.0.0/0                          192.0.2.104             200
 1  Db  10.15.30.0/24                      192.0.2.104             200
 2  Db  10.100.104.0/24                    192.0.2.104             200
 3  Db  10.100.204.0/24                    192.0.2.104             200
 4 ADb  192.0.2.1/32                       10.100.104.254          200
 5  Db  192.0.2.104/32                     192.0.2.104             200
 6  Db  192.0.2.204/32                     192.0.2.104             200
 7 ADb  192.168.122.0/24                   192.0.2.104             200

[admin@routeros-02] &gt; /ipv6 route print where bgp
Flags: X - disabled, A - active, D - dynamic, C - connect, S - static, r - rip, o - ospf, b - bgp, U - unreachable
 #      DST-ADDRESS              GATEWAY                  DISTANCE
 0  Db  2001:db8:104::/64        2001:db8:904:beef::1          200
 1  Db  2001:db8:204::/64        2001:db8:904:beef::1          200
 2  Db  2001:db8:904:beef::1/128 2001:db8:904:beef::1          200
 3  Db  2001:db8:904:beef::2/128 2001:db8:904:beef::1          200
 4  Db  2001:db8:999:beef::1/128 2001:db8:904:beef::1          200

! Ping the netsvr Loopback (192.0.2.1 and 2001:DB8:999:BEEF::1)
[admin@routeros-02] &gt;  /ping 192.0.2.1
  SEQ HOST                                     SIZE TTL TIME  STATUS
    0 192.0.2.1                                  56  63 0ms
    1 192.0.2.1                                  56  63 1ms
    2 192.0.2.1                                  56  63 1ms
    sent=3 received=3 packet-loss=0% min-rtt=0ms avg-rtt=0ms max-rtt=1ms

[admin@routeros-02] &gt; /ping 2001:db8:999:beef::1
  SEQ HOST                                     SIZE TTL TIME  STATUS
    0                                                         no route to host
    1                                                         no route to host
    sent=2 received=0 packet-loss=100%
</code></pre></div><p>Hmm, the last ping isn&rsquo;t working. Why is that? Lets have a look at the routing table: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Check the route
[admin@routeros-02] /routing bgp instance&gt; /ipv6 route print where dst-address=2001:db8:999:beef::1/128
Flags: X - disabled, A - active, D - dynamic, C - connect, S - static, r - rip, o - ospf, b - bgp, U - unreachable
 #      DST-ADDRESS              GATEWAY                  DISTANCE
 0  Db  2001:db8:999:beef::1/128 2001:db8:904:beef::1          200

! Not active? Why?
[admin@routeros-02] /routing bgp instance&gt; /ipv6 route print detail where dst-address=2001:db8:999:beef::1/128
Flags: X - disabled, A - active, D - dynamic, C - connect, S - static, r - rip, o - ospf, b - bgp, U - unreachable
 0  Db  dst-address=2001:db8:999:beef::1/128 gateway=2001:db8:904:beef::1 gateway-status=2001:db8:904:beef::1 unreachable distance=200 scope=40 target-scope=30 bgp-as-path=&#34;65430&#34; bgp-local-pref=100 bgp-med=0 bgp-origin=igp
        received-from=routeros-01-v6
</code></pre></div><p>Unreachable? Lets have a look at that gateway (i.e. the next-hop)</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-02] /routing bgp instance&gt; /ipv6 route print where dst-address=2001:db8:904:beef::1
Flags: X - disabled, A - active, D - dynamic, C - connect, S - static, r - rip, o - ospf, b - bgp, U - unreachable
 #      DST-ADDRESS              GATEWAY                  DISTANCE
 0 ADo  2001:db8:904:beef::1/128 fe80::5054:ff:fec0:5d...      110
 1  Db  2001:db8:904:beef::1/128 2001:db8:904:beef::1          200
</code></pre></div><p>So the next-hop is received via OSPF (v3). Why is it being marked as unreachable?</p>
<p>Unfortunately this is because of a bug in RouterOS. It is not able to use next-hops that are <a href="https://en.wikipedia.org/wiki/Link-local_address">link-local</a> for BGP next-hop recursion. This <a href="https://forum.mikrotik.com/viewtopic.php?f=15&amp;t=42268">MikroTik forum post</a> (with over a hundred replies) details the issue further.</p>
<p>This makes the internal IPv6 BGP peering unusable. There are ways of statically setting next hops, or using filtering to override next hops, but this goes against using a dynamic internal routing protocol.</p>
<h3 id="snmp">SNMP</h3>
<p>In this section we enable SNMP for monitoring the routers.</p>
<h4 id="playbook-3">Playbook</h4>
<p>The contents of the playbook are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for snmp</span>
- <span style="color:#66d9ef">name</span>: Enable SNMPv3
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /snmp set contact=<span style="color:#e6db74">&#34;{{ snmp[&#39;contact&#39;] }}&#34;</span> location=<span style="color:#e6db74">&#34;{{ snmp[&#39;location&#39;] }}&#34;</span>
      - /snmp community set <span style="color:#ae81ff">0</span> name=<span style="color:#e6db74">&#34;{{ snmp[&#39;user&#39;] }}&#34;</span>
      - /snmp community set <span style="color:#ae81ff">0</span> encryption-protocol=<span style="color:#e6db74">&#34;AES&#34;</span> encryption-password=<span style="color:#e6db74">&#34;{{ snmp[&#39;priv_key&#39;] }}&#34;</span>
      - /snmp community set <span style="color:#ae81ff">0</span> authentication-protocol=<span style="color:#e6db74">&#34;SHA1&#34;</span> authentication-password=<span style="color:#e6db74">&#34;{{ snmp[&#39;auth_key&#39;] }}&#34;</span>
      - /snmp community set <span style="color:#ae81ff">0</span> security=privacy
      - /snmp set enabled=yes
  <span style="color:#66d9ef">tags</span>:
    - snmp
</code></pre></div><p>We gather our SNMP details and credentials from <code>host_vars</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">snmp</span>:
  <span style="color:#66d9ef">location</span>: Yeti Home
  <span style="color:#66d9ef">contact</span>: The Hairy One
  <span style="color:#66d9ef">user</span>: yetiops
  <span style="color:#66d9ef">group</span>: yetiops_group
  <span style="color:#66d9ef">auth_key</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">            $ANSIBLE_VAULT;1.1;AES256</span>
            <span style="color:#ae81ff">383</span><span style="color:#75715e">###REDACTED###############################################################566</span>
            <span style="color:#ae81ff">363</span><span style="color:#75715e">###REDACTED###############################################################366</span>
            <span style="color:#ae81ff">343</span><span style="color:#75715e">###REDACTED###############################################################565</span>
            <span style="color:#ae81ff">326</span><span style="color:#75715e">###REDACTED###############################################################362</span>
            <span style="color:#ae81ff">3431</span>
  <span style="color:#66d9ef">priv_key</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">            $ANSIBLE_VAULT;1.1;AES256</span>
            <span style="color:#ae81ff">383</span><span style="color:#75715e">###REDACTED###############################################################566</span>
            <span style="color:#ae81ff">363</span><span style="color:#75715e">###REDACTED###############################################################366</span>
            <span style="color:#ae81ff">343</span><span style="color:#75715e">###REDACTED###############################################################565</span>
            <span style="color:#ae81ff">326</span><span style="color:#75715e">###REDACTED###############################################################362</span>
            <span style="color:#ae81ff">3431</span>
</code></pre></div><p>This generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/snmp set contact=&#34;The Hairy One&#34; location=&#34;Yeti Home&#34;
/snmp community set 0 name=&#34;yetiops&#34;
/snmp community set 0 encryption-protocol=&#34;AES&#34; encryption-password=&#34;###PRIV_KEY###&#34;
/snmp community set 0 authentication-protocol=&#34;SHA1&#34; authentication-password=&#34;###AUTH_KEY###&#34;
/snmp community set 0 security=privacy
/snmp set enabled=yes
</code></pre></div><p>We can verify this with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">admin@routeros-01] &gt; /snmp community print detail
Flags: * - default
 0 * name=&#34;yetiops&#34; addresses=::/0 security=none read-access=yes write-access=no authentication-protocol=SHA1 encryption-protocol=AES
     authentication-password=&#34;###AUTH_KEY###&#34; encryption-password=&#34;###PRIV_KEY###&#34;

[admin@routeros-01] &gt; /snmp print
          enabled: yes
          contact: The Hairy One
         location: Yeti Home
        engine-id:
      src-address: ::
      trap-target:
   trap-community: yetiops
     trap-version: 1
  trap-generators: temp-exception
</code></pre></div><h4 id="verification-6">Verification</h4>
<p>To check whether this is working, you will need either some form of monitoring system, or you can use something like <code>snmpwalk</code> to check: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">! snmpwalk to routeros-01
$ snmpwalk -v3 -u yetiops -a SHA -A <span style="color:#75715e">###AUTH-KEY### -x AES -X ###PRIV-KEY### -l authPriv 10.15.30.53</span>
iso.3.6.1.2.1.1.1.0 <span style="color:#f92672">=</span> STRING: <span style="color:#e6db74">&#34;RouterOS CHR&#34;</span>
iso.3.6.1.2.1.1.2.0 <span style="color:#f92672">=</span> OID: iso.3.6.1.4.1.14988.1
iso.3.6.1.2.1.1.3.0 <span style="color:#f92672">=</span> Timeticks: <span style="color:#f92672">(</span>369200<span style="color:#f92672">)</span> 1:01:32.00
iso.3.6.1.2.1.1.4.0 <span style="color:#f92672">=</span> STRING: <span style="color:#e6db74">&#34;The Hairy One&#34;</span>
iso.3.6.1.2.1.1.5.0 <span style="color:#f92672">=</span> STRING: <span style="color:#e6db74">&#34;routeros-01&#34;</span>
iso.3.6.1.2.1.1.6.0 <span style="color:#f92672">=</span> STRING: <span style="color:#e6db74">&#34;Yeti Home&#34;</span>
iso.3.6.1.2.1.1.7.0 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">78</span>
iso.3.6.1.2.1.2.1.0 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">6</span>
iso.3.6.1.2.1.2.2.1.1.1 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">1</span>
iso.3.6.1.2.1.2.2.1.1.2 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">2</span>
iso.3.6.1.2.1.2.2.1.1.3 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">3</span>
iso.3.6.1.2.1.2.2.1.1.12 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">12</span>
iso.3.6.1.2.1.2.2.1.1.30 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">30</span>
iso.3.6.1.2.1.2.2.1.1.32 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">32</span>
<span style="color:#f92672">[</span>...<span style="color:#f92672">]</span>

! snmpwalk to routeros-02
$ snmpwalk -v3 -u yetiops -a SHA -A <span style="color:#75715e">###AUTH-KEY### -x AES -X ###PRIV-KEY### -l authPriv 10.15.30.54</span>
iso.3.6.1.2.1.1.1.0 <span style="color:#f92672">=</span> STRING: <span style="color:#e6db74">&#34;RouterOS CHR&#34;</span>
iso.3.6.1.2.1.1.2.0 <span style="color:#f92672">=</span> OID: iso.3.6.1.4.1.14988.1
iso.3.6.1.2.1.1.3.0 <span style="color:#f92672">=</span> Timeticks: <span style="color:#f92672">(</span>378200<span style="color:#f92672">)</span> 1:03:02.00
iso.3.6.1.2.1.1.4.0 <span style="color:#f92672">=</span> STRING: <span style="color:#e6db74">&#34;The Hairy One&#34;</span>
iso.3.6.1.2.1.1.5.0 <span style="color:#f92672">=</span> STRING: <span style="color:#e6db74">&#34;routeros-02&#34;</span>
iso.3.6.1.2.1.1.6.0 <span style="color:#f92672">=</span> STRING: <span style="color:#e6db74">&#34;Yeti Home&#34;</span>
iso.3.6.1.2.1.1.7.0 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">78</span>
iso.3.6.1.2.1.2.1.0 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">4</span>
iso.3.6.1.2.1.2.2.1.1.1 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">1</span>
iso.3.6.1.2.1.2.2.1.1.2 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">2</span>
iso.3.6.1.2.1.2.2.1.1.7 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">7</span>
iso.3.6.1.2.1.2.2.1.1.16 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">16</span>
<span style="color:#f92672">[</span>...<span style="color:#f92672">]</span>
</code></pre></div><p>All looking good!</p>
<h3 id="nat">NAT</h3>
<p>In this section, we allow the <strong>internal</strong> router to reach the internet via the <strong>edge</strong> router. The <strong>edge</strong> router has a default route to the internet that is learned from DHCP. NAT is used because the internet does not know about our private IP addresses, requiring us to masquerade behind a public IP address (or addresses) to route to and from the internet.</p>
<h4 id="playbook-4">Playbook</h4>
<p>The playbook looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for nat</span>
- <span style="color:#66d9ef">name</span>: Apply NAT Masquerade
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /ip firewall nat remove [find where out-interface=<span style="color:#e6db74">&#34;{{ item.routeros_if }}&#34;</span>]
      - /ip firewall nat add chain=srcnat action=masquerade out-interface=<span style="color:#e6db74">&#34;{{ item.routeros_if }}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - item.nat is defined
    - item.nat.role is defined
    - item.nat.role is match(<span style="color:#e6db74">&#39;outside&#39;</span>)
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - nat
</code></pre></div><p>In the above, we are looking for whether we have a the <code>nat.role</code> variable on any interface in our <code>host_vars</code>, and if it is set to <code>outside</code>.</p>
<p>The relevant <code>host_vars</code> are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;ether1&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;ether2&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;vlan104&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;vlan204&#34;</span>
    <span style="color:#66d9ef">nat</span>:
      <span style="color:#66d9ef">role</span>: inside
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;ether3&#34;</span>
    <span style="color:#66d9ef">nat</span>:
      <span style="color:#66d9ef">role</span>: outside
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;loopback0&#34;</span>
</code></pre></div><p>In the above, only the <code>ether3</code> interface has the <code>outside</code> NAT role, so we generate the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/ip firewall nat remove [find where out-interface=&#34;ether3&#34;]
/ip firewall nat add chain=srcnat action=masquerade out-interface=&#34;ether3&#34;
</code></pre></div><p>We remove the existing configuration first, to ensure that any manual changes are rectified, as well as not creating multiple identical configuration items. Without this, you would see the line for NAT via <code>ether3</code> configured again every time you run the playbook.</p>
<p>We can verify this with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-01] /routing bgp peer&gt; /ip firewall nat print
Flags: X - disabled, I - invalid, D - dynamic
 0    chain=srcnat action=masquerade out-interface=ether3
</code></pre></div><h4 id="verification-7">Verification</h4>
<p>We can now test from the <strong>internal</strong> router, to see if it can reach the internet: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Can we reach the internet?
[admin@routeros-02] &gt; /ping 8.8.8.8
  SEQ HOST                                     SIZE TTL TIME  STATUS
    0 8.8.8.8                                    56  52 21ms
    1 8.8.8.8                                    56  52 20ms
    sent=2 received=2 packet-loss=0% min-rtt=20ms avg-rtt=20ms max-rtt=21ms

[admin@routeros-02] &gt;
[admin@routeros-02] &gt; /ping 1.1.1.1
  SEQ HOST                                     SIZE TTL TIME  STATUS
    0 1.1.1.1                                    56  55 14ms
    1 1.1.1.1                                    56  55 14ms
    2 1.1.1.1                                    56  55 13ms
    sent=3 received=3 packet-loss=0% min-rtt=13ms avg-rtt=13ms max-rtt=14ms

[admin@routeros-02] &gt; /ping 1.1.1.1 src-address=192.0.2.204
  SEQ HOST                                     SIZE TTL TIME  STATUS
    0 1.1.1.1                                    56  55 17ms
    1 1.1.1.1                                    56  55 15ms
    2 1.1.1.1                                    56  55 12ms
    sent=3 received=3 packet-loss=0% min-rtt=12ms avg-rtt=14ms max-rtt=17ms

[admin@routeros-02] &gt; /ping 8.8.8.8 src-address=192.0.2.204
  SEQ HOST                                     SIZE TTL TIME  STATUS
    0 8.8.8.8                                    56  52 19ms
    1 8.8.8.8                                    56  52 19ms
    sent=2 received=2 packet-loss=0% min-rtt=19ms avg-rtt=19ms max-rtt=19ms

! What does this look like on the edge router?
[admin@routeros-01] &gt; /ip firewall nat print stats
Flags: X - disabled, I - invalid, D - dynamic
 #    CHAIN     ACTION           BYTES         PACKETS
 0    srcnat    masquerade       5 662              94
</code></pre></div><p>All looking good!</p>
<h3 id="aaa">AAA</h3>
<p>The final task is AAA (<strong>A</strong>uthentication, <strong>A</strong>uthorization and <strong>A</strong>ccounting). As noted, MikroTik does not work with TACACS+, so we use RADIUS instead. This performs AAA against <strong>freeradius</strong> running on the <strong>netsvr-01</strong> machine.</p>
<p>This will allow central management of our users, as well as providing logs of commands being run.</p>
<h4 id="playbook-5">Playbook</h4>
<p>The contents of the playbook are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for aaa</span>
- <span style="color:#66d9ef">name</span>: Enable RADIUS
  <span style="color:#66d9ef">routeros_command</span>:
    <span style="color:#66d9ef">commands</span>:
      - /radius remove [find where address=<span style="color:#e6db74">&#34;{{ tacacs[&#39;ipv4&#39;] }}&#34;</span>]
      - /radius add service=login address=<span style="color:#e6db74">&#34;{{ tacacs[&#39;ipv4&#39;] }}&#34;</span> secret=<span style="color:#e6db74">&#34;{{ radius[&#39;secret&#39;] }}&#34;</span> src-address=<span style="color:#e6db74">&#34;{{ router_id }}&#34;</span> comment=<span style="color:#e6db74">&#34;netsvr-01&#34;</span>
      - /user aaa set use-radius=yes
  <span style="color:#66d9ef">tags</span>:
  - aaa
</code></pre></div><p>While it may seem a little strange using the <code>tacacs</code> variables for the server address, if MikroTik do support TACACS+ in the future, it will make transitioning between the two easier.</p>
<p>The relevant <code>host_vars</code> are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">router_id</span>: <span style="color:#ae81ff">192.0.2.104</span>
<span style="color:#66d9ef">radius</span>:
  <span style="color:#66d9ef">secret</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">          $ANSIBLE_VAULT;1.1;AES256</span>
          <span style="color:#ae81ff">356431</span><span style="color:#75715e">################REDACTED###########################31313136626333</span>
          <span style="color:#ae81ff">623664</span><span style="color:#75715e">################REDACTED###########################65366437633463</span>
          <span style="color:#ae81ff">623135</span><span style="color:#75715e">################REDACTED###########################33346233346665</span>
          <span style="color:#ae81ff">633265</span><span style="color:#75715e">################REDACTED###########################63333834396361</span>
          <span style="color:#ae81ff">333835</span><span style="color:#75715e">################REDACTED###########################13936</span>
</code></pre></div><p>The rest of our variables come from our <code>group_vars</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">tacacs</span>:
  <span style="color:#66d9ef">ipv4</span>: <span style="color:#ae81ff">192.0.2.1</span>
</code></pre></div><p>The above generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">/radius remove [find where address=&#34;192.0.2.1&#34;]
/radius add service=login address=&#34;192.0.2.1&#34; secret=&#34;###RADIUS_SECRET###&#34; src-address=&#34;192.0.2.104&#34; comment=&#34;netsvr-01&#34;
/user aaa set use-radius=yes
</code></pre></div><p>Again, we remove the existing configuration to ensure that any manual changes are overridden and to avoid generating duplicate configuration.</p>
<p>We can verify this with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[admin@routeros-01] &gt; /radius print
Flags: X - disabled
 #   SERVICE                  CALLED-ID    DOMAIN    ADDRESS       SECRET
 0   ;;; netsvr-01
     login                                           192.0.2.1     $RADIUS_SECRET
</code></pre></div><h4 id="verification-8">Verification</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Can we login with the yetiops user?
ssh yetiops@10.15.30.54
yetiops@10.15.30.54&#39;s password:

  MMM      MMM       KKK                          TTTTTTTTTTT      KKK
  MMMM    MMMM       KKK                          TTTTTTTTTTT      KKK
  MMM MMMM MMM  III  KKK  KKK  RRRRRR     OOOOOO      TTT     III  KKK  KKK
  MMM  MM  MMM  III  KKKKK     RRR  RRR  OOO  OOO     TTT     III  KKKKK
  MMM      MMM  III  KKK KKK   RRRRRR    OOO  OOO     TTT     III  KKK KKK
  MMM      MMM  III  KKK  KKK  RRR  RRR   OOOOOO      TTT     III  KKK  KKK

  MikroTik RouterOS 6.45.8 (c) 1999-2020       http://www.mikrotik.com/

[?]             Gives the list of available commands
command [?]     Gives help on the command and list of arguments

[Tab]           Completes the command/word. If the input is ambiguous,
                a second [Tab] gives possible options

/               Move up to base level
..              Move up one level
/command        Use command at the base level

      ----------------------------------------
      |
      | This banner was generated by Ansible
      |
      ----------------------------------------
      |
      | You are logged into routeros-02
      |
      ----------------------------------------

[yetiops@routeros-02] &gt;

! What about a user that doesn&#39;t exist?
$ ssh jeff@10.15.30.54
jeff@10.15.30.54&#39;s password:
Permission denied, please try again.

!! This attempt was logged to the router and syslog !!
[admin@routeros-02] &gt;
23:01:53 echo: system,error,critical login failure for user jeff from 10.15.30.1 via ssh

! What do see in our radius log?
Tue May 19 00:00:45 2020 : Auth: (2) Login OK: [yetiops/&lt;via Auth-Type = mschap&gt;] (from client routeros-02 port 0 cli 10.15.30.1)
Tue May 19 00:01:50 2020 : Auth: (5) Login incorrect (mschap: FAILED: No NT/LM-Password.  Cannot perform authentication): [jeff/&lt;via Auth-Type = mschap&gt;] (from client routeros-02 port 0 cli 10.15.30.1)

! What if freeradius goes away?
[root@netsvr-01 /var/log/radius] $ systemctl stop radiusd
[root@netsvr-01 /var/log/radius] $ systemctl status radiusd
 radiusd.service - FreeRADIUS high performance RADIUS server.
   Loaded: loaded (/usr/lib/systemd/system/radiusd.service; enabled; vendor preset: disabled)
   Active: inactive (dead) since Tue 2020-05-19 00:04:37 BST; 5s ago
  Process: 1682 ExecStart=/usr/sbin/radiusd -d /etc/raddb (code=exited, status=0/SUCCESS)
  Process: 1523 ExecStartPre=/usr/sbin/radiusd -C (code=exited, status=0/SUCCESS)
  Process: 1514 ExecStartPre=/bin/chown -R radiusd.radiusd /var/run/radiusd (code=exited, status=0/SUCCESS)
 Main PID: 1684 (code=exited, status=0/SUCCESS)

May 18 22:37:42 netsvr-01 systemd[1]: Starting FreeRADIUS high performance RADIUS server....
May 18 22:37:43 netsvr-01 systemd[1]: Started FreeRADIUS high performance RADIUS server..
May 19 00:04:37 netsvr-01 systemd[1]: Stopping FreeRADIUS high performance RADIUS server....
May 19 00:04:37 netsvr-01 systemd[1]: Stopped FreeRADIUS high performance RADIUS server..

$ ssh yetiops@10.15.30.54
yetiops@10.15.30.54&#39;s password:
Permission denied, please try again.

$ ssh ansible@10.15.30.54
ansible@10.15.30.54&#39;s password:


  MMM      MMM       KKK                          TTTTTTTTTTT      KKK
  MMMM    MMMM       KKK                          TTTTTTTTTTT      KKK
  MMM MMMM MMM  III  KKK  KKK  RRRRRR     OOOOOO      TTT     III  KKK  KKK
  MMM  MM  MMM  III  KKKKK     RRR  RRR  OOO  OOO     TTT     III  KKKKK
  MMM      MMM  III  KKK KKK   RRRRRR    OOO  OOO     TTT     III  KKK KKK
  MMM      MMM  III  KKK  KKK  RRR  RRR   OOOOOO      TTT     III  KKK  KKK

  MikroTik RouterOS 6.45.8 (c) 1999-2020       http://www.mikrotik.com/

[?]             Gives the list of available commands
command [?]     Gives help on the command and list of arguments

[Tab]           Completes the command/word. If the input is ambiguous,
                a second [Tab] gives possible options

/               Move up to base level
..              Move up one level
/command        Use command at the base level

      ----------------------------------------
      |
      | This banner was generated by Ansible
      |
      ----------------------------------------
      |
      | You are logged into routeros-02
      |
      ----------------------------------------
may/18/2020 23:01:53 system,error,critical login failure for user jeff from 10.15.30.1 via ssh
may/18/2020 23:05:06 system,error,critical login failure for user yetiops from 10.15.30.1 via ssh
</code></pre></div><p>All looking good!</p>
<h3 id="parent-playbook">Parent playbook</h3>
<p>The parent playbook (i.e. the playbook that brings all the roles together) is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
- <span style="color:#66d9ef">hosts</span>: mikrotik
  <span style="color:#66d9ef">gather_facts</span>: no
  <span style="color:#66d9ef">tasks</span>:
  - <span style="color:#66d9ef">import_role</span>:
      <span style="color:#66d9ef">name</span>: system
  - <span style="color:#66d9ef">import_role</span>:
      <span style="color:#66d9ef">name</span>: interfaces
  - <span style="color:#66d9ef">import_role</span>:
      <span style="color:#66d9ef">name</span>: firewall
  - <span style="color:#66d9ef">import_role</span>:
      <span style="color:#66d9ef">name</span>: routing
  - <span style="color:#66d9ef">import_role</span>:
      <span style="color:#66d9ef">name</span>: snmp
  - <span style="color:#66d9ef">import_role</span>:
      <span style="color:#66d9ef">name</span>: nat
  - <span style="color:#66d9ef">import_role</span>:
      <span style="color:#66d9ef">name</span>: aaa
</code></pre></div><p>All configuration changes are saved immediately, so there is no need to run a task to save the final configuration at the end.</p>
<h4 id="safe-mode">Safe mode</h4>
<p>RouterOS has has a feature known as &ldquo;safe mode&rdquo;. This allows you to apply configuration, and if it detects that your console/SSH session has timed out or reset, it will roll back all of the configuration applied within &ldquo;safe mode&rdquo;.</p>
<p>This isn&rsquo;t as comprehensive as JunOS&rsquo;s configuration commits, but it does mean that you can apply changes without losing access to the device.</p>
<p>There are cases where you may expect to lose access briefly (e.g. changing of IP addressing or routing) that safe mode does not account for these. However as a basic automatic rollback measure, it is a nice option to have.</p>
<p>To enter MikroTik&rsquo;s safe mode, use the key combination <code>CTRL+x</code>. You will then be presented with this prompt: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">[ansible@routeros-02] &gt;
[Safe Mode taken]
[ansible@routeros-02] &lt;SAFE&gt;
</code></pre></div><p>When you are finished, press <code>CTRL+x</code> to leave safe mode, &ldquo;committing&rdquo; the changes you made.</p>
<p>One point to note is that if you do not exit safe mode,  any changes you make will eventually be rolled back (due to SSH or TCP timeouts). I have experienced this myself, rolling back important spanning tree changes that ended up causing a loop, long after I&rsquo;d left the office that day!</p>
<h4 id="role-order">Role Order</h4>
<p>The role order is very similar to IOS and JunOS. The main difference here from IOS is that we are using stateful firewalling rather than access lists. The justification for the role order is detailed within <a href="/posts/ansible-for-networking-part-3-cisco-ios">Part 3 - Cisco IOS</a>. To summarize: -</p>
<ul>
<li><code>system</code> - Sets up logging, hostnames and banners
<ul>
<li>This ensure we have logging ready for if any of the other roles fail (that the Ansible debug output cannot help with)</li>
</ul>
</li>
<li><code>interfaces</code> - This is a prerequisite for most of the following tasks</li>
<li><code>firewall</code> - Apply before routing so that the device is not open to the world when publicly routable
<ul>
<li>This has to come after the interfaces, as some of the filters and lists are dependent on interfaces existing</li>
</ul>
</li>
<li><code>routing</code> - Routing is required for NAT and AAA to function</li>
<li><code>snmp</code> - No dependency on any service, so this can go anywhere</li>
<li><code>nat</code> - Apply this after routing, otherwise the <strong>internal</strong> router has no default route to reach external destinations anyway</li>
<li><code>aaa</code> - It depends upon routing, and if configured incorrectly it can break login sessions</li>
</ul>
<p>Running <code>aaa</code> last means that all other configuration is complete. If login sessions do break due to the <code>aaa</code> configuration, you have a fully configured router that is potentially reachable via other means (e.g. Out-of-Band, Admin accounts)</p>
<h2 id="artifacts">Artifacts</h2>
<p>The final directory structure looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">$ tree -L 2
.
├── ansible.cfg
├── ansible.log
├── group_vars
│   └── mikrotik
├── host_vars
│   ├── routeros-01.yaml
│   └── routeros-02.yaml
├── inventory
├── mikrotik.yaml
└── roles
    ├── aaa
    ├── firewall
    ├── interfaces
    ├── nat
    ├── routing
    ├── snmp
    └── system
</code></pre></div><p>The final contents of our <code>group_vars</code> are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">ansible_user</span>: ansible
<span style="color:#66d9ef">ansible_connection</span>: network_cli
<span style="color:#66d9ef">ansible_network_os</span>: routeros
<span style="color:#66d9ef">ansible_ssh_pass</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">                  $ANSIBLE_VAULT;1.1;AES256</span>
                  <span style="color:#ae81ff">383</span><span style="color:#75715e">###REDACTED###############################################################566</span>
                  <span style="color:#ae81ff">363</span><span style="color:#75715e">###REDACTED###############################################################366</span>
                  <span style="color:#ae81ff">343</span><span style="color:#75715e">###REDACTED###############################################################565</span>
                  <span style="color:#ae81ff">326</span><span style="color:#75715e">###REDACTED###############################################################362</span>
                  <span style="color:#ae81ff">3431</span>
<span style="color:#66d9ef">log_host</span>: <span style="color:#ae81ff">10.100.104.254</span>
<span style="color:#66d9ef">tacacs</span>:
  <span style="color:#66d9ef">ipv4</span>: <span style="color:#ae81ff">192.0.2.1</span>
  <span style="color:#66d9ef">secret</span>: supersecret
<span style="color:#66d9ef">snmp</span>:
  <span style="color:#66d9ef">location</span>: Yeti Home
  <span style="color:#66d9ef">contact</span>: The Hairy One
  <span style="color:#66d9ef">user</span>: yetiops
  <span style="color:#66d9ef">group</span>: yetiops_group
  <span style="color:#66d9ef">auth_key</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">            $ANSIBLE_VAULT;1.1;AES256</span>
            <span style="color:#ae81ff">383</span><span style="color:#75715e">###REDACTED###############################################################566</span>
            <span style="color:#ae81ff">363</span><span style="color:#75715e">###REDACTED###############################################################366</span>
            <span style="color:#ae81ff">343</span><span style="color:#75715e">###REDACTED###############################################################565</span>
            <span style="color:#ae81ff">326</span><span style="color:#75715e">###REDACTED###############################################################362</span>
            <span style="color:#ae81ff">3431</span>
  <span style="color:#66d9ef">priv_key</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">            $ANSIBLE_VAULT;1.1;AES256</span>
            <span style="color:#ae81ff">386</span><span style="color:#75715e">###REDACTED###############################################################764</span>
            <span style="color:#ae81ff">613</span><span style="color:#75715e">###REDACTED###############################################################630</span>
            <span style="color:#ae81ff">646</span><span style="color:#75715e">###REDACTED###############################################################331</span>
            <span style="color:#ae81ff">376</span><span style="color:#75715e">###REDACTED###############################################################137</span>
            <span style="color:#ae81ff">3563</span>
</code></pre></div><p>The final contents of our <code>host_vars</code> are: -</p>
<p><strong>routeros-01.yaml</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">router_id</span>: <span style="color:#ae81ff">192.0.2.104</span>
<span style="color:#66d9ef">rtr_role</span>: edge
<span style="color:#66d9ef">bgp</span>:
  <span style="color:#66d9ef">local_as</span>: <span style="color:#ae81ff">65104</span>
  <span style="color:#66d9ef">redist</span>:
    <span style="color:#66d9ef">ospf</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">neighbors</span>:
    <span style="color:#66d9ef">ipv4</span>:
     - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">10.100.104.254</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65430</span>
       <span style="color:#66d9ef">ebgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">name</span>: netsvr<span style="color:#ae81ff">-01</span>-v4
       <span style="color:#66d9ef">acl</span>: bgp-ipv4-peers
     - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">192.0.2.204</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65104</span>
       <span style="color:#66d9ef">ibgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">update_source</span>: loopback0
       <span style="color:#66d9ef">default_originate</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">name</span>: routeros<span style="color:#ae81ff">-02</span>-v4
    <span style="color:#66d9ef">ipv6</span>:
     - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:104::ffff&#34;</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65430</span>
       <span style="color:#66d9ef">ebgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">name</span>: netsvr<span style="color:#ae81ff">-01</span>-v6
       <span style="color:#66d9ef">acl</span>: bgp-ipv6-peers
     - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:904:beef::2&#34;</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65104</span>
       <span style="color:#66d9ef">ibgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">update_source</span>: loopback0
       <span style="color:#66d9ef">name</span>: routeros<span style="color:#ae81ff">-02</span>-v6
<span style="color:#66d9ef">vlans</span>:
  - <span style="color:#66d9ef">name</span>: netsvr<span style="color:#ae81ff">-01</span>
    <span style="color:#66d9ef">vlan_id</span>: <span style="color:#ae81ff">104</span>
    <span style="color:#66d9ef">interface</span>: ether2
  - <span style="color:#66d9ef">name</span>: routeros<span style="color:#ae81ff">-02</span>
    <span style="color:#66d9ef">vlan_id</span>: <span style="color:#ae81ff">204</span>
    <span style="color:#66d9ef">interface</span>: ether2
<span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;ether1&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Management&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.15.30.53/24&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;ether2&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;VLAN Bridge&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;vlan104&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To netsvr&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.100.104.253/24&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:104::f/64&#34;</span>
    <span style="color:#66d9ef">acl</span>:
      <span style="color:#66d9ef">ipv4</span>:
        - bgp-ipv4-peers
        - syslog
        - aaa
      <span style="color:#66d9ef">ipv6</span>:
        - bgp-ipv6-peers
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;vlan204&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To routeros-02&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.100.204.254/24&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:204::a/64&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
    <span style="color:#66d9ef">nat</span>:
      <span style="color:#66d9ef">role</span>: inside
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;ether3&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To the Internet&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;dhcp&#34;</span>
    <span style="color:#66d9ef">nat</span>:
      <span style="color:#66d9ef">role</span>: outside
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;loopback0&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;192.0.2.104/32&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:904:beef::1/128&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
<span style="color:#66d9ef">radius</span>:
  <span style="color:#66d9ef">secret</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">          $ANSIBLE_VAULT;1.1;AES256</span>
          <span style="color:#ae81ff">356</span><span style="color:#75715e">###REDACTED###############################################################33</span>
          <span style="color:#ae81ff">623</span><span style="color:#75715e">###REDACTED###############################################################63</span>
          <span style="color:#ae81ff">623</span><span style="color:#75715e">###REDACTED###############################################################65</span>
          <span style="color:#ae81ff">633</span><span style="color:#75715e">###REDACTED###############################################################61</span>
          <span style="color:#ae81ff">333</span><span style="color:#75715e">###REDACTED###########################################36</span>
</code></pre></div><p><strong>routeros-02.yaml</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">router_id</span>: <span style="color:#ae81ff">192.0.2.204</span>
<span style="color:#66d9ef">rtr_role</span>: internal
<span style="color:#66d9ef">bgp</span>:
  <span style="color:#66d9ef">local_as</span>: <span style="color:#ae81ff">65104</span>
  <span style="color:#66d9ef">neighbors</span>:
    <span style="color:#66d9ef">ipv4</span>:
     - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">192.0.2.104</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65104</span>
       <span style="color:#66d9ef">ibgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">update_source</span>: loopback0
       <span style="color:#66d9ef">name</span>: routeros<span style="color:#ae81ff">-01</span>-v4
    <span style="color:#66d9ef">ipv6</span>:
     - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:904:beef::1&#34;</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65104</span>
       <span style="color:#66d9ef">ibgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">update_source</span>: loopback0
       <span style="color:#66d9ef">name</span>: routeros<span style="color:#ae81ff">-01</span>-v6
<span style="color:#66d9ef">vlans</span>:
  - <span style="color:#66d9ef">name</span>: routeros<span style="color:#ae81ff">-01</span>
    <span style="color:#66d9ef">vlan_id</span>: <span style="color:#ae81ff">204</span>
    <span style="color:#66d9ef">interface</span>: ether2
<span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;ether1&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Management&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;ether2&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;VLAN Bridge&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">subint</span>:
      <span style="color:#66d9ef">vlans</span>:
      - <span style="color:#ae81ff">204</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;vlan204&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To routeros-01&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.100.204.253/24&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:204::f/64&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
  - <span style="color:#66d9ef">routeros_if</span>: <span style="color:#e6db74">&#34;loopback0&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;192.0.2.204/32&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:904:beef::2/128&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
<span style="color:#66d9ef">radius</span>:
  <span style="color:#66d9ef">secret</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">          $ANSIBLE_VAULT;1.1;AES256</span>
          <span style="color:#ae81ff">356</span><span style="color:#75715e">###REDACTED###############################################################33</span>
          <span style="color:#ae81ff">623</span><span style="color:#75715e">###REDACTED###############################################################63</span>
          <span style="color:#ae81ff">623</span><span style="color:#75715e">###REDACTED###############################################################65</span>
          <span style="color:#ae81ff">633</span><span style="color:#75715e">###REDACTED###############################################################61</span>
          <span style="color:#ae81ff">333</span><span style="color:#75715e">###REDACTED###########################################36</span>
</code></pre></div><p>Despite the lack of modules, having this few variables to configure a fully functioning router is still incredibly useful, as well as being able to make idempotent updates of BGP peers, OSPF networks and more.</p>
<p>If you integrate these tasks with something like <a href="https://github.com/netbox-community/netbox">Netbox</a> to supply your variables, you can build a huge estate all from a single source of truth, with little to no manual configuration involved.</p>
<h2 id="running-the-playbooks">Running the playbooks</h2>
<p>Below is an Asciinema output of my terminal when running the playbooks, so you can see them being applied: -</p>
<script id="asciicast-331616" src="https://asciinema.org/a/331616.js" async></script>
<p>Unlike the other labs, no task is marked as changed. This is because Ansible is applying raw RouterOS commands, with no real feedback mechanism to say whether the changes were successful or not. In this scenario, Ansible defaults to the change being &ldquo;OK&rdquo;, unless the task times out or receives a failed exit code.</p>
<h2 id="native-modules-versus-routeros_command">Native modules versus <code>routeros_command</code></h2>
<p>Unlike the other vendors we have covered so far, we only have one module we can use for configuration, <code>routeros_command</code>. The only other Ansible native modules we have used are <code>set_fact</code> and <code>routeros_facts</code> to either create variables, or to retrieve data from the routers where it is available.</p>
<p>Whether other modules will be created for RouterOS is an unknown. Given that the syntax for removing or updating configuration differs from adding configuration, the Ansible modules would be more complex than those for IOS or JunOS (although not impossible).</p>
<h2 id="thoughts-compared-to-ios-junos-and-eos">Thoughts compared to IOS, JunOS and EOS</h2>
<p>I knew going into this part of the series that the lack of any native modules would increase the complexity of the roles. Having supported MikroTik routers earlier in my career, as well as running them in my own network, I know the quirks and useful features that were available.</p>
<p>For anyone new to RouterOS, you&rsquo;ll need to become familiar with the syntax before using configuring them with Ansible, otherwise you would struggle to extend the roles or create your own.</p>
<p>The experience of using Ansible for this compared to IOS, JunOS and EOS is vastly different. With IOS, JunOS and EOS, you can build Jinja2 templates if a module does not exist. This option is not available with RouterOS, meaning you need to know what you expect each command to do and what order to apply them to create repeatable tasks.</p>
<p>I still believe that using Ansible for RouterOS has many advantages over managing your network estate manually. You gain consistency in the changes being made, and the ability to manage devices en masse, both of which are huge benefits.</p>
<h2 id="summary-1">Summary</h2>
<p>Overall, managing RouterOS with Ansible is more of a challenge than it is with other vendors. However we still meet the goals of repeatable and consistent configuration changes. This alone for me outweighs the need to effectively create your own modules to achieve idempotence.</p>
<p>I am hoping that at some point in the future, using Jinja2 templates is an option. This would allow the use of filters and conditional logic within a template. We could then define eBGP and iBGP peers within the same task, or IPv4 and IPv6, and much more.</p>
<p>The final configs from the routers are in my <a href="https://gitlab.com/stuh84/network-automation-ansible/-/tree/master/mikrotik/configs">Network Automation with Ansible repository</a>. The next part of this series will cover configuring VyOS routers using Ansible.</p>
]]></content>
        </item>
        
        <item>
            <title>Ansible for Networking - Part 5: Arista EOS</title>
            <link>https://yetiops.net/posts/ansible-for-networking-part-5-arista-eos/</link>
            <pubDate>Mon, 20 Apr 2020 18:30:25 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/ansible-for-networking-part-5-arista-eos/</guid>
            <description>The fifth part of my ongoing series of posts on Ansible for Networking will cover Arista&amp;rsquo;s EOS. You can view the other posts in the series below: -
 Part 1 - Start of the series Part 2 - The Lab Environment Part 3 - Cisco IOS Part 4 - Juniper JunOS Part 6 - MikroTik RouterOS Part 7 - VyOS  All the playbooks, roles and variables used in this article are available in my Network Automation with Ansible repository</description>
            <content type="html"><![CDATA[<p>The fifth part of my ongoing series of posts on Ansible for Networking will cover Arista&rsquo;s EOS. You can view the other posts in the series below: -</p>
<ul>
<li><a href="/posts/ansible-for-networking-series">Part 1 - Start of the series</a></li>
<li><a href="/posts/ansible-for-networking-part-2-lab">Part 2 - The Lab Environment</a></li>
<li><a href="/posts/ansible-for-networking-part-3-cisco-ios">Part 3 - Cisco IOS</a></li>
<li><a href="/posts/ansible-for-networking-part-4-juniper-junos">Part 4 - Juniper JunOS</a></li>
<li><a href="/posts/ansible-for-networking-part-6-mikrotik-routeros">Part 6 - MikroTik RouterOS</a></li>
<li><a href="/posts/ansible-for-networking-part-7-vyos">Part 7 - VyOS</a></li>
</ul>
<p>All the playbooks, roles and variables used in this article are available in my <a href="https://gitlab.com/stuh84/network-automation-ansible">Network Automation with Ansible repository</a></p>
<h2 id="why-eos">Why EOS?</h2>
<p>Arista was formed by former Cisco employees (more information <a href="https://en.wikipedia.org/wiki/Arista_Networks">here</a>), all of whom had major involvement in Cisco&rsquo;s switching portfolio at different points in time. Since their founding in 2004, they have become a very popular option in the data centre switching market, and elsewhere.</p>
<p>Rather than building their own operating system, Arista is based upon Linux (similar to how JunOS is based upon FreeBSD). You can even access the Linux operating system, and run everything from standard Linux applications to Docker containers.</p>
<p>Arista&rsquo;s CLI is very similar to Cisco. Many Cisco commands are valid on an Arista switch. For this reason, migrating from a Cisco-centric network platform (and associated Ansible playbooks!) to an Arista-centric platform is quite a smooth transition.</p>
<p>Arista offer top-of-rack switching, as well as BGP-speaking edge/transit routers. They often come with high capacity ports (i.e. multiple 10/40/100G ports), as well as a high density of ports.</p>
<p>In my career I have managed a few Arista switches. Other than making sure I was familiar with the Multi-Chassis Link Aggregation syntax (i.e. allowing LACP across multiple switches, rather than one), most of my Cisco knowledge translated instantly.</p>
<p>To make transitioning from another vendor even easier Arista make a virtualised Arista EOS switch available, known as vEOS. It does have come with a few limitations, but nothing which makes it unsuitable for testing or training.</p>
<h2 id="ios-and-eos-similarities">IOS and EOS similarities</h2>
<p>Due of their shared heritage, not only are the CLI commands almost identical between EOS and a Cisco IOS, but also the available Ansible modules. Rather than refactoring large sections of playbooks (like we did for JunOS), in many cases the only major differences is using the <code>eos</code> in place of <code>ios</code>.</p>
<p>Due to the similarities, instead of explaining each individual task and what it does, most of sections will detail how they differ from the IOS playbooks. If I went into detail explaining each section, this post would look remarkably similar to <a href="/posts/ansible-for-networking-part-3-cisco-ios">part 3</a>!</p>
<p>To see the rationale behind the configuration and decisions made, I would recommend reading the Cisco IOS post, and using this post to draw comparisons.</p>
<h2 id="objectives">Objectives</h2>
<p>For each vendor, I will be using Ansible to configure two routers/switches/firewalls/appliances.</p>
<p>One will serve as the <em>Edge</em> switch, connecting to the Internet and also via BGP to the <strong>Net Server</strong>. The <strong>Net Server</strong> is a CentOS 8 Virtual Machine acting as a route server, syslog collector and TACACS+ server (detailed in <a href="/posts/ansible-for-networking-part-2-lab">The Lab Environment</a>)</p>
<p>The other will be an <em>internal</em> switch, performing <strong>core</strong> functions (i.e. internal routing rather than external).</p>
<h3 id="edge-switch">Edge switch</h3>
<p>The edge switch will run the following: -</p>
<ul>
<li><strong>External BGP</strong> (eBGP) to the <strong>Net Server</strong>
<ul>
<li>Advertising internal networks</li>
</ul>
</li>
<li><strong>Internal BGP</strong> (iBGP) to the <strong>Internal</strong> switch
<ul>
<li>Advertising any routes received from the <strong>Net Server</strong></li>
<li>Advertising a default route (for internet access)</li>
</ul>
</li>
<li><strong>OSPF</strong>
<ul>
<li>Advertising loopbacks and internal networks between both switches</li>
</ul>
</li>
<li><strong>IPv4</strong> and <strong>IPv6</strong> routing
<ul>
<li>Using OSPFv3 (for <strong>IPv6</strong> support)</li>
<li>Using the IPv6 <em>Address Family</em> for BGP</li>
</ul>
</li>
<li><strong>SNMPv3</strong> for monitoring</li>
<li><strong>Logging</strong> via Syslog to the <strong>Net Server</strong></li>
<li><strong>Authentication, Authorization and Accounting</strong> (AAA) via TACACS+ to the <strong>Net Server</strong></li>
<li><strong>Access Lists</strong> on the port facing the <strong>Net Server</strong></li>
</ul>
<p>Usually you would also place some form of <code>access-list</code> or other filtering on your ports facing the internet, but this is in a lab environment and already behind a firewall.</p>
<h3 id="internal-switch">Internal switch</h3>
<p>The internal switch runs a subset of the functions that the edge switch does: -</p>
<ul>
<li><strong>Internal BGP</strong> (iBGP) to the <strong>Edge</strong> switch
<ul>
<li>Receiving any routes received from the <strong>Net Server</strong></li>
<li>Receiving a default route (for internet access)</li>
</ul>
</li>
<li><strong>OSPF</strong>
<ul>
<li>Advertising loopbacks and internal networks between both switches</li>
</ul>
</li>
<li><strong>IPv4</strong> and <strong>IPv6</strong> routing
<ul>
<li>Using OSPFv3 (for <strong>IPv6</strong> support)</li>
<li>Using the IPv6 <em>Address Family</em> for BGP</li>
</ul>
</li>
<li><strong>SNMPv3</strong> for monitoring</li>
<li><strong>Logging</strong> via Syslog to the <strong>Net Server</strong></li>
<li><strong>Authentication, Authorization and Accounting</strong> (AAA) via TACACS+ to the <strong>Net Server</strong></li>
</ul>
<h2 id="prerequisites">Prerequisites</h2>
<p>To manage an Arista EOS device with Ansible, the following steps  are required to allow access to the switches. We also make some changes to the default Ansible connection configuration.</p>
<h3 id="ansible-configuration">Ansible Configuration</h3>
<p>The following defaults are required to use Ansible with Arista EOS: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">ansible_user</span>: ansible
<span style="color:#66d9ef">ansible_connection</span>: network_cli
<span style="color:#66d9ef">ansible_network_os</span>: eos
<span style="color:#66d9ef">ansible_ssh_pass</span>: <span style="color:#75715e">###REDACTED###</span>
<span style="color:#66d9ef">ansible_become</span>: yes
<span style="color:#66d9ef">ansible_become_method</span>: enable
<span style="color:#66d9ef">ansible_become_password</span>: <span style="color:#75715e">###REDACTED###</span>
</code></pre></div><p>This is exactly the same as the Cisco IOS configuration, except the Network OS has changed to <code>eos</code>.</p>
<h3 id="arista-eos-configuration">Arista EOS Configuration</h3>
<p>The prerequisites for allowing Ansible access to manage an EOS device are: -</p>
<ul>
<li>Create a user with the correct privilege level</li>
<li>Create an <code>enable</code> password</li>
<li>Give the device a <code>hostname</code></li>
</ul>
<p>Password encryption is enabled by default (using SHA512 rather than a proprietary method like Cisco), and SSH is enabled by default too.</p>
<p>Below shows how to do all of the above: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">arista&gt; enable
arista# conf t
Enter configuration commands, one per line.  End with CNTL/Z.

! Add the ansible user - Privilege level <span style="color:#ae81ff">15</span> is equivalent to full admin rights
arista<span style="color:#f92672">(</span>config<span style="color:#f92672">)</span><span style="color:#75715e"># username ansible privilege 15 secret 0 $PASSWORD-HERE$</span>

! Create the enable password
arista<span style="color:#f92672">(</span>config<span style="color:#f92672">)</span><span style="color:#75715e"># enable password 0 $ENABLE-PASSWORD$</span>

! Give the device a hostname
arista<span style="color:#f92672">(</span>config<span style="color:#f92672">)</span><span style="color:#75715e"># hostname arista-01</span>
arista-01<span style="color:#f92672">(</span>config<span style="color:#f92672">)</span><span style="color:#75715e"># end</span>

! Write your configuration - Important, otherwise you will lose it <span style="color:#66d9ef">if</span> the devices powers off or reboots
arista-01# copy running-config startup-config
</code></pre></div><p>Once the above is done, add the device into your Ansible inventory. My inventory file looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#66d9ef">[arista]</span>
<span style="color:#a6e22e">arista-01 ansible_host</span><span style="color:#f92672">=</span><span style="color:#e6db74">10.15.30.43</span>
<span style="color:#a6e22e">arista-02 ansible_host</span><span style="color:#f92672">=</span><span style="color:#e6db74">10.15.30.44</span>
</code></pre></div><h4 id="verification">Verification</h4>
<p>Can we contact both devices?</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ansible arista -m eos_facts | grep -i hostname
        <span style="color:#e6db74">&#34;ansible_net_hostname&#34;</span>: <span style="color:#e6db74">&#34;arista-01&#34;</span>,
        <span style="color:#e6db74">&#34;ansible_net_hostname&#34;</span>: <span style="color:#e6db74">&#34;arista-02&#34;</span>,
</code></pre></div><p>That will do it!</p>
<h2 id="setup">Setup</h2>
<p>The setup is similar to the IOS lab. However, I couldn&rsquo;t use the VLAN Bridge. The Arista vEOS images in my lab were not able to receive frames with tagged VLANs.</p>
<p>I verified (using packet captures) that each vEOS virtual machine sent frames with tagged VLANs, but they would not receive them (presumably being dropped somewhere at the kernel level).</p>
<p>I tried this lab with two different KVM machines, and also attempted it with a Hyper-V hypervisor instance too, with no success. The same setup has worked with every other vendor I have tried so far (including some I haven&rsquo;t written blogs about yet), so it does appear to be an issue with the vEOS image itself.</p>
<p>Instead of using VLANs, we are using dedicated interfaces (and bridges) between <em>netsvr-01</em> and <em>arista-01</em>, and between the two <em>arista</em> instances.</p>
<h3 id="vlans-ip-addressing-and-autonomous-system-numbers">VLANs, IP addressing and Autonomous System numbers</h3>
<p>The ID chosen for Arista EOS is <code>03</code>.</p>
<h4 id="interfaces">Interfaces</h4>
<p>VLANs are not being used, due to limitations with the vEOS image. Instead, physical interfaces are used instead</p>
<ul>
<li><code>enp10s0</code> on <strong>netsvr-01</strong> is connected to <code>ether1</code> on the <strong>edge</strong> switch - <strong>netsvr bridge</strong></li>
<li><code>ether2</code> on <strong>arista-01</strong> is connected to <code>ether1</code> on the <strong>internal</strong> switch - <strong>arista bridge</strong></li>
</ul>
<h4 id="ip-addressing">IP Addressing</h4>
<ul>
<li>IPv4 Subnet on the <strong>netsvr bridge</strong>: <code>10.100.103.0/24</code>
<ul>
<li><em>edge</em> switch - <code>10.100.103.253/24</code></li>
<li><em>netsvr-01</em> - <code>10.100.103.254/24</code></li>
</ul>
</li>
<li>IPv4 Subnet on the <strong>arista bridge</strong>: 10.100.203.0/24
<ul>
<li><em>edge</em> switch - <code>10.100.203.254/24</code></li>
<li><em>internal</em> switch - <code>10.100.203.253/24</code></li>
</ul>
</li>
<li>IPv6 Subnet on the <strong>netsvr bridge</strong>: <code>2001:db8:103::/64</code>
<ul>
<li><em>edge</em> switch - <code>2001:db8:103::f/64</code></li>
<li><em>netsvr-01</em> - <code>2001:db8:103:ffff/64</code></li>
</ul>
</li>
<li>IPv6 Subnet on the <strong>arista bridge</strong>: 2001:db8:203::/64
<ul>
<li><em>edge</em> switch - <code>2001:db8:203::a/64</code></li>
<li><em>internal</em> switch - <code>2001:db8:203:f/64</code></li>
</ul>
</li>
<li>IPv4 Loopback Addressing
<ul>
<li><em>edge</em> switch - <code>192.0.2.103/32</code></li>
<li><em>internal</em> switch - <code>192.0.2.203/32</code></li>
</ul>
</li>
<li>IPv6 Loopback Address
<ul>
<li><em>edge</em> switch - <code>2001:db8:903:beef::1/128</code></li>
<li><em>internal</em> switch - <code>2001:db8:903:beef::2/128</code></li>
</ul>
</li>
</ul>
<h4 id="bgp-autonomous-system">BGP Autonomous System</h4>
<p>The BGP <a href="https://en.wikipedia.org/wiki/Autonomous_system_(Internet)">Autonomous System</a> number will be <code>AS65103</code>.</p>
<h2 id="configuration">Configuration</h2>
<p>In this section we will go through the configuration of the switches. Due to the similarities between Arista and Cisco, much of the configuration is close to identical. Rather than explaining each task individually, I shall go through the major differences.</p>
<p>Also, we have remove the NAT role. This is because the Arista switches (like most switches) do not support <strong>N</strong>etwork <strong>A</strong>ddress <strong>T</strong>ranslation. While I have never managed an Arista router, looking at Arista <a href="https://www.arista.com/en/um-eos/eos-section-28-8-ip-nat">documentation</a> shows that the configuration is similar to IOS. The only major difference is that NAT is tied to an interface directly, rather than being configured at the <strong>global</strong> level and then added to an interface with <code>ip nat inside</code> or <code>ip nat outside</code> at the <strong>interface</strong> hierarchical level.</p>
<p>Additionally, while we will configure access-lists, they do not take effect on Arista vEOS. This is because it has no real switch hardware to apply the access-lists to. The configuration would work on a real Arista switch, but in our virtual lab, while we can configure it but it will not actually block/allow traffic.</p>
<h3 id="system-tasks">System Tasks</h3>
<p>As noted in previous parts, the system tasks setup basic logging, banners and the hostname.</p>
<h4 id="playbook">Playbook</h4>
<p>The contents of the playbook are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for system</span>
- <span style="color:#66d9ef">name</span>: Set system hostname
  <span style="color:#66d9ef">eos_system</span>:
    <span style="color:#66d9ef">hostname</span>: <span style="color:#e6db74">&#34;{{ inventory_hostname }}&#34;</span>

- <span style="color:#66d9ef">name</span>: Remove unneeded banners
  <span style="color:#66d9ef">eos_banner</span>:
    <span style="color:#66d9ef">banner</span>: <span style="color:#e6db74">&#34;{{ item }}&#34;</span>
    <span style="color:#66d9ef">state</span>: absent
  <span style="color:#66d9ef">loop</span>:
  - motd

- <span style="color:#66d9ef">name</span>: Update login banner
  <span style="color:#66d9ef">eos_banner</span>:
    <span style="color:#66d9ef">banner</span>: login
    <span style="color:#66d9ef">text</span>: <span style="color:#e6db74">|
</span><span style="color:#e6db74">      ----------------------------------------</span>
      <span style="color:#e6db74">|
</span><span style="color:#e6db74">      | This banner was generated by Ansible</span>
      <span style="color:#e6db74">|
</span><span style="color:#e6db74">      ----------------------------------------</span>
      <span style="color:#e6db74">|
</span><span style="color:#e6db74">      | You are logged into {{ inventory_hostname }}</span>
      <span style="color:#e6db74">|
</span><span style="color:#e6db74">      ----------------------------------------</span>
    <span style="color:#66d9ef">state</span>: present

- <span style="color:#66d9ef">name</span>: Configure syslog
  <span style="color:#66d9ef">eos_logging</span>:
    <span style="color:#66d9ef">dest</span>: host
    <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;{{ log_host }}&#34;</span>
    <span style="color:#66d9ef">state</span>: present

- <span style="color:#66d9ef">name</span>: Set log source
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - logging source-interface Loopback0

- <span style="color:#66d9ef">name</span>: Configure log buffer
  <span style="color:#66d9ef">eos_logging</span>:
    <span style="color:#66d9ef">dest</span>: buffered
    <span style="color:#66d9ef">level</span>: informational
    <span style="color:#66d9ef">size</span>: <span style="color:#ae81ff">5000</span>
    <span style="color:#66d9ef">state</span>: present
</code></pre></div><p>The main differences between this playbook and the Cisco IOS playbook are: -</p>
<ul>
<li>The word <code>ios</code> is replaced with <code>eos</code> (e.g. <code>eos_banner</code> instead of <code>ios_banner</code>)</li>
<li>We only remove the <code>motd</code> banner, as the <code>exec</code> and <code>incoming</code> banners are not available on EOS</li>
<li>We set the hostname
<ul>
<li>We could have done this in the Cisco playbook as well, using <code>ios_system</code></li>
</ul>
</li>
<li>We set the source of logs to be the <code>Loopback0</code> interface
<ul>
<li>This allows logs to come from the same interface, no matter the egress point of traffic</li>
</ul>
</li>
<li>Setting the log size in <code>eos_logging</code>
<ul>
<li>This is a mandatory setting in the <code>eos_logging</code> module</li>
</ul>
</li>
<li>As noted previously, password encryption is enabled by default in Arista</li>
</ul>
<p>While this might seem like a few changes, in practice the differences in the playbooks are quite small: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ diff arista/roles/system/tasks/main.yml cisco/roles/system/tasks/main.yml
3,6d2
&lt; - name: Set system hostname
&lt;   eos_system:
&lt;     hostname: <span style="color:#e6db74">&#34;{{ inventory_hostname }}&#34;</span>
&lt;
8c4
&lt;   eos_banner:
---
&gt;   ios_banner:
12a9,10
&gt;   - exec
&gt;   - incoming
15c13
&lt;   eos_banner:
---
&gt;   ios_banner:
30c28
&lt;   eos_logging:
---
&gt;   ios_logging:
35,39d32
&lt; - name: Set log source
&lt;   eos_config:
&lt;     lines:
&lt;       - logging source-interface Loopback0
&lt;
41c34
&lt;   eos_logging:
---
&gt;   ios_logging:
44d36
&lt;     size: <span style="color:#ae81ff">5000</span>
45a38,42
&gt;
&gt; - name: Enable service password-encryption
&gt;   ios_config:
&gt;     lines:
&gt;       - service password-encryption
</code></pre></div><p>Compared this to the differences you&rsquo;ll find in the JunOS playbooks, or the upcoming playbooks for MikroTik or Cumulus, this is minimal.</p>
<p>All Ansible modules are referred to and used almost identically to the IOS modules, including the use of <code>parents</code> for <code>eos_config</code> (for commands only available in certain hierarchies).</p>
<h4 id="output">Output</h4>
<p>The generated configuration from this playbook is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">logging buffered 5000 informational
logging host 10.100.103.254 514
logging source-interface Loopback0

hostname arista-01

banner login
----------------------------------------
|
| This banner was generated by Ansible
|
----------------------------------------
|
| You are logged into arista-01
|
----------------------------------------
EOF
</code></pre></div><h3 id="interfaces-1">Interfaces</h3>
<p>This role configures the interfaces, descriptions and IP addressing (IPv4 and IPv6). We are not able to make use of VLANs due to the lack of VLAN tagging support on the vEOS images (or at least in the setups I have tried), so all references to subinterfaces in our <code>host_vars</code> have been removed.</p>
<h4 id="playbook-1">Playbook</h4>
<p>The contents of the playbook are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">name</span>: Configure subinterfaces first
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">src</span>: subints.j2

- <span style="color:#66d9ef">name</span>: Configure routed ports
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#e6db74">&#34;no switchport&#34;</span>
    <span style="color:#66d9ef">parents</span>:
      - interface {{ item.eos_if }}
  <span style="color:#66d9ef">when</span>:
    - item.routed is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>

- <span style="color:#66d9ef">name</span>: Configure interfaces - Status and Descriptions
  <span style="color:#66d9ef">eos_interfaces</span>:
    <span style="color:#66d9ef">config</span>:
      - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;{{ item.eos_if }}&#34;</span>
        <span style="color:#66d9ef">description</span>: <span style="color:#e6db74">&#34;{{ item.desc }}&#34;</span>
        <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;{{ item.enabled }}&#34;</span>
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>

- <span style="color:#66d9ef">name</span>: Configure interfaces - L3 IPv4
  <span style="color:#66d9ef">eos_l3_interfaces</span>:
    <span style="color:#66d9ef">config</span>:
      - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;{{ item.eos_if }}&#34;</span>
        <span style="color:#66d9ef">ipv4</span>:
        - <span style="color:#66d9ef">address</span>: <span style="color:#e6db74">&#34;{{ item.ipv4 }}&#34;</span>
  <span style="color:#66d9ef">when</span>: item.ipv4 is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>

- <span style="color:#66d9ef">name</span>: Configure interfaces - L3 IPv6
  <span style="color:#66d9ef">eos_l3_interfaces</span>:
    <span style="color:#66d9ef">config</span>:
      - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;{{ item.eos_if }}&#34;</span>
        <span style="color:#66d9ef">ipv6</span>:
        - <span style="color:#66d9ef">address</span>: <span style="color:#e6db74">&#34;{{ item.ipv6 }}&#34;</span>
  <span style="color:#66d9ef">when</span>: item.ipv6 is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
</code></pre></div><p>The differences between this and the IOS interfaces playbook are: -</p>
<ul>
<li>Setting ports to routed mode, rather than switched mode
<ul>
<li>Switched ports cannot have IPs assigned to them</li>
<li>The same configuration would be required on a Cisco IOS switch</li>
</ul>
</li>
<li>The word <code>ios</code> is replaced with <code>eos</code> (e.g. <code>eos_interfaces</code> instead of <code>ios_interfaces</code></li>
</ul>
<p>The actual differences are minimal: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ diff arista/roles/system/main.yml cisco/roles/interfaces/tasks/main.yml
1a2
&gt; <span style="color:#75715e"># tasks file for interfaces</span>
3c4
&lt;   eos_config:
---
&gt;   ios_config:
6,15d6
&lt; - name: Configure routed ports
&lt;   eos_config:
&lt;     lines:
&lt;       - <span style="color:#e6db74">&#34;no switchport&#34;</span>
&lt;     parents:
&lt;       - interface <span style="color:#f92672">{{</span> item.eos_if <span style="color:#f92672">}}</span>
&lt;   when:
&lt;     - item.routed is defined
&lt;   loop: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
&lt;
17c8
&lt;   eos_interfaces:
---
&gt;   ios_interfaces:
19c10
&lt;       - name: <span style="color:#e6db74">&#34;{{ item.eos_if }}&#34;</span>
---
&gt;       - name: <span style="color:#e6db74">&#34;{{ item.ios_if }}&#34;</span>
25c16
&lt;   eos_l3_interfaces:
---
&gt;   ios_l3_interfaces:
27c18
&lt;       - name: <span style="color:#e6db74">&#34;{{ item.eos_if }}&#34;</span>
---
&gt;       - name: <span style="color:#e6db74">&#34;{{ item.ios_if }}&#34;</span>
34c25
&lt;   eos_l3_interfaces:
---
&gt;   ios_l3_interfaces:
36c27
&lt;       - name: <span style="color:#e6db74">&#34;{{ item.eos_if }}&#34;</span>
---
&gt;       - name: <span style="color:#e6db74">&#34;{{ item.ios_if }}&#34;</span>
</code></pre></div><p>The <code>routed</code> variable comes from our <code>host_vars</code>, which can be seen below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">eos_if</span>: <span style="color:#e6db74">&#34;Management1&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Management&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.15.30.43/24&#34;</span>
  - <span style="color:#66d9ef">eos_if</span>: <span style="color:#e6db74">&#34;Ethernet1&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To netsvr&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">routed</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.100.103.253/24&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:103::f/64&#34;</span>
  - <span style="color:#66d9ef">eos_if</span>: <span style="color:#e6db74">&#34;Ethernet2&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To arista-02&#34;</span>
    <span style="color:#66d9ef">routed</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.100.203.254/24&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:203::a/64&#34;</span>
  - <span style="color:#66d9ef">eos_if</span>: <span style="color:#e6db74">&#34;Ethernet3&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To the Internet&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">routed</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;dhcp&#34;</span>
  - <span style="color:#66d9ef">eos_if</span>: <span style="color:#e6db74">&#34;Loopback0&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;192.0.2.103/32&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:903:beef::1/128&#34;</span>
</code></pre></div><p>The Management port and Loopback interface do not require the <code>routed</code> option, as they are routed by default.</p>
<p>If you run this playbook on real hardware, and have access to sub-interfaces, then the below will be relevant: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ diff arista/roles/interfaces/templates/subints.j2 cisco/roles/interfaces/templates/subints.j2
4,5c4,5
&lt; interface <span style="color:#f92672">{{</span> interface<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;eos_if&#39;</span><span style="color:#f92672">]</span> <span style="color:#f92672">}}</span>.<span style="color:#f92672">{{</span> vlan <span style="color:#f92672">}}</span>
&lt;  encapsulation dot1q vlan <span style="color:#f92672">{{</span> vlan <span style="color:#f92672">}}</span>
---
&gt; interface <span style="color:#f92672">{{</span> interface<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;ios_if&#39;</span><span style="color:#f92672">]</span> <span style="color:#f92672">}}</span>.<span style="color:#f92672">{{</span> vlan <span style="color:#f92672">}}</span>
&gt;  encapsulation dot1q <span style="color:#f92672">{{</span> vlan <span style="color:#f92672">}}</span>
</code></pre></div><h4 id="output-1">Output</h4>
<p>The generated configuration looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">interface Ethernet1
   description To netsvr
   no switchport
   ip address 10.100.103.253/24
   ipv6 address 2001:db8:103::f/64
!
interface Ethernet2
   description To arista-02
   no switchport
   ip address 10.100.203.254/24
   ipv6 address 2001:db8:203::a/64
!
interface Ethernet3
   description To the Internet
   no switchport
   ip address dhcp
!
interface Loopback0
   description Loopback
   ip address 192.0.2.103/32
   ipv6 address 2001:db8:903:beef::1/128
!
interface Management1
   description Management
   ip address 10.15.30.43/24
</code></pre></div><p>Again, if you are used to Cisco IOS, this should look very familiar. The only major difference is the use of CIDR notation (i.e. <code>10.100.103.253/24</code>) rather than network masks (i.e. <code>10.100.103.253 255.255.255.0</code>) for IPv4 addresses.</p>
<h4 id="verification-1">Verification</h4>
<p><strong>arista-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show IPs
rista-01#show ip int brief
                                                                                  Address
Interface         IP Address              Status       Protocol            MTU    Owner
----------------- ----------------------- ------------ -------------- ----------- -------
Ethernet1         10.100.103.253/24       up           up                 1500
Ethernet2         10.100.203.254/24       up           up                 1500
Ethernet3         192.168.122.72/24       up           up                 1500
Loopback0         192.0.2.103/32          up           up                65535
Management1       10.15.30.43/24          up           up                 1500

arista-01#show ipv6 interface brief
   Interface       Status         MTU       IPv6 Address                     Addr State    Addr Source
--------------- ------------ ----------- -------------------------------- ---------------- -----------
   Et1             up            1500       fe80::5054:ff:fec5:df3c/64       up            link local
                                            2001:db8:103::f/64               up            config
   Et2             up            1500       fe80::5054:ff:fec5:df3c/64       up            link local
                                            2001:db8:203::a/64               up            config
   Lo0             up           65535       fe80::ff:fe00:0/64               up            link local
                                            2001:db8:903:beef::1/128         up            config


! Show interface statuses and descriptions
arista-01#show interfaces description
Interface                      Status         Protocol           Description
Et1                            up             up                 To netsvr
Et2                            up             up                 To arista-02
Et3                            up             up                 To the Internet
Lo0                            up             up                 Loopback
Ma1                            up             up                 Management

! Ping to netsvr-01 on IPv4 and IPv6
arista-01#ping 10.100.103.254
PING 10.100.103.254 (10.100.103.254) 72(100) bytes of data.
80 bytes from 10.100.103.254: icmp_seq=1 ttl=64 time=1.62 ms
80 bytes from 10.100.103.254: icmp_seq=2 ttl=64 time=1.08 ms
80 bytes from 10.100.103.254: icmp_seq=3 ttl=64 time=1.19 ms
80 bytes from 10.100.103.254: icmp_seq=4 ttl=64 time=1.01 ms
80 bytes from 10.100.103.254: icmp_seq=5 ttl=64 time=1.53 ms

--- 10.100.103.254 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 8ms
rtt min/avg/max/mdev = 1.010/1.289/1.622/0.244 ms, ipg/ewma 2.017/1.458 ms

arista-01#ping 2001:db8:103::ffff
PING 2001:db8:103::ffff(2001:db8:103::ffff) 72 data bytes
80 bytes from 2001:db8:103::ffff: icmp_seq=1 ttl=64 time=1.53 ms
80 bytes from 2001:db8:103::ffff: icmp_seq=2 ttl=64 time=1.02 ms
80 bytes from 2001:db8:103::ffff: icmp_seq=3 ttl=64 time=1.04 ms
80 bytes from 2001:db8:103::ffff: icmp_seq=4 ttl=64 time=1.06 ms
80 bytes from 2001:db8:103::ffff: icmp_seq=5 ttl=64 time=1.37 ms

--- 2001:db8:103::ffff ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 7ms
rtt min/avg/max/mdev = 1.021/1.209/1.539/0.210 ms, ipg/ewma 1.879/1.376 ms

! Ping to cisco-02 on IPv4 and IPv6
arista-01#ping 10.100.203.253
PING 10.100.203.253 (10.100.203.253) 72(100) bytes of data.
80 bytes from 10.100.203.253: icmp_seq=1 ttl=64 time=3.43 ms
80 bytes from 10.100.203.253: icmp_seq=2 ttl=64 time=2.52 ms
80 bytes from 10.100.203.253: icmp_seq=3 ttl=64 time=2.66 ms
80 bytes from 10.100.203.253: icmp_seq=4 ttl=64 time=3.39 ms
80 bytes from 10.100.203.253: icmp_seq=5 ttl=64 time=3.63 ms

--- 10.100.203.253 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 14ms
rtt min/avg/max/mdev = 2.523/3.128/3.630/0.451 ms, ipg/ewma 3.622/3.306 ms

arista-01#ping 2001:db8:203::f
PING 2001:db8:203::f(2001:db8:203::f) 72 data bytes
80 bytes from 2001:db8:203::f: icmp_seq=1 ttl=64 time=8.17 ms
80 bytes from 2001:db8:203::f: icmp_seq=2 ttl=64 time=3.34 ms
80 bytes from 2001:db8:203::f: icmp_seq=3 ttl=64 time=2.88 ms
80 bytes from 2001:db8:203::f: icmp_seq=4 ttl=64 time=2.80 ms
80 bytes from 2001:db8:203::f: icmp_seq=5 ttl=64 time=2.67 ms

--- 2001:db8:203::f ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 30ms
rtt min/avg/max/mdev = 2.676/3.977/8.172/2.110 ms, ipg/ewma 7.608/5.988 ms
</code></pre></div><p>For those who use Linux, you&rsquo;ll probably recognise the layout of the <code>ping</code> output, as it is exactly the same as on a Linux machine. The Linux underpinnings of Arista do creep through every now and then!</p>
<p><strong>arista-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show IPs
arista-02#show ip interface brief
                                                                                  Address
Interface         IP Address              Status       Protocol            MTU    Owner
----------------- ----------------------- ------------ -------------- ----------- -------
Ethernet1         10.100.203.253/24       up           up                 1500
Loopback0         192.0.2.203/32          up           up                65535
Management1       10.15.30.44/24          up           up                 1500

arista-02#show ipv6 interface brief
   Interface       Status         MTU       IPv6 Address                     Addr State    Addr Source
--------------- ------------ ----------- -------------------------------- ---------------- -----------
   Et1             up            1500       fe80::5054:ff:fed8:9755/64       up            link local
                                            2001:db8:203::f/64               up            config
   Lo0             up           65535       fe80::ff:fe00:0/64               up            link local
                                            2001:db8:903:beef::2/128         up            config

! Show interface statuses and descriptions
arista-02#show interfaces description
Interface                      Status         Protocol           Description
Et1                            up             up                 To arista-01
Lo0                            up             up                 Loopback
Ma1                            up             up                 Management

! Ping to cisco-01 on IPv4 and IPv6
arista-02#ping 10.100.203.254
PING 10.100.203.254 (10.100.203.254) 72(100) bytes of data.
80 bytes from 10.100.203.254: icmp_seq=1 ttl=64 time=3.13 ms
80 bytes from 10.100.203.254: icmp_seq=2 ttl=64 time=2.57 ms
80 bytes from 10.100.203.254: icmp_seq=3 ttl=64 time=2.51 ms
80 bytes from 10.100.203.254: icmp_seq=4 ttl=64 time=2.47 ms
80 bytes from 10.100.203.254: icmp_seq=5 ttl=64 time=3.26 ms

--- 10.100.203.254 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 13ms
rtt min/avg/max/mdev = 2.477/2.793/3.262/0.337 ms, ipg/ewma 3.272/2.972 ms

arista-02#ping 2001:db8:203::a
PING 2001:db8:203::a(2001:db8:203::a) 72 data bytes
80 bytes from 2001:db8:203::a: icmp_seq=1 ttl=64 time=3.18 ms
80 bytes from 2001:db8:203::a: icmp_seq=2 ttl=64 time=2.66 ms
80 bytes from 2001:db8:203::a: icmp_seq=3 ttl=64 time=2.53 ms
80 bytes from 2001:db8:203::a: icmp_seq=4 ttl=64 time=2.67 ms
80 bytes from 2001:db8:203::a: icmp_seq=5 ttl=64 time=3.19 ms

--- 2001:db8:203::a ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 13ms
rtt min/avg/max/mdev = 2.534/2.849/3.195/0.284 ms, ipg/ewma 3.388/3.024 ms
</code></pre></div><p>All looking good so far!</p>
<h3 id="access-lists">Access Lists</h3>
<p>As noted earlier, the access-lists applied in this task will be part of the configuration, but will not take effect and therefore will not be able to verify they are correct. If you run these tasks on real Arista hardware though, they should function correctly.</p>
<h4 id="playbook-2">Playbook</h4>
<p>The contents of the playbook are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for acl</span>
- <span style="color:#66d9ef">name</span>: Edge Access List - Outbound IPv4
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#ae81ff">10</span> permit icmp any any log
      - <span style="color:#ae81ff">20</span> permit udp any host {{ log_host }}  eq syslog log
      - <span style="color:#ae81ff">30</span> permit tcp any host {{ tacacs[<span style="color:#e6db74">&#39;ipv4&#39;</span>] }}  eq tacacs log
      - <span style="color:#ae81ff">1000</span> deny ip any any log
    <span style="color:#66d9ef">parents</span>:
      - ip access-list extended EDGE-OUT
  <span style="color:#66d9ef">when</span>:
    - rtr_role is search(<span style="color:#e6db74">&#34;edge&#34;</span>)
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv4

- <span style="color:#66d9ef">name</span>: Edge Access List - Inbound IPv4
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#ae81ff">10</span> permit icmp any any log
      - <span style="color:#ae81ff">20</span> permit tcp host {{ tacacs[<span style="color:#e6db74">&#39;ipv4&#39;</span>] }} eq tacacs any log
      - <span style="color:#ae81ff">1000</span> deny ip any any log
    <span style="color:#66d9ef">parents</span>:
      - ip access-list extended EDGE-IN
  <span style="color:#66d9ef">when</span>:
    - rtr_role is search(<span style="color:#e6db74">&#34;edge&#34;</span>)
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv4

- <span style="color:#66d9ef">name</span>: Edge Access List - BGP Outbound IPv4
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#e6db74">&#34;no {{ item.acl.acl_index }}&#34;</span>
      - <span style="color:#e6db74">&#34;{{ item.acl.acl_index }} permit tcp any host {{ item.peer }} eq bgp log&#34;</span>
    <span style="color:#66d9ef">parents</span>:
      - ip access-list extended EDGE-OUT
  <span style="color:#66d9ef">when</span>:
    - rtr_role is search(<span style="color:#e6db74">&#34;edge&#34;</span>)
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv4 is defined
    - item.acl is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv4 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv4

- <span style="color:#66d9ef">name</span>: Edge Access List - BGP Inbound IPv4
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#e6db74">&#34;no {{ item.acl.acl_index }}&#34;</span>
      - <span style="color:#e6db74">&#34;{{ item.acl.acl_index }} permit tcp host {{ item.peer }} eq bgp any log&#34;</span>
    <span style="color:#66d9ef">parents</span>:
      - ip access-list extended EDGE-IN
  <span style="color:#66d9ef">when</span>:
    - rtr_role is search(<span style="color:#e6db74">&#34;edge&#34;</span>)
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv4 is defined
    - item.acl is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv4 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv4

- <span style="color:#66d9ef">name</span>: Apply Inbound IPv4 ACL
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - ip access-group {{ item.acl.ipv4.in }} in
    <span style="color:#66d9ef">parents</span>:
      - interface {{ item.eos_if }}
  <span style="color:#66d9ef">when</span>:
    - item.acl is defined
    - item.acl.ipv4 is defined
    - item.acl.ipv4.in is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv4

- <span style="color:#66d9ef">name</span>: Apply Outbound IPv4 ACL
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - ip access-group {{ item.acl.ipv4.out }} out
    <span style="color:#66d9ef">parents</span>:
      - interface {{ item.eos_if }}
  <span style="color:#66d9ef">when</span>:
    - item.acl is defined
    - item.acl.ipv4 is defined
    - item.acl.ipv4.out is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv4

- <span style="color:#66d9ef">name</span>: Edge Access List - Outbound IPv6
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#ae81ff">10</span> permit icmp any any log
      - <span style="color:#ae81ff">1000</span> deny ipv6 any any log
    <span style="color:#66d9ef">parents</span>:
      - ipv6 access-list EDGEv6-OUT
  <span style="color:#66d9ef">when</span>:
    - rtr_role is search(<span style="color:#e6db74">&#34;edge&#34;</span>)
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv6

- <span style="color:#66d9ef">name</span>: Edge Access List - Inbound IPv6
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#ae81ff">10</span> permit icmp any any log
      - <span style="color:#ae81ff">1000</span> deny ipv6 any any log
    <span style="color:#66d9ef">parents</span>:
      - ipv6 access-list EDGEv6-IN
  <span style="color:#66d9ef">when</span>:
    - rtr_role is search(<span style="color:#e6db74">&#34;edge&#34;</span>)
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv6

- <span style="color:#66d9ef">name</span>: Edge Access List - BGP Outbound IPv6
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#e6db74">&#34;no {{ item.acl.acl_index }}&#34;</span>
      - <span style="color:#e6db74">&#34;{{ item.acl.acl_index }} permit tcp any host {{ item.peer }} eq bgp log&#34;</span>
    <span style="color:#66d9ef">parents</span>:
      - ipv6 access-list EDGEv6-OUT
  <span style="color:#66d9ef">when</span>:
    - rtr_role is search(<span style="color:#e6db74">&#34;edge&#34;</span>)
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv6 is defined
    - item.acl is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv6 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv6

- <span style="color:#66d9ef">name</span>: Edge Access List - BGP Inbound IPv6
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#e6db74">&#34;no {{ item.acl.acl_index }}&#34;</span>
      - <span style="color:#e6db74">&#34;{{ item.acl.acl_index }} permit tcp host {{ item.peer }} eq bgp any log&#34;</span>
    <span style="color:#66d9ef">parents</span>:
      - ipv6 access-list EDGEv6-IN
  <span style="color:#66d9ef">when</span>:
    - rtr_role is search(<span style="color:#e6db74">&#34;edge&#34;</span>)
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv6 is defined
    - item.acl is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv6 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv6

- <span style="color:#66d9ef">name</span>: Apply Inbound IPv6 ACL
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - ipv6 access-group {{ item.acl.ipv6.in }} in
    <span style="color:#66d9ef">parents</span>:
      - interface {{ item.eos_if }}
  <span style="color:#66d9ef">when</span>:
    - item.acl is defined
    - item.acl.ipv6 is defined
    - item.acl.ipv6.in is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv6

- <span style="color:#66d9ef">name</span>: Apply Outbound IPv6 ACL
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - ipv6 access-group {{ item.acl.ipv6.out }} out
    <span style="color:#66d9ef">parents</span>:
      - interface {{ item.eos_if }}
  <span style="color:#66d9ef">when</span>:
    - item.acl is defined
    - item.acl.ipv6 is defined
    - item.acl.ipv6.out is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv6
</code></pre></div><p>The differences between this and the IOS playbook are: -</p>
<ul>
<li>We are remove existing entries in an access-list, so that we do not have out-of-date configuration</li>
<li>We do not need to prefix numbered rules in an IPv6 access-list with <code>sequence</code></li>
<li>We use <code>ipv6 access-group</code> to apply IPv6 access-lists to an interface, rather than <code>ipv6 traffic-filter</code>
<ul>
<li>This is consistent with IPv4 (applied using <code>ip access-group</code>)</li>
</ul>
</li>
<li>The word <code>ios</code> is replaced with <code>eos</code> (e.g. <code>eos_config</code> instead of <code>ios_config</code>)</li>
</ul>
<p>The full differences are shown below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ diff arista/roles/acl/tasks/main.yml cisco/roles/acl/tasks/main.yml
&lt;   eos_config:
---
&gt;   ios_config:
19c19
&lt;   eos_config:
---
&gt;   ios_config:
33c33
&lt;   eos_config:
---
&gt;   ios_config:
35d34
&lt;       - <span style="color:#e6db74">&#34;no {{ item.acl.acl_index }}&#34;</span>
51c50
&lt;   eos_config:
---
&gt;   ios_config:
53d51
&lt;       - <span style="color:#e6db74">&#34;no {{ item.acl.acl_index }}&#34;</span>
69c67
&lt;   eos_config:
---
&gt;   ios_config:
73c71
&lt;       - interface <span style="color:#f92672">{{</span> item.eos_if <span style="color:#f92672">}}</span>
---
&gt;       - interface <span style="color:#f92672">{{</span> item.ios_if <span style="color:#f92672">}}</span>
84c82
&lt;   eos_config:
---
&gt;   ios_config:
88c86
&lt;       - interface <span style="color:#f92672">{{</span> item.eos_if <span style="color:#f92672">}}</span>
---
&gt;       - interface <span style="color:#f92672">{{</span> item.ios_if <span style="color:#f92672">}}</span>
99c97
&lt;   eos_config:
---
&gt;   ios_config:
101,102c99,100
&lt;       - <span style="color:#ae81ff">10</span> permit icmp any any log
&lt;       - <span style="color:#ae81ff">1000</span> deny ipv6 any any log
---
&gt;       - sequence <span style="color:#ae81ff">10</span> permit icmp any any log
&gt;       - sequence <span style="color:#ae81ff">1000</span> deny ipv6 any any log
112c110
&lt;   eos_config:
---
&gt;   ios_config:
114,115c112,113
&lt;       - <span style="color:#ae81ff">10</span> permit icmp any any log
&lt;       - <span style="color:#ae81ff">1000</span> deny ipv6 any any log
---
&gt;       - sequence <span style="color:#ae81ff">10</span> permit icmp any any log
&gt;       - sequence <span style="color:#ae81ff">1000</span> deny ipv6 any any log
125c123
&lt;   eos_config:
---
&gt;   ios_config:
127,128c125
&lt;       - <span style="color:#e6db74">&#34;no {{ item.acl.acl_index }}&#34;</span>
&lt;       - <span style="color:#e6db74">&#34;{{ item.acl.acl_index }} permit tcp any host {{ item.peer }} eq bgp log&#34;</span>
---
&gt;       - <span style="color:#e6db74">&#34;sequence {{ item.acl.acl_index }} permit tcp any host {{ item.peer }} eq bgp log&#34;</span>
143c140
&lt;   eos_config:
---
&gt;   ios_config:
145,146c142
&lt;       - <span style="color:#e6db74">&#34;no {{ item.acl.acl_index }}&#34;</span>
&lt;       - <span style="color:#e6db74">&#34;{{ item.acl.acl_index }} permit tcp host {{ item.peer }} eq bgp any log&#34;</span>
---
&gt;       - <span style="color:#e6db74">&#34;sequence {{ item.acl.acl_index }} permit tcp host {{ item.peer }} eq bgp any log&#34;</span>
161c157
&lt;   eos_config:
---
&gt;   ios_config:
163c159
&lt;       - ipv6 access-group <span style="color:#f92672">{{</span> item.acl.ipv6.in <span style="color:#f92672">}}</span> in
---
&gt;       - ipv6 traffic-filter <span style="color:#f92672">{{</span> item.acl.ipv6.in <span style="color:#f92672">}}</span> in
165c161
&lt;       - interface <span style="color:#f92672">{{</span> item.eos_if <span style="color:#f92672">}}</span>
---
&gt;       - interface <span style="color:#f92672">{{</span> item.ios_if <span style="color:#f92672">}}</span>
176c172
&lt;   eos_config:
---
&gt;   ios_config:
178c174
&lt;       - ipv6 access-group <span style="color:#f92672">{{</span> item.acl.ipv6.out <span style="color:#f92672">}}</span> out
---
&gt;       - ipv6 traffic-filter <span style="color:#f92672">{{</span> item.acl.ipv6.out <span style="color:#f92672">}}</span> out
180c176
&lt;       - interface <span style="color:#f92672">{{</span> item.eos_if <span style="color:#f92672">}}</span>
---
&gt;       - interface <span style="color:#f92672">{{</span> item.ios_if <span style="color:#f92672">}}</span>
</code></pre></div><p>The below is the relevant section in our <code>host_vars</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">bgp</span>:
  <span style="color:#66d9ef">neighbors</span>:
    <span style="color:#66d9ef">ipv4</span>:
     - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">10.100.103.254</span>
       <span style="color:#66d9ef">acl</span>:
         <span style="color:#66d9ef">acl_index</span>: <span style="color:#ae81ff">110</span>
    <span style="color:#66d9ef">ipv6</span>:
     - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:103::ffff&#34;</span>
       <span style="color:#66d9ef">acl</span>:
         <span style="color:#66d9ef">acl_index</span>: <span style="color:#ae81ff">110</span>
<span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">eos_if</span>: <span style="color:#e6db74">&#34;Ethernet1&#34;</span>
    <span style="color:#66d9ef">acl</span>:
      <span style="color:#66d9ef">ipv4</span>:
        <span style="color:#66d9ef">in</span>: EDGE-IN
        <span style="color:#66d9ef">out</span>: EDGE-OUT
      <span style="color:#66d9ef">ipv6</span>:
        <span style="color:#66d9ef">in</span>: EDGEv6-IN
        <span style="color:#66d9ef">out</span>: EDGEv6-OUT
</code></pre></div><p>The access-lists create rules based upon BGP peers, <strong>syslog</strong> and <strong>tacacs</strong>.</p>
<h4 id="generated-configuration">Generated configuration</h4>
<p>The configuration the above generates is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">interface Ethernet1
   ip access-group EDGE-IN in
   ip access-group EDGE-OUT out
   ipv6 access-group EDGEv6-IN in
   ipv6 access-group EDGEv6-OUT out
!
ipv6 access-list EDGEv6-IN
   10 permit icmpv6 any any log
   110 permit tcp host 2001:db8:103::ffff eq bgp any log
   1000 deny ipv6 any any log
!
ipv6 access-list EDGEv6-OUT
   10 permit icmpv6 any any log
   110 permit tcp any host 2001:db8:103::ffff eq bgp log
   1000 deny ipv6 any any log
!
ip access-list EDGE-IN
   10 permit icmp any any log
   20 permit tcp host 192.0.2.1 eq tacacs any log
   110 permit tcp host 10.100.103.254 eq bgp any log
   1000 deny ip any any log
!
ip access-list EDGE-OUT
   10 permit icmp any any log
   20 permit udp any host 10.100.103.254 eq syslog log
   30 permit tcp any host 192.0.2.1 eq tacacs log
   110 permit tcp any host 10.100.103.254 eq bgp log
   1000 deny ip any any log
!
</code></pre></div><p>If you apply this on a vEOS image, the access-lists will be created. However you will see these error messages when applying the <code>access-group</code> configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">arista-01(config)#interface Ethernet1
arista-01(config-if-Et1)#   ip access-group EDGE-IN in
% Unavailable command (not supported on this hardware platform)

arista-01(config-if-Et1)#   ip access-group EDGE-OUT out
% Unavailable command (not supported on this hardware platform)

arista-01(config-if-Et1)#   ipv6 access-group EDGEv6-IN in
% Unavailable command (not supported on this hardware platform)

arista-01(config-if-Et1)#   ipv6 access-group EDGEv6-OUT out
% Unavailable command (not supported on this hardware platform)
</code></pre></div><h3 id="routing">Routing</h3>
<p>This section is where we will configure BGP, OSPF and OSPFv3. As per the previous parts, we are using OSPF (for IPv4 routing) and OSPFv3 (for IPv6 routing) because not all vendors support IPv4 in OSPFv3. If you are targeting an Arista-only network, or deemed IPv4 support in OSPFv3 an acceptance criteria on any network hardware choices, then you could remove the need for OSPF.</p>
<h4 id="main-playbook">Main Playbook</h4>
<p>The main playbook looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for routing</span>
<span style="color:#75715e">##</span>
- <span style="color:#66d9ef">name</span>: Include OSPF routing
  <span style="color:#66d9ef">include</span>: ospf.yml

- <span style="color:#66d9ef">name</span>: Include OSPFv3 routing
  <span style="color:#66d9ef">include</span>: ospfv3.yml

- <span style="color:#66d9ef">name</span>: Include BGP routing
  <span style="color:#66d9ef">include</span>: bgp.yml
</code></pre></div><p>This is identical to the IOS playbook, as we are not doing anything except including other playbooks (for ease of reading/debugging).</p>
<h4 id="ospf-playbook">OSPF Playbook</h4>
<p>The contents of the OSPF playbook are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for routing</span>
<span style="color:#75715e">##</span>
- <span style="color:#66d9ef">name</span>: Enable IPv4 routing
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#e6db74">&#34;ip routing&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf

- <span style="color:#66d9ef">name</span>: OSPF Process
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#e6db74">&#34;router ospf 1&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf

- <span style="color:#66d9ef">name</span>: OSPF Process - Router ID
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#e6db74">&#34;router-id {{ router_id }}&#34;</span>
    <span style="color:#66d9ef">parents</span>: router ospf <span style="color:#ae81ff">1</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf

- <span style="color:#66d9ef">name</span>: OSPF Interfaces
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - ip ospf area {{ item.ospf.area }}
    <span style="color:#66d9ef">parents</span>: interface {{ item.eos_if }}
  <span style="color:#66d9ef">when</span>: item.ospf is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf

- <span style="color:#66d9ef">name</span>: OSPF Interfaces - Passive
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - passive-interface {{ item.eos_if }}
    <span style="color:#66d9ef">parents</span>: router ospf <span style="color:#ae81ff">1</span>
  <span style="color:#66d9ef">when</span>:
    - item.ospf is defined
    - item.ospf.passive is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf
</code></pre></div><p>There are two major differences with this playbook and the IOS playbook: -</p>
<ul>
<li>We enable <code>ip routing</code>
<ul>
<li>We are emulating an Arista switch, and many switches tend to have routing disabled by default</li>
</ul>
</li>
<li>We do not set a process ID in the <code>ip ospf area</code> command</li>
</ul>
<p>The reason for the latter is because Cisco allows multiple OSPF processes in the default routing table. By contrast, Arista do not. Arista allow multiple OSPF processes, but they must be configured as part of different VRFs (<strong>V</strong>irtual <strong>R</strong>outing and <strong>F</strong>orwarding). VRFs are used to create separate and unique routing tables, with different interfaces (or subinterfaces) bound to them. This makes it so each customer can have their own private routing table, with different egress and ingress points to their network, while still running over the same physical <strong>core</strong> network as each other.</p>
<p>Besides that, all other details are the same.</p>
<p>The below <code>host_vars</code> are relevant to the above playbook: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  - <span style="color:#66d9ef">eos_if</span>: <span style="color:#e6db74">&#34;Ethernet1&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
  - <span style="color:#66d9ef">eos_if</span>: <span style="color:#e6db74">&#34;Ethernet2&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
  - <span style="color:#66d9ef">eos_if</span>: <span style="color:#e6db74">&#34;Loopback0&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
</code></pre></div><h5 id="generated-configuration-1">Generated configuration</h5>
<p>The configuration this generates is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">interface Ethernet1
   ip ospf area 0.0.0.0
!
interface Ethernet2
   ip ospf area 0.0.0.0
!
interface Loopback0
   ip ospf area 0.0.0.0
!
router ospf 1
   router-id 192.0.2.103
   passive-interface Ethernet1
   passive-interface Loopback0
!
</code></pre></div><h5 id="verification-2">Verification</h5>
<p><strong>arista-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show OSPF interfaces
arista-01#show ip ospf interface brief
   Interface    Instance VRF        Area            IP Address         Cost  State      Nbrs
   Et2          1        default    0.0.0.0         10.100.203.254/24  10    Backup DR  1
   Lo0          1        default    0.0.0.0         192.0.2.103/32     10    DR         0
   Et1          1        default    0.0.0.0         10.100.103.253/24  10    DR         0

! Show OSPF neighbours
arista-01#show ip ospf neighbor
Neighbor ID     Instance VRF      Pri State                  Dead Time   Address         Interface
192.0.2.203     1        default  1   FULL/DR                00:00:29    10.100.203.253  Ethernet2

! Show routing table
arista-01#show ip route ospf

VRF: default
Codes: C - connected, S - static, K - kernel,
       O - OSPF, IA - OSPF inter area, E1 - OSPF external type 1,
       E2 - OSPF external type 2, N1 - OSPF NSSA external type 1,
       N2 - OSPF NSSA external type2, B - BGP, B I - iBGP, B E - eBGP,
       R - RIP, I L1 - IS-IS level 1, I L2 - IS-IS level 2,
       O3 - OSPFv3, A B - BGP Aggregate, A O - OSPF Summary,
       NG - Nexthop Group Static Route, V - VXLAN Control Service,
       DH - DHCP client installed default route, M - Martian,
       DP - Dynamic Policy Route, L - VRF Leaked

 O        192.0.2.203/32 [110/20] via 10.100.203.253, Ethernet2

! Ping!
arista-01#ping 192.0.2.203
PING 192.0.2.203 (192.0.2.203) 72(100) bytes of data.
80 bytes from 192.0.2.203: icmp_seq=1 ttl=64 time=3.54 ms
80 bytes from 192.0.2.203: icmp_seq=2 ttl=64 time=2.57 ms
80 bytes from 192.0.2.203: icmp_seq=3 ttl=64 time=2.57 ms
80 bytes from 192.0.2.203: icmp_seq=4 ttl=64 time=2.87 ms
80 bytes from 192.0.2.203: icmp_seq=5 ttl=64 time=2.92 ms

--- 192.0.2.203 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 14ms
rtt min/avg/max/mdev = 2.574/2.898/3.549/0.362 ms, ipg/ewma 3.525/3.222 ms

arista-01#ping 192.0.2.203 source 192.0.2.103
PING 192.0.2.203 (192.0.2.203) from 192.0.2.103 : 72(100) bytes of data.
80 bytes from 192.0.2.203: icmp_seq=1 ttl=64 time=3.04 ms
80 bytes from 192.0.2.203: icmp_seq=2 ttl=64 time=2.60 ms
80 bytes from 192.0.2.203: icmp_seq=3 ttl=64 time=2.58 ms
80 bytes from 192.0.2.203: icmp_seq=4 ttl=64 time=2.88 ms
80 bytes from 192.0.2.203: icmp_seq=5 ttl=64 time=3.23 ms

--- 192.0.2.203 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 14ms
rtt min/avg/max/mdev = 2.583/2.871/3.235/0.252 ms, ipg/ewma 3.524/2.971 ms
</code></pre></div><p><strong>arista-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show OSPF interfaces
arista-02#show ip ospf interface brief
   Interface    Instance VRF        Area            IP Address         Cost  State      Nbrs
   Lo0          1        default    0.0.0.0         192.0.2.203/32     10    DR         0
   Et1          1        default    0.0.0.0         10.100.203.253/24  10    DR         1

! Show OSPF neighbours
arista-02#show ip ospf neighbor
Neighbor ID     Instance VRF      Pri State                  Dead Time   Address         Interface
192.0.2.103     1        default  1   FULL/BDR               00:00:32    10.100.203.254  Ethernet1

! Show routing table
arista-02#show ip route ospf

VRF: default
Codes: C - connected, S - static, K - kernel,
       O - OSPF, IA - OSPF inter area, E1 - OSPF external type 1,
       E2 - OSPF external type 2, N1 - OSPF NSSA external type 1,
       N2 - OSPF NSSA external type2, B - BGP, B I - iBGP, B E - eBGP,
       R - RIP, I L1 - IS-IS level 1, I L2 - IS-IS level 2,
       O3 - OSPFv3, A B - BGP Aggregate, A O - OSPF Summary,
       NG - Nexthop Group Static Route, V - VXLAN Control Service,
       DH - DHCP client installed default route, M - Martian,
       DP - Dynamic Policy Route, L - VRF Leaked

 O        10.100.103.0/24 [110/20] via 10.100.203.254, Ethernet1
 O        192.0.2.103/32 [110/20] via 10.100.203.254, Ethernet1

! Ping!
arista-02#ping 192.0.2.103
PING 192.0.2.103 (192.0.2.103) 72(100) bytes of data.
80 bytes from 192.0.2.103: icmp_seq=1 ttl=64 time=3.23 ms
80 bytes from 192.0.2.103: icmp_seq=2 ttl=64 time=2.55 ms
80 bytes from 192.0.2.103: icmp_seq=3 ttl=64 time=2.61 ms
80 bytes from 192.0.2.103: icmp_seq=4 ttl=64 time=2.75 ms
80 bytes from 192.0.2.103: icmp_seq=5 ttl=64 time=3.12 ms

--- 192.0.2.103 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 13ms
rtt min/avg/max/mdev = 2.550/2.854/3.233/0.279 ms, ipg/ewma 3.352/3.050 ms

arista-02#ping 192.0.2.103 source lo0
PING 192.0.2.103 (192.0.2.103) from 192.0.2.203 : 72(100) bytes of data.
80 bytes from 192.0.2.103: icmp_seq=1 ttl=64 time=3.13 ms
80 bytes from 192.0.2.103: icmp_seq=2 ttl=64 time=2.50 ms
80 bytes from 192.0.2.103: icmp_seq=3 ttl=64 time=2.59 ms
80 bytes from 192.0.2.103: icmp_seq=4 ttl=64 time=2.78 ms
80 bytes from 192.0.2.103: icmp_seq=5 ttl=64 time=3.18 ms

--- 192.0.2.103 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 13ms
rtt min/avg/max/mdev = 2.505/2.842/3.188/0.277 ms, ipg/ewma 3.291/3.001 ms

arista-02#ping 10.100.103.253 source lo0
PING 10.100.103.253 (10.100.103.253) from 192.0.2.203 : 72(100) bytes of data.
80 bytes from 10.100.103.253: icmp_seq=1 ttl=64 time=3.26 ms
80 bytes from 10.100.103.253: icmp_seq=2 ttl=64 time=2.75 ms
80 bytes from 10.100.103.253: icmp_seq=3 ttl=64 time=2.76 ms
80 bytes from 10.100.103.253: icmp_seq=4 ttl=64 time=4.98 ms
80 bytes from 10.100.103.253: icmp_seq=5 ttl=64 time=3.42 ms

--- 10.100.103.253 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 15ms
rtt min/avg/max/mdev = 2.759/3.440/4.983/0.815 ms, ipg/ewma 3.841/3.384 ms
</code></pre></div><p>All looking good!</p>
<h4 id="ospfv3-playbook">OSPFv3 Playbook</h4>
<p>As noted, we run OSPFv3 for IPv6 routing. It is technically capable of IPv4 routing to, but not all vendors support this.</p>
<p>The contents of the playbook are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for routing</span>
<span style="color:#75715e">##</span>

- <span style="color:#66d9ef">name</span>: Enable IPv6 routing
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#e6db74">&#34;ipv6 unicast-routing&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospfv3

- <span style="color:#66d9ef">name</span>: OSPFv3 Process
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#e6db74">&#34;router ospfv3&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospfv3

- <span style="color:#66d9ef">name</span>: OSPFv3 Process - Router ID
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#e6db74">&#34;router-id {{ router_id }}&#34;</span>
    <span style="color:#66d9ef">parents</span>: router ospfv3
  <span style="color:#66d9ef">tags</span>:
    - ospfv3

- <span style="color:#66d9ef">name</span>: OSPFv3 Interfaces
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - ospf ipv6 area {{ item.ospfv3.area }}
    <span style="color:#66d9ef">parents</span>: interface {{ item.eos_if }}
  <span style="color:#66d9ef">when</span>: item.ospfv3 is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospfv3

- <span style="color:#66d9ef">name</span>: OSPFv3 Interfaces - Passive
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - ospf ipv6 passive-interface
    <span style="color:#66d9ef">parents</span>: interface {{ item.eos_if }}
  <span style="color:#66d9ef">when</span>:
    - item.ospfv3 is defined
    - item.ospfv3.passive is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospfv3

- <span style="color:#66d9ef">name</span>: Removed IPv4 address family
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#e6db74">&#34;no address-family ipv4&#34;</span>
    <span style="color:#66d9ef">parents</span>: router ospfv3
  <span style="color:#66d9ef">tags</span>:
    - ospfv3
</code></pre></div><p>There are some small differences in this playbook from the IOS playbook: -</p>
<ul>
<li>No OSPF process ID is specified anywhere in Arista (for the same reasons as IPv4)</li>
<li><code>ospf ipv6 area</code> is used under an interface rather than <code>ipv6 ospf 1 area</code></li>
<li>Passive interface configuration is at the interface level, rather than the process level (<code>parents: interface</code> rather than <code>parents: router ospfv3 1</code>)</li>
<li><code>no address-family ipv4</code> disables the IPv4 address family, the <code>unicast</code> option is not required</li>
</ul>
<p>The differences between the playbooks themselves are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">diff arista/roles/routing/tasks/ospfv3.yml cisco/roles/routing/tasks/ospfv3.yml
6c6
&lt;   eos_config:
---
&gt;   ios_config:
9,10d8
&lt;   tags:
&lt;     - ospfv3
13c11
&lt;   eos_config:
---
&gt;   ios_config:
15,17c13
&lt;       - <span style="color:#e6db74">&#34;router ospfv3&#34;</span>
&lt;   tags:
&lt;     - ospfv3
---
&gt;       - <span style="color:#e6db74">&#34;router ospfv3 1&#34;</span>
20c16
&lt;   eos_config:
---
&gt;   ios_config:
23,25c19
&lt;     parents: router ospfv3
&lt;   tags:
&lt;     - ospfv3
---
&gt;     parents: router ospfv3 <span style="color:#ae81ff">1</span>
28c22
&lt;   eos_config:
---
&gt;   ios_config:
30,31c24,25
&lt;       - ospf ipv6 area <span style="color:#f92672">{{</span> item.ospfv3.area <span style="color:#f92672">}}</span>
&lt;     parents: interface <span style="color:#f92672">{{</span> item.eos_if <span style="color:#f92672">}}</span>
---
&gt;       - ipv6 ospf <span style="color:#ae81ff">1</span> area <span style="color:#f92672">{{</span> item.ospfv3.area <span style="color:#f92672">}}</span>
&gt;     parents: interface <span style="color:#f92672">{{</span> item.ios_if <span style="color:#f92672">}}</span>
34,35d27
&lt;   tags:
&lt;     - ospfv3
38c30
&lt;   eos_config:
---
&gt;   ios_config:
40,41c32,33
&lt;       - ospf ipv6 passive-interface
&lt;     parents: interface <span style="color:#f92672">{{</span> item.eos_if <span style="color:#f92672">}}</span>
---
&gt;       - passive-interface <span style="color:#f92672">{{</span> item.ios_if <span style="color:#f92672">}}</span>
&gt;     parents: router ospfv3 <span style="color:#ae81ff">1</span>
46,47d37
&lt;   tags:
&lt;     - ospfv3
50c40
&lt;   eos_config:
---
&gt;   ios_config:
52,55c42,43
&lt;       - <span style="color:#e6db74">&#34;no address-family ipv4&#34;</span>
&lt;     parents: router ospfv3
&lt;   tags:
&lt;     - ospfv3
---
&gt;       - <span style="color:#e6db74">&#34;no address-family ipv4 unicast&#34;</span>
&gt;     parents: router ospfv3 <span style="color:#ae81ff">1</span>
</code></pre></div><p>In some ways I prefer this configuration. All of the OSPF configuration for an interface is grouped together, rather than partly under the process, partly under the interface.</p>
<p>The relevant <code>host_vars</code> are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  - <span style="color:#66d9ef">eos_if</span>: <span style="color:#e6db74">&#34;Ethernet1&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
  - <span style="color:#66d9ef">eos_if</span>: <span style="color:#e6db74">&#34;Ethernet2&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
  - <span style="color:#66d9ef">eos_if</span>: <span style="color:#e6db74">&#34;Loopback0&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
</code></pre></div><h5 id="generated-configuration-2">Generated configuration</h5>
<p>The generated configuration is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">interface Ethernet1
   ospfv3 ipv6 passive-interface
   ospfv3 ipv6 area 0.0.0.0
!
interface Ethernet2
   ospfv3 ipv6 area 0.0.0.0
!
interface Loopback0
   ospfv3 ipv6 passive-interface
   ospfv3 ipv6 area 0.0.0.0
!
router ospfv3
   router-id 192.0.2.103
   !
   address-family ipv6
</code></pre></div><p>Arista appears to translate the <code>ospf ipv6</code> command to <code>ospfv3 ipv6</code>, so you could change the playbook to this style instead.</p>
<h5 id="verification-3">Verification</h5>
<p><strong>arista-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show OSPF interfaces (no brief option available)
arista-01# show ospfv3 interface
OSPFv3 address-family ipv6
Ethernet2 is up
  Interface Address fe80::5054:ff:fec5:df3c, VRF default, Area 0.0.0.0
  Network Type Broadcast, Cost 10
  Transmit Delay is 1 sec, State Backup DR, Priority 1
  Designated Router is 192.0.2.203
  Backup Designated Router is 192.0.2.103
  Timer intervals configured, Hello 10, Dead 40, Retransmit 5
  Neighbor Count is 1
  Options are R E V6
Ethernet1 is up
  Interface Address fe80::5054:ff:fec5:df3c, VRF default, Area 0.0.0.0
  Network Type Broadcast, Cost 10
  Transmit Delay is 1 sec, State DR, Priority 1
  Designated Router is 192.0.2.103
  Backup Designated Router is 0.0.0.0
  Timer intervals configured, Hello 10, Dead 40, Retransmit 5
  Neighbor Count is 0 (Passive Interface)
  Options are R E V6
Loopback0 is up
  Interface Address unassigned, VRF default, Area 0.0.0.0
  Network Type Broadcast, Cost 10
  Transmit Delay is 1 sec, State DR, Priority 1
  Designated Router is 192.0.2.103
  Backup Designated Router is 0.0.0.0
  Timer intervals configured, Hello 10, Dead 40, Retransmit 5
  Neighbor Count is 0 (Passive Interface)
  Options are R E V6

! Show OSPF neighbours
arista-01#show ipv6 ospf neighbor
Routing Process &#34;ospf 0&#34;:
Neighbor 192.0.2.203 VRF default priority is 1, state is Full
  In area 0.0.0.0 interface Ethernet2
  DR is 192.0.2.203 BDR is 192.0.2.103
  Options is R E V6
  Dead timer is due in 29 seconds
  Graceful-restart-helper mode is Inactive
  Graceful-restart attempts: 0

! Show routing table
arista-01#show ipv6 route ospf

VRF: default
Displaying 1 of 11 IPv6 routing table entries
Codes: C - connected, S - static, K - kernel, O3 - OSPFv3, B - BGP, R - RIP, A B - BGP Aggregate, I L1 - IS-IS level 1, I L2 - IS-IS level 2, DH - DHCP, NG - Nexthop Group Static Route, M - Martian, DP - Dynamic Policy Route, L - VRF Leaked

 O3       2001:db8:903:beef::2/128 [110/20]
           via fe80::5054:ff:fed8:9755, Ethernet2

! Ping!
arista-01#ping 2001:db8:903:beef::2
PING 2001:db8:903:beef::2(2001:db8:903:beef::2) 72 data bytes
80 bytes from 2001:db8:903:beef::2: icmp_seq=1 ttl=64 time=7.31 ms
80 bytes from 2001:db8:903:beef::2: icmp_seq=2 ttl=64 time=2.62 ms
80 bytes from 2001:db8:903:beef::2: icmp_seq=3 ttl=64 time=2.55 ms
80 bytes from 2001:db8:903:beef::2: icmp_seq=4 ttl=64 time=2.54 ms
80 bytes from 2001:db8:903:beef::2: icmp_seq=5 ttl=64 time=2.51 ms

--- 2001:db8:903:beef::2 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 27ms
rtt min/avg/max/mdev = 2.513/3.509/7.310/1.901 ms, ipg/ewma 6.827/5.341 ms

arista-01#ping 2001:db8:903:beef::2 source lo0
PING 2001:db8:903:beef::2(2001:db8:903:beef::2) from 2001:db8:903:beef::1 : 72 data bytes
80 bytes from 2001:db8:903:beef::2: icmp_seq=1 ttl=64 time=3.02 ms
80 bytes from 2001:db8:903:beef::2: icmp_seq=2 ttl=64 time=2.39 ms
80 bytes from 2001:db8:903:beef::2: icmp_seq=3 ttl=64 time=2.44 ms
80 bytes from 2001:db8:903:beef::2: icmp_seq=4 ttl=64 time=2.67 ms
80 bytes from 2001:db8:903:beef::2: icmp_seq=5 ttl=64 time=3.12 ms

--- 2001:db8:903:beef::2 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 13ms
rtt min/avg/max/mdev = 2.393/2.732/3.121/0.297 ms, ipg/ewma 3.388/2.890 ms
</code></pre></div><p><strong>arista-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show OSPF interfaces
arista-02#show ipv6 ospf interface
Ethernet1 is up
  Interface Address fe80::5054:ff:fed8:9755, VRF default, Area 0.0.0.0
  Network Type Broadcast, Cost 10
  Transmit Delay is 1 sec, State DR, Priority 1
  Designated Router is 192.0.2.203
  Backup Designated Router is 192.0.2.103
  Timer intervals configured, Hello 10, Dead 40, Retransmit 5
  Neighbor Count is 1
  Options are R E V6
Loopback0 is up
  Interface Address unassigned, VRF default, Area 0.0.0.0
  Network Type Broadcast, Cost 10
  Transmit Delay is 1 sec, State DR, Priority 1
  Designated Router is 192.0.2.203
  Backup Designated Router is 0.0.0.0
  Timer intervals configured, Hello 10, Dead 40, Retransmit 5
  Neighbor Count is 0 (Passive Interface)
  Options are R E V6

! Show OSPF neighbours
arista-02#show ipv6 ospf neighbor
Routing Process &#34;ospf 0&#34;:
Neighbor 192.0.2.103 VRF default priority is 1, state is Full
  In area 0.0.0.0 interface Ethernet1
  DR is 192.0.2.203 BDR is 192.0.2.103
  Options is R E V6
  Dead timer is due in 31 seconds
  Graceful-restart-helper mode is Inactive
  Graceful-restart attempts: 0

! Show routing table
arista-02#show ipv6 route ospf

VRF: default
Displaying 2 of 10 IPv6 routing table entries
Codes: C - connected, S - static, K - kernel, O3 - OSPFv3, B - BGP, R - RIP, A B - BGP Aggregate, I L1 - IS-IS level 1, I L2 - IS-IS level 2, DH - DHCP, NG - Nexthop Group Static Route, M - Martian, DP - Dynamic Policy Route, L - VRF Leaked

 O3       2001:db8:103::/64 [110/20]
           via fe80::5054:ff:fec5:df3c, Ethernet1
 O3       2001:db8:903:beef::1/128 [110/20]
           via fe80::5054:ff:fec5:df3c, Ethernet1

! Ping!
arista-02#ping 2001:db8:903:beef::1
PING 2001:db8:903:beef::1(2001:db8:903:beef::1) 72 data bytes
80 bytes from 2001:db8:903:beef::1: icmp_seq=1 ttl=64 time=6.94 ms
80 bytes from 2001:db8:903:beef::1: icmp_seq=2 ttl=64 time=2.67 ms
80 bytes from 2001:db8:903:beef::1: icmp_seq=3 ttl=64 time=2.64 ms
80 bytes from 2001:db8:903:beef::1: icmp_seq=4 ttl=64 time=2.64 ms
80 bytes from 2001:db8:903:beef::1: icmp_seq=5 ttl=64 time=2.47 ms

--- 2001:db8:903:beef::1 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 25ms
rtt min/avg/max/mdev = 2.474/3.477/6.946/1.736 ms, ipg/ewma 6.387/5.147 ms

arista-02#ping 2001:db8:903:beef::1 source lo0
PING 2001:db8:903:beef::1(2001:db8:903:beef::1) from 2001:db8:903:beef::2 : 72 data bytes
80 bytes from 2001:db8:903:beef::1: icmp_seq=1 ttl=64 time=3.17 ms
80 bytes from 2001:db8:903:beef::1: icmp_seq=2 ttl=64 time=2.61 ms
80 bytes from 2001:db8:903:beef::1: icmp_seq=3 ttl=64 time=2.48 ms
80 bytes from 2001:db8:903:beef::1: icmp_seq=4 ttl=64 time=2.47 ms
80 bytes from 2001:db8:903:beef::1: icmp_seq=5 ttl=64 time=3.59 ms

--- 2001:db8:903:beef::1 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 13ms
rtt min/avg/max/mdev = 2.476/2.868/3.592/0.447 ms, ipg/ewma 3.267/3.039 ms

arista-02#ping 2001:db8:103::f
PING 2001:db8:103::f(2001:db8:103::f) 72 data bytes
80 bytes from 2001:db8:103::f: icmp_seq=1 ttl=64 time=3.10 ms
80 bytes from 2001:db8:103::f: icmp_seq=2 ttl=64 time=2.51 ms
80 bytes from 2001:db8:103::f: icmp_seq=3 ttl=64 time=2.47 ms
80 bytes from 2001:db8:103::f: icmp_seq=4 ttl=64 time=2.99 ms
80 bytes from 2001:db8:103::f: icmp_seq=5 ttl=64 time=3.26 ms

--- 2001:db8:103::f ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 13ms
rtt min/avg/max/mdev = 2.476/2.872/3.269/0.322 ms, ipg/ewma 3.308/3.002 ms
</code></pre></div><p>Unfortunately the IPv6 verification commands for OSPF are a little on the verbose side, with no <code>brief</code> options available. It is nice to see the extra information, but it makes it harder to quickly glance at the output to see a status for all neighbours and interfaces.</p>
<p>Other than though, everything is looking good!</p>
<h4 id="bgp-playbook">BGP Playbook</h4>
<p>We are using BGP to communicate with <strong>netsvr-01</strong>, as well as advertising routes received from <strong>netsvr-01</strong> from the <strong>edge</strong> switch to the <strong>internal</strong> switch.</p>
<p>The contents of the playbook are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for routing</span>
<span style="color:#75715e">##</span>
- <span style="color:#66d9ef">name</span>: Configure BGP - eBGP v4 peers
  <span style="color:#66d9ef">eos_bgp</span>:
    <span style="color:#66d9ef">config</span>:
      <span style="color:#66d9ef">bgp_as</span>: <span style="color:#e6db74">&#34;{{ bgp[&#39;local_as&#39;] }}&#34;</span>
      <span style="color:#66d9ef">log_neighbor_changes</span>: True
      <span style="color:#66d9ef">router_id</span>: <span style="color:#e6db74">&#34;{{ router_id }}&#34;</span>
      <span style="color:#66d9ef">neighbors</span>:
      - <span style="color:#66d9ef">neighbor</span>: <span style="color:#e6db74">&#34;{{ item.peer }}&#34;</span>
        <span style="color:#66d9ef">remote_as</span>: <span style="color:#e6db74">&#34;{{ item.remote_as }}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv4 is defined
    - item.ebgp is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv4 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v4

- <span style="color:#66d9ef">name</span>: Configure BGP - eBGP v6 peers
  <span style="color:#66d9ef">eos_bgp</span>:
    <span style="color:#66d9ef">config</span>:
      <span style="color:#66d9ef">bgp_as</span>: <span style="color:#e6db74">&#34;{{ bgp[&#39;local_as&#39;] }}&#34;</span>
      <span style="color:#66d9ef">log_neighbor_changes</span>: True
      <span style="color:#66d9ef">router_id</span>: <span style="color:#e6db74">&#34;{{ router_id }}&#34;</span>
      <span style="color:#66d9ef">neighbors</span>:
      - <span style="color:#66d9ef">neighbor</span>: <span style="color:#e6db74">&#34;{{ item.peer }}&#34;</span>
        <span style="color:#66d9ef">remote_as</span>: <span style="color:#e6db74">&#34;{{ item.remote_as }}&#34;</span>
      <span style="color:#66d9ef">address_family</span>:
        - <span style="color:#66d9ef">afi</span>: ipv6
          <span style="color:#66d9ef">neighbors</span>:
          - <span style="color:#66d9ef">neighbor</span>: <span style="color:#e6db74">&#34;{{ item.peer }}&#34;</span>
            <span style="color:#66d9ef">activate</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">when</span>:
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv6 is defined
    - item.ebgp is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv6 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v6

- <span style="color:#66d9ef">name</span>: Configure BGP - iBGP v4 peers
  <span style="color:#66d9ef">eos_bgp</span>:
    <span style="color:#66d9ef">config</span>:
      <span style="color:#66d9ef">bgp_as</span>: <span style="color:#e6db74">&#34;{{ bgp[&#39;local_as&#39;] }}&#34;</span>
      <span style="color:#66d9ef">log_neighbor_changes</span>: True
      <span style="color:#66d9ef">router_id</span>: <span style="color:#e6db74">&#34;{{ router_id }}&#34;</span>
      <span style="color:#66d9ef">neighbors</span>:
      - <span style="color:#66d9ef">neighbor</span>: <span style="color:#e6db74">&#34;{{ item.peer }}&#34;</span>
        <span style="color:#66d9ef">remote_as</span>: <span style="color:#e6db74">&#34;{{ item.remote_as }}&#34;</span>
        <span style="color:#66d9ef">update_source</span>: <span style="color:#e6db74">&#34;{{ item.update_source }}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv4 is defined
    - item.ibgp is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv4 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v4

- <span style="color:#66d9ef">name</span>: Configure BGP - iBGP v6 peers
  <span style="color:#66d9ef">eos_bgp</span>:
    <span style="color:#66d9ef">config</span>:
      <span style="color:#66d9ef">bgp_as</span>: <span style="color:#e6db74">&#34;{{ bgp[&#39;local_as&#39;] }}&#34;</span>
      <span style="color:#66d9ef">log_neighbor_changes</span>: True
      <span style="color:#66d9ef">router_id</span>: <span style="color:#e6db74">&#34;{{ router_id }}&#34;</span>
      <span style="color:#66d9ef">neighbors</span>:
      - <span style="color:#66d9ef">neighbor</span>: <span style="color:#e6db74">&#34;{{ item.peer }}&#34;</span>
        <span style="color:#66d9ef">remote_as</span>: <span style="color:#e6db74">&#34;{{ item.remote_as }}&#34;</span>
        <span style="color:#66d9ef">update_source</span>: <span style="color:#e6db74">&#34;{{ item.update_source }}&#34;</span>
      <span style="color:#66d9ef">address_family</span>:
        - <span style="color:#66d9ef">afi</span>: ipv6
          <span style="color:#66d9ef">neighbors</span>:
          - <span style="color:#66d9ef">neighbor</span>: <span style="color:#e6db74">&#34;{{ item.peer }}&#34;</span>
            <span style="color:#66d9ef">activate</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">when</span>:
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv6 is defined
    - item.ibgp is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv6 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v6

- <span style="color:#66d9ef">name</span>: Configure BGP - Redistribute OSPF
  <span style="color:#66d9ef">eos_bgp</span>:
    <span style="color:#66d9ef">config</span>:
      <span style="color:#66d9ef">bgp_as</span>: <span style="color:#e6db74">&#34;{{ bgp[&#39;local_as&#39;] }}&#34;</span>
      <span style="color:#66d9ef">redistribute</span>:
        - <span style="color:#66d9ef">protocol</span>: ospf
        - <span style="color:#66d9ef">protocol</span>: connected
  <span style="color:#66d9ef">when</span>:
    - bgp is defined
    - bgp.redist is defined
    - bgp.redist.ospf is defined
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v4

- <span style="color:#66d9ef">name</span>: Configure BGP - Redistribute OSPFv3
  <span style="color:#66d9ef">eos_bgp</span>:
    <span style="color:#66d9ef">config</span>:
      <span style="color:#66d9ef">bgp_as</span>: <span style="color:#e6db74">&#34;{{ bgp[&#39;local_as&#39;] }}&#34;</span>
      <span style="color:#66d9ef">address_family</span>:
        - <span style="color:#66d9ef">afi</span>: ipv6
          <span style="color:#66d9ef">redistribute</span>:
            - <span style="color:#66d9ef">protocol</span>: ospf
  <span style="color:#66d9ef">when</span>:
    - bgp is defined
    - bgp.redist is defined
    - bgp.redist.ospfv3 is defined
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v6

- <span style="color:#66d9ef">name</span>: Configure BGP - iBGP v4 Default Originate
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - neighbor {{ item.peer }} default-originate
    <span style="color:#66d9ef">parents</span>:
      - router bgp {{ bgp[<span style="color:#e6db74">&#39;local_as&#39;</span>] }}
  <span style="color:#66d9ef">when</span>:
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv4 is defined
    - item.ibgp is defined
    - item.default_originate is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv4 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v4
</code></pre></div><p>The main differences here are: -</p>
<ul>
<li>Arista does not support the <code>include-connected</code> statement when redistributing <strong>OSPF</strong></li>
<li>We redistribute connected routes, due to the above</li>
<li>Most of the IPv4-specific statements do not go under <code>address-family ipv4</code> (e.g. redistribution)</li>
</ul>
<p>Other than that, there are no major differences: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">&lt;   eos_bgp:
---
&gt;   ios_bgp:
24c24
&lt;   eos_bgp:
---
&gt;   ios_bgp:
48c48
&lt;   eos_bgp:
---
&gt;   ios_bgp:
68c68
&lt;   eos_bgp:
---
&gt;   ios_bgp:
92,93c92,93
&lt; - name: Configure BGP - Redistribute OSPF and connected routes
&lt;   eos_bgp:
---
&gt; - name: Configure BGP - Redistribute OSPF
&gt;   ios_bgp:
96,98c96,100
&lt;       redistribute:
&lt;         - protocol: ospf
&lt;         - protocol: connected
---
&gt;       address_family:
&gt;         - afi: ipv4
&gt;           redistribute:
&gt;             - protocol: ospf
&gt;               id: <span style="color:#ae81ff">1</span>
107,108c109,110
&lt; - name: Configure BGP - Redistribute OSPFv3 and connected routes
&lt;   eos_bgp:
---
&gt; - name: Configure BGP - Redistribute OSPFv3
&gt;   ios_bgp:
115c117
&lt;             - protocol: connected
---
&gt;               id: <span style="color:#ae81ff">1</span>
123a126,140
&gt; - name: Configure BGP - Redistribute OSPFv3 and include connected networks
&gt;   ios_config:
&gt;     lines:
&gt;       - redistribute ospf <span style="color:#ae81ff">1</span> include-connected
&gt;     parents:
&gt;       - router bgp <span style="color:#f92672">{{</span> bgp<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;local_as&#39;</span><span style="color:#f92672">]</span> <span style="color:#f92672">}}</span>
&gt;       - address-family ipv6 unicast
&gt;   when:
&gt;     - bgp is defined
&gt;     - bgp.redist is defined
&gt;     - bgp.redist.ospfv3 is defined
&gt;   tags:
&gt;   - bgp
&gt;   - bgp_v6
&gt;
125c142
&lt;   eos_config:
---
&gt;   ios_config:
129a147
&gt;       - address-family ipv4 unicast
</code></pre></div><p>The relevant <code>host_vars</code> for this are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">bgp</span>:
  <span style="color:#66d9ef">local_as</span>: <span style="color:#ae81ff">65103</span>
  <span style="color:#66d9ef">redist</span>:
    <span style="color:#66d9ef">ospf</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">neighbors</span>:
    <span style="color:#66d9ef">ipv4</span>:
     - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">10.100.103.254</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65430</span>
       <span style="color:#66d9ef">ebgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">acl</span>:
         <span style="color:#66d9ef">acl_index</span>: <span style="color:#ae81ff">110</span>
     - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">192.0.2.203</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65103</span>
       <span style="color:#66d9ef">ibgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">update_source</span>: Loopback0
       <span style="color:#66d9ef">default_originate</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv6</span>:
     - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:103::ffff&#34;</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65430</span>
       <span style="color:#66d9ef">ebgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">acl</span>:
         <span style="color:#66d9ef">acl_index</span>: <span style="color:#ae81ff">110</span>
     - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:903:beef::2&#34;</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65103</span>
       <span style="color:#66d9ef">ibgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">update_source</span>: Loopback0
</code></pre></div><h5 id="generated-configuration-3">Generated configuration</h5>
<p>The generated configuration from the above is: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">router bgp 65103
   router-id 192.0.2.103
   neighbor 10.100.103.254 remote-as 65430
   neighbor 192.0.2.203 remote-as 65103
   neighbor 192.0.2.203 update-source Loopback0
   neighbor 192.0.2.203 default-originate
   neighbor 192.0.2.203 maximum-routes 12000
   neighbor 2001:db8:103::ffff remote-as 65430
   neighbor 2001:db8:903:beef::2 remote-as 65103
   neighbor 2001:db8:903:beef::2 update-source Loopback0
   redistribute connected
   redistribute ospf
   !
   address-family ipv6
      neighbor 2001:db8:103::ffff activate
      neighbor 2001:db8:903:beef::2 activate
      redistribute ospfv3
</code></pre></div><p>Other than the slightly shorter <code>address-family</code> commands, this would qualify as valid Cisco IOS configuration too!</p>
<h5 id="verification-4">Verification</h5>
<p><strong>arista-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show BGP neighbours on IPv4 and IPv6
arista-01#show ip bgp summary
BGP summary information for VRF default
Router identifier 192.0.2.103, local AS number 65103
Neighbor Status Codes: m - Under maintenance
  Neighbor         V  AS           MsgRcvd   MsgSent  InQ OutQ  Up/Down State   PfxRcd PfxAcc
  10.100.103.254   4  65430            403       403    0    0 06:36:38 Estab   1      1
  192.0.2.203      4  65103            399       404    0    0 06:35:11 Estab   0      0

arista-01#show ipv6 bgp summary
BGP summary information for VRF default
Router identifier 192.0.2.103, local AS number 65103
Neighbor Status Codes: m - Under maintenance
  Neighbor         V  AS           MsgRcvd   MsgSent  InQ OutQ  Up/Down State   PfxRcd PfxAcc
  2001:db8:103::ffff 4  65430            402       402    0    0 06:36:56 Estab   1      1
  2001:db8:903:beef::2 4  65103            399       402    0    0 06:35:31 Estab   0      0

! Show BGP routes
arista-01#show ip bgp
BGP routing table information for VRF default
Router identifier 192.0.2.103, local AS number 65103
Route status codes: s - suppressed, * - valid, &gt; - active, # - not installed, E - ECMP head, e - ECMP
                    S - Stale, c - Contributing to ECMP, b - backup, L - labeled-unicast
Origin codes: i - IGP, e - EGP, ? - incomplete
AS Path Attributes: Or-ID - Originator ID, C-LST - Cluster List, LL Nexthop - Link Local Nexthop

         Network                Next Hop            Metric  LocPref Weight  Path
 * &gt;     10.15.30.0/24          -                     1       0       -       i
 * &gt;     10.100.103.0/24        -                     1       0       -       i
 * &gt;     10.100.203.0/24        -                     1       0       -       i
 * &gt;     192.0.2.1/32           10.100.103.254        0       100     0       65430 i
 * &gt;     192.0.2.103/32         -                     0       0       -       i
 * &gt;     192.0.2.203/32         -                     20      0       -       i
 * &gt;     192.168.122.0/24       -                     1       0       -       i

arista-01#show ipv6 bgp
BGP routing table information for VRF default
Router identifier 192.0.2.103, local AS number 65103
Route status codes: s - suppressed, * - valid, &gt; - active, # - not installed, E - ECMP head, e - ECMP
                    S - Stale, c - Contributing to ECMP, b - backup, L - labeled-unicast
Origin codes: i - IGP, e - EGP, ? - incomplete
AS Path Attributes: Or-ID - Originator ID, C-LST - Cluster List, LL Nexthop - Link Local Nexthop

         Network                Next Hop            Metric  LocPref Weight  Path
 * &gt;     2001:db8:103::/64      -                     1       0       -       i
 * &gt;     2001:db8:203::/64      -                     1       0       -       i
 * &gt;     2001:db8:903:beef::1/128 -                     0       0       -       i
 * &gt;     2001:db8:903:beef::2/128 -                     20      0       -       i
 * &gt;     2001:db8:999:beef::1/128 2001:db8:103::ffff    0       100     0       65430 i

! Ping the netsvr Loopback (192.0.2.1 and 2001:DB8:999:BEEF::1)
arista-01#ping 192.0.2.1
PING 192.0.2.1 (192.0.2.1) 72(100) bytes of data.
80 bytes from 192.0.2.1: icmp_seq=1 ttl=64 time=1.52 ms
80 bytes from 192.0.2.1: icmp_seq=2 ttl=64 time=1.08 ms
80 bytes from 192.0.2.1: icmp_seq=3 ttl=64 time=1.01 ms
80 bytes from 192.0.2.1: icmp_seq=4 ttl=64 time=1.12 ms
80 bytes from 192.0.2.1: icmp_seq=5 ttl=64 time=1.56 ms

--- 192.0.2.1 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 7ms
rtt min/avg/max/mdev = 1.017/1.263/1.564/0.232 ms, ipg/ewma 1.899/1.398 ms

arista-01#ping 192.0.2.1 source lo0
PING 192.0.2.1 (192.0.2.1) from 192.0.2.103 : 72(100) bytes of data.
80 bytes from 192.0.2.1: icmp_seq=1 ttl=64 time=1.48 ms
80 bytes from 192.0.2.1: icmp_seq=2 ttl=64 time=1.04 ms
80 bytes from 192.0.2.1: icmp_seq=3 ttl=64 time=1.05 ms
80 bytes from 192.0.2.1: icmp_seq=4 ttl=64 time=1.11 ms
80 bytes from 192.0.2.1: icmp_seq=5 ttl=64 time=1.47 ms

--- 192.0.2.1 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 7ms
rtt min/avg/max/mdev = 1.040/1.233/1.486/0.204 ms, ipg/ewma 1.916/1.365 ms

arista-01#ping 192.0.2.1 source 10.100.203.254
PING 192.0.2.1 (192.0.2.1) from 10.100.203.254 : 72(100) bytes of data.
80 bytes from 192.0.2.1: icmp_seq=1 ttl=64 time=1.47 ms
80 bytes from 192.0.2.1: icmp_seq=2 ttl=64 time=1.09 ms
80 bytes from 192.0.2.1: icmp_seq=3 ttl=64 time=1.03 ms
80 bytes from 192.0.2.1: icmp_seq=4 ttl=64 time=1.23 ms
80 bytes from 192.0.2.1: icmp_seq=5 ttl=64 time=1.60 ms

--- 192.0.2.1 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 7ms
rtt min/avg/max/mdev = 1.038/1.290/1.607/0.219 ms, ipg/ewma 1.825/1.392 ms

arista-01#ping 2001:db8:999:beef::1
PING 2001:db8:999:beef::1(2001:db8:999:beef::1) 72 data bytes
80 bytes from 2001:db8:999:beef::1: icmp_seq=1 ttl=64 time=1.42 ms
80 bytes from 2001:db8:999:beef::1: icmp_seq=2 ttl=64 time=1.09 ms
80 bytes from 2001:db8:999:beef::1: icmp_seq=3 ttl=64 time=1.19 ms
80 bytes from 2001:db8:999:beef::1: icmp_seq=4 ttl=64 time=1.59 ms
80 bytes from 2001:db8:999:beef::1: icmp_seq=5 ttl=64 time=1.76 ms

--- 2001:db8:999:beef::1 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 7ms
rtt min/avg/max/mdev = 1.092/1.413/1.764/0.249 ms, ipg/ewma 1.903/1.435 ms

arista-01#ping 2001:db8:999:beef::1 source lo0
PING 2001:db8:999:beef::1(2001:db8:999:beef::1) from 2001:db8:903:beef::1 : 72 data bytes
80 bytes from 2001:db8:999:beef::1: icmp_seq=1 ttl=64 time=1.52 ms
80 bytes from 2001:db8:999:beef::1: icmp_seq=2 ttl=64 time=1.08 ms
80 bytes from 2001:db8:999:beef::1: icmp_seq=3 ttl=64 time=1.05 ms
80 bytes from 2001:db8:999:beef::1: icmp_seq=4 ttl=64 time=1.11 ms
80 bytes from 2001:db8:999:beef::1: icmp_seq=5 ttl=64 time=1.71 ms

--- 2001:db8:999:beef::1 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 7ms
rtt min/avg/max/mdev = 1.056/1.297/1.713/0.271 ms, ipg/ewma 1.851/1.420 ms
</code></pre></div><p><strong>arista-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show BGP neighbours on IPv4 and IPv6
arista-02#show ip bgp summary
BGP summary information for VRF default
Router identifier 192.0.2.203, local AS number 65103
Neighbor Status Codes: m - Under maintenance
  Neighbor         V  AS           MsgRcvd   MsgSent  InQ OutQ  Up/Down State   PfxRcd PfxAcc
  192.0.2.103      4  65103            412       407    0    0 06:43:44 Estab   7      7

arista-02#show ipv6 bgp summary
BGP summary information for VRF default
Router identifier 192.0.2.203, local AS number 65103
Neighbor Status Codes: m - Under maintenance
  Neighbor         V  AS           MsgRcvd   MsgSent  InQ OutQ  Up/Down State   PfxRcd PfxAcc
  2001:db8:903:beef::1 4  65103            410       407    0    0 06:43:45 Estab   5      5

! Show BGP routes
BGP routing table information for VRF default
Router identifier 192.0.2.203, local AS number 65103
Route status codes: s - suppressed, * - valid, &gt; - active, # - not installed, E - ECMP head, e - ECMP
                    S - Stale, c - Contributing to ECMP, b - backup, L - labeled-unicast
Origin codes: i - IGP, e - EGP, ? - incomplete
AS Path Attributes: Or-ID - Originator ID, C-LST - Cluster List, LL Nexthop - Link Local Nexthop

         Network                Next Hop            Metric  LocPref Weight  Path
 * &gt;     0.0.0.0/0              192.0.2.103           0       100     0       ?
 * #     10.15.30.0/24          192.0.2.103           0       100     0       i
 * #     10.100.103.0/24        192.0.2.103           0       100     0       i
 * #     10.100.203.0/24        192.0.2.103           0       100     0       i
 * &gt;     192.0.2.1/32           10.100.103.254        0       100     0       65430 i
 * #     192.0.2.103/32         192.0.2.103           0       100     0       i
 * &gt;     192.168.122.0/24       192.0.2.103           0       100     0       i

arista-02#show ipv6 bgp
BGP routing table information for VRF default
Router identifier 192.0.2.203, local AS number 65103
Route status codes: s - suppressed, * - valid, &gt; - active, # - not installed, E - ECMP head, e - ECMP
                    S - Stale, c - Contributing to ECMP, b - backup, L - labeled-unicast
Origin codes: i - IGP, e - EGP, ? - incomplete
AS Path Attributes: Or-ID - Originator ID, C-LST - Cluster List, LL Nexthop - Link Local Nexthop

         Network                Next Hop            Metric  LocPref Weight  Path
 * #     2001:db8:103::/64      2001:db8:903:beef::1  0       100     0       i
 * #     2001:db8:203::/64      2001:db8:903:beef::1  0       100     0       i
 * #     2001:db8:903:beef::1/128 2001:db8:903:beef::1  0       100     0       i
 * #     2001:db8:903:beef::2/128 2001:db8:903:beef::1  0       100     0       i
 * &gt;     2001:db8:999:beef::1/128 2001:db8:103::ffff    0       100     0       65430 i

! Ping the netsvr Loopback (192.0.2.1 and 2001:db8:999:beef::1)
arista-02#ping 192.0.2.1
PING 192.0.2.1 (192.0.2.1) 72(100) bytes of data.
80 bytes from 192.0.2.1: icmp_seq=1 ttl=63 time=4.88 ms
80 bytes from 192.0.2.1: icmp_seq=2 ttl=63 time=4.38 ms
80 bytes from 192.0.2.1: icmp_seq=3 ttl=63 time=4.62 ms
80 bytes from 192.0.2.1: icmp_seq=4 ttl=63 time=4.52 ms
80 bytes from 192.0.2.1: icmp_seq=5 ttl=63 time=5.12 ms

--- 192.0.2.1 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 20ms
rtt min/avg/max/mdev = 4.383/4.707/5.120/0.262 ms, ipg/ewma 5.142/4.806 ms

arista-02#ping 192.0.2.1 source lo0
PING 192.0.2.1 (192.0.2.1) from 192.0.2.203 : 72(100) bytes of data.
80 bytes from 192.0.2.1: icmp_seq=1 ttl=63 time=4.51 ms
80 bytes from 192.0.2.1: icmp_seq=2 ttl=63 time=3.86 ms
80 bytes from 192.0.2.1: icmp_seq=3 ttl=63 time=4.29 ms
80 bytes from 192.0.2.1: icmp_seq=4 ttl=63 time=4.60 ms
80 bytes from 192.0.2.1: icmp_seq=5 ttl=63 time=4.61 ms

--- 192.0.2.1 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 18ms
rtt min/avg/max/mdev = 3.866/4.378/4.613/0.287 ms, ipg/ewma 4.706/4.460 ms

arista-02#ping 2001:db8:999:beef::1 source lo0
PING 2001:db8:999:beef::1(2001:db8:999:beef::1) from 2001:db8:903:beef::2 : 72 data bytes
80 bytes from 2001:db8:999:beef::1: icmp_seq=1 ttl=63 time=4.47 ms
80 bytes from 2001:db8:999:beef::1: icmp_seq=2 ttl=63 time=3.99 ms
80 bytes from 2001:db8:999:beef::1: icmp_seq=3 ttl=63 time=4.11 ms
80 bytes from 2001:db8:999:beef::1: icmp_seq=4 ttl=63 time=4.53 ms
80 bytes from 2001:db8:999:beef::1: icmp_seq=5 ttl=63 time=5.06 ms

--- 2001:db8:999:beef::1 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 18ms
rtt min/avg/max/mdev = 3.999/4.436/5.063/0.382 ms, ipg/ewma 4.644/4.479 ms
</code></pre></div><p>All looking good! Because of including connected routes, we do not have the same reachability issues we encountered in the Cisco post.</p>
<p>Ideally we should be filtering what connected routes are redistributed too (like in <a href="/posts/ansible-for-networking-part-4-juniper-junos">Part 4 - Juniper JunOS</a>), but we are just redistributing all the routes for now.</p>
<h3 id="nat">NAT</h3>
<p>In the Cisco post, I covered doing NAT. However as mentioned the vEOS images do not support NAT (due to them emulating switches rather than routers). WIth this being the case, I have omitted this role, as I have no way of verifying that the commands will even apply. I hope to one day have access to a routed-based Arista image (or an Arista router) to test the role out.</p>
<h3 id="snmp">SNMP</h3>
<p>SNMP is enabled so that we would be able to add the vEOS images into a monitoring system.</p>
<h4 id="playbook-3">Playbook</h4>
<p>The contents of the playbook are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for snmp</span>
- <span style="color:#66d9ef">name</span>: Enable SNMPv3
  <span style="color:#66d9ef">eos_config</span>:
    <span style="color:#66d9ef">src</span>: snmpv3.j2
  <span style="color:#66d9ef">tags</span>:
    - snmp
</code></pre></div><p>This is identical to the IOS playbook, except we use <code>eos_config</code> rather than <code>ios_config</code>. The template itself is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-jinja" data-lang="jinja">snmp-server location <span style="color:#75715e">{{</span> snmp<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;location&#39;</span><span style="color:#f92672">]</span> <span style="color:#75715e">}}</span>
snmp-server contact <span style="color:#75715e">{{</span> snmp<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;contact&#39;</span><span style="color:#f92672">]</span> <span style="color:#75715e">}}</span>
snmp-server group <span style="color:#75715e">{{</span> snmp<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;group&#39;</span><span style="color:#f92672">]</span> <span style="color:#75715e">}}</span> v3 priv
snmp-server user <span style="color:#75715e">{{</span> snmp<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;user&#39;</span><span style="color:#f92672">]</span> <span style="color:#75715e">}}</span> <span style="color:#75715e">{{</span> snmp<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;group&#39;</span><span style="color:#f92672">]</span> <span style="color:#75715e">}}</span> v3 auth sha <span style="color:#75715e">{{</span> snmp<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;auth_key&#39;</span><span style="color:#f92672">]</span> <span style="color:#75715e">}}</span> priv aes <span style="color:#75715e">{{</span> snmp<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;priv_key&#39;</span><span style="color:#f92672">]</span> <span style="color:#75715e">}}</span>
</code></pre></div><p>The only difference between this template and the Cisco template is that in the Arista template, we specify <code>priv aes</code> for AES128-based encryption, whereas in Cisco you specify <code>priv aes 128</code>. Everything else is identical.</p>
<p>Our <code>group_vars</code> relevant to this are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">snmp</span>:
  <span style="color:#66d9ef">location</span>: Yeti Home
  <span style="color:#66d9ef">contact</span>: The Hairy One
  <span style="color:#66d9ef">user</span>: yetiops
  <span style="color:#66d9ef">group</span>: yetiops_group
  <span style="color:#66d9ef">auth_key</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">            $ANSIBLE_VAULT;1.1;AES256</span>
            <span style="color:#ae81ff">383</span><span style="color:#75715e">###REDACTED###############################################################566</span>
            <span style="color:#ae81ff">363</span><span style="color:#75715e">###REDACTED###############################################################366</span>
            <span style="color:#ae81ff">343</span><span style="color:#75715e">###REDACTED###############################################################565</span>
            <span style="color:#ae81ff">326</span><span style="color:#75715e">###REDACTED###############################################################362</span>
            <span style="color:#ae81ff">3431</span>
  <span style="color:#66d9ef">priv_key</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">            $ANSIBLE_VAULT;1.1;AES256</span>
            <span style="color:#ae81ff">386</span><span style="color:#75715e">###REDACTED###############################################################764</span>
            <span style="color:#ae81ff">613</span><span style="color:#75715e">###REDACTED###############################################################630</span>
            <span style="color:#ae81ff">646</span><span style="color:#75715e">###REDACTED###############################################################331</span>
            <span style="color:#ae81ff">376</span><span style="color:#75715e">###REDACTED###############################################################137</span>
            <span style="color:#ae81ff">3563</span>
</code></pre></div><p>These are the same as for our Cisco playbooks, and again we use Ansible Vault to encrypt the authentication and privacy keys.</p>
<h4 id="generated-configuration-4">Generated configuration</h4>
<p>The generated configuration looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">snmp-server contact The Hairy One
snmp-server location Yeti Home
snmp-server group yetiops_group v3 priv
snmp-server user yetiops yetiops_group v3 localized f5717f525400c5df3c00 auth sha ###AUTH-KEY### priv aes ###PRIV-KEY### 
</code></pre></div><p>The <code>localized</code> command is based upon the <strong>engineID</strong> that is generated by the image itself. For this image, this is <code>snmp-server engineID local f5717f525400c5df3c00</code>.</p>
<h4 id="verification-5">Verification</h4>
<p>To check this, we&rsquo;ll check using <code>snmpwalk</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">$ snmpwalk -v3 -u yetiops -a SHA -A yeti_hash123 -x AES -X yeti_key123 -l authPriv 10.15.30.43
iso.3.6.1.2.1.1.1.0 = STRING: &#34;Linux arista-01 4.9.122.Ar-14427155.4231F #1 SMP PREEMPT Wed Nov 27 21:50:11 PST 2019 x86_64&#34;
iso.3.6.1.2.1.1.2.0 = OID: iso.3.6.1.4.1.30065.1.2759
iso.3.6.1.2.1.1.3.0 = Timeticks: (2573396) 7:08:53.96
iso.3.6.1.2.1.1.4.0 = STRING: &#34;The Hairy One&#34;
iso.3.6.1.2.1.1.5.0 = STRING: &#34;arista-01&#34;
iso.3.6.1.2.1.1.6.0 = STRING: &#34;Yeti Home&#34;
iso.3.6.1.2.1.1.7.0 = INTEGER: 14
[...]

$ snmpwalk -v3 -u yetiops -a SHA -A yeti_hash123 -x AES -X yeti_key123 -l authPriv 10.15.30.44 | head -n 7
iso.3.6.1.2.1.1.1.0 = STRING: &#34;Linux arista-02 4.9.122.Ar-14427155.4231F #1 SMP PREEMPT Wed Nov 27 21:50:11 PST 2019 x86_64&#34;
iso.3.6.1.2.1.1.2.0 = OID: iso.3.6.1.4.1.30065.1.2759
iso.3.6.1.2.1.1.3.0 = Timeticks: (2579360) 7:09:53.60
iso.3.6.1.2.1.1.4.0 = STRING: &#34;The Hairy One&#34;
iso.3.6.1.2.1.1.5.0 = STRING: &#34;arista-02&#34;
iso.3.6.1.2.1.1.6.0 = STRING: &#34;Yeti Home&#34;
iso.3.6.1.2.1.1.7.0 = INTEGER: 14
[...]
</code></pre></div><p>All looking good!</p>
<h3 id="aaa">AAA</h3>
<p>We use AAA (via the <code>tac_plus</code> instance on our <strong>netsvr-01</strong>) to allow central management of user login, command authorization, and accounting of all commands ran on the devices.</p>
<h4 id="playbook-4">Playbook</h4>
<p>The contents of the playbook are as follows: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for aaa</span>
- <span style="color:#66d9ef">name</span>: Enable TACACS<span style="color:#e6db74">+
</span><span style="color:#e6db74">  eos_config:</span>
    <span style="color:#66d9ef">src</span>: tacacs.j2
  <span style="color:#66d9ef">tags</span>:
  - aaa
</code></pre></div><p>The only difference here to the IOS task is that we use <code>eos_config</code> rather than <code>ios_config</code>. The template itself looks like the below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">aaa authentication enable default group tacacs+ local
aaa authorization exec default group tacacs+ none
aaa authorization commands 0 default group tacacs+ none
aaa authorization commands 1 default group tacacs+ none
aaa authorization commands 15 default group tacacs+ none
aaa accounting exec default start-stop group tacacs+
aaa accounting commands 0 default start-stop group tacacs+
aaa accounting commands 1 default start-stop group tacacs+
aaa accounting commands 15 default start-stop group tacacs+
tacacs-server host {{ tacacs['ipv4'] }} key 0 {{ tacacs['secret'] }} timeout 1
ip tacacs source-interface Loopback0
</code></pre><p>There are a few differences here from the Cisco template, which are: -</p>
<ul>
<li>We don&rsquo;t specify <code>aaa new-model</code>, as the default <strong>model</strong> for AAA on Arista is equivalent to Cisco&rsquo;s &ldquo;new&rdquo; <strong>model</strong></li>
<li><code>enable default group tacacs+ local</code>, as IOS uses the word <code>enable</code> instead of <code>local</code> as the last part of the command</li>
<li>We source TACACS+ from our Loopback interface, to ensure that it is not tied to physical interface</li>
<li>We do not need to specify <code>line vty</code> and <code>line console</code> commands to enable TACACS+ for remote and console users</li>
<li>The <code>tacacs server</code> syntax in IOS does not work on EOS, so we use the &ldquo;older&rdquo; style syntax</li>
</ul>
<p>The reason I say &ldquo;older&rdquo; syntax for specifying <code>tacacs-server host</code>'s is that if you apply this command on any recent Cisco router, it would give a warning that the syntax style will be deprecated soon, to be replaced by: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt"> tacacs server NETSVR
  address ipv4 $IP-ADDRESS 
  key 0 $SECRET 
  timeout 1
</code></pre></div><p>Despite all of this, the configuration being applied is still very similar to Cisco. This is another case where if you are used to Cisco IOS, you are going to be immediately familiar with most of an Arista EOS configuration.</p>
<p>The <code>group_vars</code> that are relevant to this are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">tacacs</span>:
  <span style="color:#66d9ef">ipv4</span>: <span style="color:#ae81ff">192.0.2.1</span>
  <span style="color:#66d9ef">secret</span>: supersecret
</code></pre></div><h4 id="generated-configuration-5">Generated configuration</h4>
<p>The configuration that is generated looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">aaa authentication enable default group tacacs+ local
aaa authorization exec default group tacacs+ none
aaa authorization commands 0 default group tacacs+ none
aaa authorization commands 1 default group tacacs+ none
aaa authorization commands 15 default group tacacs+ none
aaa accounting exec default start-stop group tacacs+
aaa accounting commands 0 default start-stop group tacacs+
aaa accounting commands 1 default start-stop group tacacs+
aaa accounting commands 15 default start-stop group tacacs+
tacacs-server host 192.0.2.1 key 0 supersecret timeout 1 
ip tacacs source-interface Loopback0
</code></pre></div><p>After it has been applied, the running configuration will look like: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">aaa authentication login default group tacacs+ local
aaa authentication enable default group tacacs+ local
aaa authorization exec default group tacacs+ none
aaa authorization commands 0-1,15 default group tacacs+ none
aaa accounting exec default start-stop group tacacs+
aaa accounting commands 0-1,15 default start-stop group tacacs+
!
tacacs-server host 192.0.2.1 key 7 095F5B191C170417081E013E6B30213E302D06135652
!
ip tacacs source-interface Loopback0
</code></pre></div><p>In the above, the separate commands for <code>0</code>, <code>1</code> and <code>15</code> (the privilege levels) are grouped together as one command. Also the TACACS+ secret key is encrypted.</p>
<h4 id="verification-6">Verification</h4>
<pre><code>! Can we login with our yetiops user, and be placed into enable mode instantly?
$ ssh yetiops@10.15.30.43
----------------------------------------
|
| This banner was generated by Ansible
|
----------------------------------------
|
| You are logged into arista-01
|
----------------------------------------
Password:
arista-01#

! What about a user that doesn't exist?
$ ssh bob@10.15.30.43
----------------------------------------
|
| This banner was generated by Ansible
|
----------------------------------------
|
| You are logged into arista-01
|
----------------------------------------
Password:
Password:
Password:
bob@10.15.30.43: Permission denied (publickey,keyboard-interactive).

! What do we see in our accounting log?
Apr 18 20:22:12	192.0.2.103	ansible	vty3	10.15.30.1	stop	task_id=1	service=shell	start_time=1587237505	timezone=UTC	elapsed_time=227
Apr 18 20:22:18	192.0.2.103	yetiops	ssh	10.15.30.1	start	task_id=5	service=shell	start_time=1587237737	timezone=UTC
Apr 18 20:22:37	192.0.2.103	yetiops	vty3	10.15.30.1	stop	task_id=6	service=shell	priv-lvl=15	start_time=1587237756	timezone=UTC	cmd=exit &lt;cr&gt;
Apr 18 20:22:37	192.0.2.103	yetiops	vty3	10.15.30.1	stop	task_id=5	service=shell	start_time=1587237737	timezone=UTC	elapsed_time=20

! What about if the TACACS+ server goes away?
$ sudo systemctl stop tac_plus
$ sudo systemctl status tac_plus
● tac_plus.service - LSB: TACACS+ server based on Cisco source release
   Loaded: loaded (/etc/rc.d/init.d/tac_plus; generated)
   Active: inactive (dead) since Sat 2020-04-18 20:24:08 BST; 11s ago
     Docs: man:systemd-sysv-generator(8)
  Process: 7549 ExecStop=/etc/rc.d/init.d/tac_plus stop (code=exited, status=0/SUCCESS)
  Process: 7388 ExecStart=/etc/rc.d/init.d/tac_plus start (code=exited, status=0/SUCCESS)

Apr 18 20:22:50 netsvr-01 tac_plus[7522]: login failure: bob 192.0.2.103 (192.0.2.103) ssh
Apr 18 20:23:33 netsvr-01 tac_plus[7524]: connect from 192.0.2.103 [192.0.2.103]
Apr 18 20:23:39 netsvr-01 tac_plus[7528]: connect from 192.0.2.103 [192.0.2.103]
Apr 18 20:23:39 netsvr-01 tac_plus[7529]: connect from 192.0.2.103 [192.0.2.103]
Apr 18 20:23:40 netsvr-01 tac_plus[7530]: connect from 192.0.2.103 [192.0.2.103]
Apr 18 20:23:44 netsvr-01 tac_plus[7531]: connect from 192.0.2.103 [192.0.2.103]
Apr 18 20:23:48 netsvr-01 tac_plus[7533]: connect from 192.0.2.103 [192.0.2.103]
Apr 18 20:24:08 netsvr-01 systemd[1]: Stopping LSB: TACACS+ server based on Cisco source release...
Apr 18 20:24:08 netsvr-01 tac_plus[7549]: Shutting down tacacs+: [  OK  ]
Apr 18 20:24:08 netsvr-01 systemd[1]: Stopped LSB: TACACS+ server based on Cisco source release.

$ ssh ansible@10.15.30.43
----------------------------------------
|
| This banner was generated by Ansible
|
----------------------------------------
|
| You are logged into arista-01
|
----------------------------------------
Password:
arista-01&gt;en
Password:
arista-01#
</code></pre><p>All looking good!</p>
<h3 id="parent-playbook">Parent playbook</h3>
<p>The parent playbook (i.e. the playbook that brings all the roles together) is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
- <span style="color:#66d9ef">hosts</span>: arista
  <span style="color:#66d9ef">gather_facts</span>: no
  <span style="color:#66d9ef">tasks</span>:
  - <span style="color:#66d9ef">import_role</span>:
      <span style="color:#66d9ef">name</span>: system
  - <span style="color:#66d9ef">import_role</span>:
      <span style="color:#66d9ef">name</span>: interfaces
  - <span style="color:#66d9ef">import_role</span>:
      <span style="color:#66d9ef">name</span>: acl
  - <span style="color:#66d9ef">import_role</span>:
      <span style="color:#66d9ef">name</span>: routing
  - <span style="color:#66d9ef">import_role</span>:
      <span style="color:#66d9ef">name</span>: snmp
      <span style="color:#75715e">#  - import_role:</span>
      <span style="color:#75715e">#      name: nat</span>
  - <span style="color:#66d9ef">import_role</span>:
      <span style="color:#66d9ef">name</span>: aaa
  - <span style="color:#66d9ef">name</span>: save running to startup when modified
    <span style="color:#66d9ef">eos_config</span>:
      <span style="color:#66d9ef">save_when</span>: modified
</code></pre></div><p>Again, this is the same as the IOS playbook. The only exception is that we have commented out the NAT role until it can be tested.</p>
<h4 id="role-order">Role Order</h4>
<p>The role order is similar to the IOS playbook, except the SNMP task is applied later in the playbook. As SNMP has no dependencies, we can place it anywhere.</p>
<p>To reiterate the reasons for the order of the roles: -</p>
<ul>
<li><code>system</code> - Basic setup, and enabling logging</li>
<li><code>interfaces</code> - A device cannot route traffic without IPs!</li>
<li><code>acl</code> - Ensures that we do not allow nefarious traffic when external routing is enabled</li>
<li><code>routing</code> - Required for NAT and AAA</li>
<li><code>snmp</code> - As mentioned, this can go anywhere in the playbook</li>
<li><code>nat</code> - If we could enable NAT, traffic from the <strong>internal</strong> router would not reach the <strong>NAT</strong> interfaces until routing is established</li>
<li><code>aaa</code> - Ensures that if TACACS+ is not functioning correctly, the device is not in a half configured (and potentially broken) state</li>
</ul>
<h2 id="artifacts">Artifacts</h2>
<p>The final directory structure looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">$ tree -L 2
.
├── ansible.cfg
├── ansible.log
├── arista.yaml
├── group_vars
│   └── arista
├── host_vars
│   ├── arista-01.yml
│   └── arista-02.yml
├── inventory
└── roles
    ├── aaa
    ├── acl
    ├── interfaces
    ├── lldp
    ├── nat
    ├── routing
    ├── snmp
    └── system

11 directories, 7 files
</code></pre></div><p>The final contents of our <code>group_vars</code> are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">ansible_user</span>: ansible
<span style="color:#66d9ef">ansible_connection</span>: network_cli
<span style="color:#66d9ef">ansible_network_os</span>: eos
<span style="color:#66d9ef">ansible_ssh_pass</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">                  $ANSIBLE_VAULT;1.1;AES256</span>
                  <span style="color:#ae81ff">383</span><span style="color:#75715e">###REDACTED###############################################################566</span>
                  <span style="color:#ae81ff">363</span><span style="color:#75715e">###REDACTED###############################################################366</span>
                  <span style="color:#ae81ff">343</span><span style="color:#75715e">###REDACTED###############################################################565</span>
                  <span style="color:#ae81ff">326</span><span style="color:#75715e">###REDACTED###############################################################362</span>
                  3431Ta
<span style="color:#66d9ef">ansible_become</span>: yes
<span style="color:#66d9ef">ansible_become_method</span>: enable
<span style="color:#66d9ef">ansible_become_password</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">                  $ANSIBLE_VAULT;1.1;AES256</span>
                  <span style="color:#ae81ff">383</span><span style="color:#75715e">###REDACTED###############################################################566</span>
                  <span style="color:#ae81ff">363</span><span style="color:#75715e">###REDACTED###############################################################366</span>
                  <span style="color:#ae81ff">343</span><span style="color:#75715e">###REDACTED###############################################################565</span>
                  <span style="color:#ae81ff">326</span><span style="color:#75715e">###REDACTED###############################################################362</span>
                  3431Ta
<span style="color:#66d9ef">log_host</span>: <span style="color:#ae81ff">10.100.101.254</span>
<span style="color:#66d9ef">tacacs</span>:
  <span style="color:#66d9ef">ipv4</span>: <span style="color:#ae81ff">192.0.2.1</span>
  <span style="color:#66d9ef">secret</span>: supersecret
<span style="color:#66d9ef">snmp</span>:
  <span style="color:#66d9ef">location</span>: Yeti Home
  <span style="color:#66d9ef">contact</span>: The Hairy One
  <span style="color:#66d9ef">user</span>: yetiops
  <span style="color:#66d9ef">group</span>: yetiops_group
  <span style="color:#66d9ef">auth_key</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">            $ANSIBLE_VAULT;1.1;AES256</span>
            <span style="color:#ae81ff">383</span><span style="color:#75715e">###REDACTED###############################################################566</span>
            <span style="color:#ae81ff">363</span><span style="color:#75715e">###REDACTED###############################################################366</span>
            <span style="color:#ae81ff">343</span><span style="color:#75715e">###REDACTED###############################################################565</span>
            <span style="color:#ae81ff">326</span><span style="color:#75715e">###REDACTED###############################################################362</span>
            <span style="color:#ae81ff">3431</span>
  <span style="color:#66d9ef">priv_key</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">            $ANSIBLE_VAULT;1.1;AES256</span>
            <span style="color:#ae81ff">386</span><span style="color:#75715e">###REDACTED###############################################################764</span>
            <span style="color:#ae81ff">613</span><span style="color:#75715e">###REDACTED###############################################################630</span>
            <span style="color:#ae81ff">646</span><span style="color:#75715e">###REDACTED###############################################################331</span>
            <span style="color:#ae81ff">376</span><span style="color:#75715e">###REDACTED###############################################################137</span>
            <span style="color:#ae81ff">3563</span>
</code></pre></div><p>The final contents of our <code>host_vars</code> are: -</p>
<p><strong>arista-01.yaml</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">outer_id</span>: <span style="color:#ae81ff">192.0.2.103</span>
<span style="color:#66d9ef">rtr_role</span>: edge
<span style="color:#66d9ef">bgp</span>:
  <span style="color:#66d9ef">local_as</span>: <span style="color:#ae81ff">65103</span>
  <span style="color:#66d9ef">redist</span>:
    <span style="color:#66d9ef">ospf</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">neighbors</span>:
    <span style="color:#66d9ef">ipv4</span>:
     - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">10.100.103.254</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65430</span>
       <span style="color:#66d9ef">ebgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">acl</span>:
         <span style="color:#66d9ef">acl_index</span>: <span style="color:#ae81ff">110</span>
     - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">192.0.2.203</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65103</span>
       <span style="color:#66d9ef">ibgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">update_source</span>: Loopback0
       <span style="color:#66d9ef">default_originate</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv6</span>:
     - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:103::ffff&#34;</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65430</span>
       <span style="color:#66d9ef">ebgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">acl</span>:
         <span style="color:#66d9ef">acl_index</span>: <span style="color:#ae81ff">110</span>
     - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:903:beef::2&#34;</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65103</span>
       <span style="color:#66d9ef">ibgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">update_source</span>: Loopback0
<span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">eos_if</span>: <span style="color:#e6db74">&#34;Management1&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Management&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.15.30.43/24&#34;</span>
  - <span style="color:#66d9ef">eos_if</span>: <span style="color:#e6db74">&#34;Ethernet1&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To netsvr&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">routed</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.100.103.253/24&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:103::f/64&#34;</span>
    <span style="color:#66d9ef">acl</span>:
      <span style="color:#66d9ef">ipv4</span>:
        <span style="color:#66d9ef">in</span>: EDGE-IN
        <span style="color:#66d9ef">out</span>: EDGE-OUT
      <span style="color:#66d9ef">ipv6</span>:
        <span style="color:#66d9ef">in</span>: EDGEv6-IN
        <span style="color:#66d9ef">out</span>: EDGEv6-OUT
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
  - <span style="color:#66d9ef">eos_if</span>: <span style="color:#e6db74">&#34;Ethernet2&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To arista-02&#34;</span>
    <span style="color:#66d9ef">routed</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.100.203.254/24&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:203::a/64&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
  - <span style="color:#66d9ef">eos_if</span>: <span style="color:#e6db74">&#34;Ethernet3&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To the Internet&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">routed</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;dhcp&#34;</span>
  - <span style="color:#66d9ef">eos_if</span>: <span style="color:#e6db74">&#34;Loopback0&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;192.0.2.103/32&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:903:beef::1/128&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
</code></pre></div><p><strong>arista-02-yaml</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">router_id</span>: <span style="color:#ae81ff">192.0.2.203</span>
<span style="color:#66d9ef">rtr_role</span>: internal
<span style="color:#66d9ef">bgp</span>:
  <span style="color:#66d9ef">local_as</span>: <span style="color:#ae81ff">65103</span>
  <span style="color:#66d9ef">neighbors</span>:
    <span style="color:#66d9ef">ipv4</span>:
     - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">192.0.2.103</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65103</span>
       <span style="color:#66d9ef">ibgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">update_source</span>: Loopback0
    <span style="color:#66d9ef">ipv6</span>:
     - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:903:beef::1&#34;</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65103</span>
       <span style="color:#66d9ef">ibgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">update_source</span>: Loopback0
<span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">eos_if</span>: <span style="color:#e6db74">&#34;Management1&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Management&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
  - <span style="color:#66d9ef">eos_if</span>: <span style="color:#e6db74">&#34;Ethernet1&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To arista-01&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">routed</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.100.203.253/24&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:203::f/64&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
  - <span style="color:#66d9ef">eos_if</span>: <span style="color:#e6db74">&#34;Loopback0&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;192.0.2.203/32&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:903:beef::2/128&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
</code></pre></div><h2 id="running-the-playbooks">Running the playbooks</h2>
<p>Below is an Asciinema output of my terminal when running the playbooks, so you can see them being applied: -</p>
<script id="asciicast-321495" src="https://asciinema.org/a/321495.js" async></script>
<p>Compared to the IOS playbooks, only a couple of tasks were marked as changed, despite applying the same configuration.</p>
<p>When looking through the TACACS+ accounting logs, you can see the command <code>show session-config diffs</code>, which is a method similar to JunOS&rsquo;s <code>show | compare</code>. Arista allows you to enter a &ldquo;session&rdquo; for configuration and compare candidate configuration to the running configuration before committing. This isn&rsquo;t the case with the Cisco IOS tasks. Tasks are marked as changed based upon the difference between the commands being applied, and the resulting output in the running configuration.</p>
<h2 id="native-modules-versus-eos_config">Native modules versus <code>eos_config</code></h2>
<p>Below is a summary of how many different modules are used, and also how many in total were native modules (compared to using <code>eos_config</code>).</p>
<table>
<thead>
<tr>
<th>Module</th>
<th>Used</th>
</tr>
</thead>
<tbody>
<tr>
<td>eos_config</td>
<td>31</td>
</tr>
<tr>
<td>eos_bgp</td>
<td>6</td>
</tr>
<tr>
<td>eos_logging</td>
<td>2</td>
</tr>
<tr>
<td>eos_l3_interfaces</td>
<td>2</td>
</tr>
<tr>
<td>eos_banner</td>
<td>2</td>
</tr>
<tr>
<td>eos_system</td>
<td>1</td>
</tr>
<tr>
<td>eos_interfaces</td>
<td>1</td>
</tr>
</tbody>
</table>
<p>As with the IOS playbooks, we are using the <code>eos_config</code> module significantly more than the others. The reason we use the <code>eos_config</code> module more than in the Cisco module is because we are setting a log source for <code>syslog</code>, and because we need to enable IPv4 routing (due to emulating a switch rather than a router).</p>
<h2 id="thoughts-compared-to-ios-and-junos">Thoughts compared to IOS and JunOS</h2>
<p>If we compare the Arista playbooks to the JunOS playbooks, there are a number of differences. This is to be expected, as the configuration style of JunOS is very different from Arista EOS (or Cisco IOS). Until either common APIs or data modelling languages like <a href="https://en.wikipedia.org/wiki/YANG">YANG</a> become commonplace, this will probably always be the case.</p>
<p>However if we compare them to IOS, there are so many similarities, we could almost get away with making no changes except module names.</p>
<p>To build the playbooks for this module, rather than rewriting the tasks, I did a mass <strong>find and replace</strong> for the word <code>ios</code> and replaced it with <code>eos</code>. The majority of the tasks applied cleanly, with only a few failing. Within a couple of hours of comparison, reading Arista documentation and checking against the Arista command line, I had a finished set of tasks and playbooks.</p>
<p>In the first post in this series, I said the following: -</p>
<blockquote>
<p>I also believe that showing it across multiple vendors (and how similar some aspects are) will make it easier for people to move from one vendor to another, without worrying about needing to relearn syntax or tools for managing their networking estate.</p>
</blockquote>
<p>The differences between IOS and EOS (in CLI syntax and in Ansible module usage) are a perfect demonstration of this. If you were to decide to move from an IOS platform to an EOS platform, migrating your Ansible playbooks would take very little effort.</p>
<p>In contrast, the differences between the IOS and JunOS playbooks are significant. A few days or weeks familiarising yourself with JunOS syntax would probably be enough to move from one vendor to another, but it is not quite as straightforward as the move from IOS to EOS. This is not to say that JunOS (or other vendors) are impossible to migrate between. It just means that if you do rely on Ansible, you&rsquo;ll need to account for the extra time in refactoring your roles and playbooks.</p>
<h2 id="summary">Summary</h2>
<p>While the previous two posts in the series were more about using Ansible with either Cisco or Juniper (and how each task/playbook works), this post has mainly covered how to  the differences between IOS playbooks and EOS playbooks.</p>
<p>However this is not a criticism of Arista. It is quite easy to see how Arista has made the gains they have in the networking industry. With Cisco being such a massive part of the networking industry, using similar (and in some cases identical) syntax allows comparisons and purchasing decisions to be made entirely on price and features. I have seen first hand companies unwilling to change their networking vendor purely out of familiarity.</p>
<p>The final configs from the switches are in my <a href="https://gitlab.com/stuh84/network-automation-ansible/-/tree/master/arista/configs">Network Automation with Ansible repository</a>. The next part of this series will be configuring MikroTik devices, running RouterOS.</p>
]]></content>
        </item>
        
        <item>
            <title>Ansible for Networking - Part 4: Juniper JunOS</title>
            <link>https://yetiops.net/posts/ansible-for-networking-part-4-juniper-junos/</link>
            <pubDate>Sat, 04 Apr 2020 23:30:53 +0000</pubDate>
            
            <guid>https://yetiops.net/posts/ansible-for-networking-part-4-juniper-junos/</guid>
            <description>The fourth part of my ongoing series of posts on Ansible for Networking will cover Juniper&amp;rsquo;s JunOS. You can view the other posts in the series below: -
 Part 1 - Start of the series Part 2 - The Lab Environment Part 3 - Cisco IOS Part 5 - Arista EOS Part 6 - MikroTik RouterOS Part 7 - VyOS  All the playbooks, roles and variables used in this article are available in my Network Automation with Ansible repository</description>
            <content type="html"><![CDATA[<p>The fourth part of my ongoing series of posts on Ansible for Networking will cover Juniper&rsquo;s JunOS. You can view the other posts in the series below: -</p>
<ul>
<li><a href="/posts/ansible-for-networking-series">Part 1 - Start of the series</a></li>
<li><a href="/posts/ansible-for-networking-part-2-lab">Part 2 - The Lab Environment</a></li>
<li><a href="/posts/ansible-for-networking-part-3-cisco-ios">Part 3 - Cisco IOS</a></li>
<li><a href="/posts/ansible-for-networking-part-5-arista-eos">Part 5 - Arista EOS</a></li>
<li><a href="/posts/ansible-for-networking-part-6-mikrotik-routeros">Part 6 - MikroTik RouterOS</a></li>
<li><a href="/posts/ansible-for-networking-part-7-vyos">Part 7 - VyOS</a></li>
</ul>
<p>All the playbooks, roles and variables used in this article are available in my <a href="https://gitlab.com/stuh84/network-automation-ansible">Network Automation with Ansible repository</a></p>
<h2 id="why-junos">Why JunOS?</h2>
<p>Juniper are often placed in the top 3 or 4 networking vendors in terms of market share (along with Cisco, HPE/Aruba and Huawei). They are especially popular in the service provider sector.</p>
<p>My current workplace has an almost entirely Juniper-based network (save for a few Cisco management and access switches), so this article not only helps those using Juniper, but may also help with automating our network too.</p>
<p>Unlike Cisco, Juniper do not have different operating systems across their firewalls, routing and switching (data centre or access) portfolio. Instead, they all run JunOS. Some features are not enabled on some platforms, for example firewall policies on switches, or ethernet switching on routers. However most other features are common.</p>
<p>To configure BGP on Cisco devices, the syntax differs between IOS-XR and IOS-XE. Configuring OSPF is not the same on NX-OS and IOS-XR. However on JunOS, it is the same across all of their hardware.</p>
<p>JunOS has its roots in FreeBSD, although the configuration is done in a vendor-specific CLI rather than within the FreeBSD base. If you run <code>start shell</code> though, you will be taken into a FreeBSD shell. While it isn&rsquo;t a fully featured desktop operating system, it still has many commands you may be familiar with (e.g. <code>ifconfig</code>, <code>tcpdump</code>).</p>
<p>One of the biggest selling points of JunOS is the <strong>commit</strong>-style configuration. When you apply configuration, it is not activated immediately. Instead you can choose to either keep working on the <em>candidate</em> configuration, check it (using <code>commit check</code>), or commit it.</p>
<p>You also have the option of using <code>commit confirmed</code>. With this, all changes will be rolled backed (after a short period of time) if you do not confirm your commit (i.e. type <code>commit</code> again). You can specify the period of time, or just type <code>commit confirmed</code> on its own, which will roll back changes after 5 minutes.</p>
<p>In Cisco IOS, it does not have this feature. You either have to run your changes in such a way that you cannot lose access, a dedicated out-of-band solution (usually via a serial console), use <code>reload in x</code> (<code>x</code> being minutes, rebooting the router after that amount of time) on every change, or you have to hope a field engineer can visit site within your SLA.</p>
<p>Cisco appear to have taken note of this. In Cisco&rsquo;s IOS-XR (their operating system geared towards service providers), they have adopted the commit-based system of managing configuration rather than immediately applying changes.</p>
<h3 id="configuration-style">Configuration style</h3>
<p>The configuration style for JunOS differs from IOS. Rather than entering different hierarchical &ldquo;levels&rdquo; to apply changes, you can apply them all from one level.</p>
<p>For example, in IOS you need to type <code>interface Gi0/0/0</code> first before you can then run <code>ip address 192.168.0.1 255.255.255.0</code>. For JunOS, this would be <code>set interface ge-0/0/0 unit 0 family inet address 192.168.0.1/24</code>.</p>
<p>All interfaces have <strong>units</strong>, meaning that all configuration appears as if it is using a sub-interface. What this means in practice is that your Layer 3 configuration will always be part of a <strong>unit</strong> (usually <code>unit 0</code>).</p>
<p>You can choose to enter a level of hierarchy too, so that multiple commands at the same level can be applied: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ansible@junos-01# edit interfaces fxp0 

[edit interfaces fxp0]
ansible@junos-01# set unit 0 description Management

[edit interfaces fxp0]
ansible@junos-01# set unit 0 family inet address 10.15.30.33/24
</code></pre></div><p>You can view the configuration in multiple ways. There is a JSON-like syntax, as seen below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ansible@junos-01&gt; show configuration snmp 
v3 {
    usm {
        local-engine {
            user yetiops {
                authentication-sha {
                    authentication-key &#34;###REDACTED###&#34;; ## SECRET-DATA
                }
                privacy-aes128 {
                    privacy-key &#34;###REDACTED###&#34;; ## SECRET-DATA
                }
            }
        }
    }
[...]
</code></pre></div><p>Alternatively, you can choose to output this in true JSON format: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"><span style="color:#960050;background-color:#1e0010">ansible@junos</span><span style="color:#ae81ff">-01</span><span style="color:#960050;background-color:#1e0010">&gt;</span> <span style="color:#960050;background-color:#1e0010">show</span> <span style="color:#960050;background-color:#1e0010">configuration</span> <span style="color:#960050;background-color:#1e0010">snmp</span> <span style="color:#960050;background-color:#1e0010">|</span> <span style="color:#960050;background-color:#1e0010">display</span> <span style="color:#960050;background-color:#1e0010">json</span> 
{
    <span style="color:#f92672">&#34;configuration&#34;</span> : {
        <span style="color:#f92672">&#34;@&#34;</span> : {
            <span style="color:#f92672">&#34;junos:commit-seconds&#34;</span> : <span style="color:#e6db74">&#34;1584185530&#34;</span>, 
            <span style="color:#f92672">&#34;junos:commit-localtime&#34;</span> : <span style="color:#e6db74">&#34;2020-03-14 11:32:10 UTC&#34;</span>, 
            <span style="color:#f92672">&#34;junos:commit-user&#34;</span> : <span style="color:#e6db74">&#34;ansible&#34;</span>
        }, 
        <span style="color:#f92672">&#34;snmp&#34;</span> : {
            <span style="color:#f92672">&#34;v3&#34;</span> : {
                <span style="color:#f92672">&#34;usm&#34;</span> : {
                    <span style="color:#f92672">&#34;local-engine&#34;</span> : {
                        <span style="color:#f92672">&#34;user&#34;</span> : [
[<span style="color:#960050;background-color:#1e0010">...</span>]
</code></pre></div><p>Another option is to show the actual commands used for configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ansible@junos-01&gt; show configuration snmp | display set 
set snmp v3 usm local-engine user yetiops authentication-sha authentication-key &#34;###REDACTED###&#34;
set snmp v3 usm local-engine user yetiops privacy-aes128 privacy-key &#34;###REDACTED###&#34;
set snmp v3 vacm security-to-group security-model usm security-name yetiops group yetiops_group
set snmp v3 vacm access group yetiops_group default-context-prefix security-model any security-level authentication read-view all
set snmp v3 vacm access group yetiops_group default-context-prefix security-model any security-level authentication write-view all
set snmp view all oid .1
</code></pre></div><p>This is especially useful for when you want to plan changes, and need to essentially replicate sections with minor updates to them.</p>
<h2 id="objectives">Objectives</h2>
<p>For each vendor, I will be using Ansible to configure two routers/switches/firewalls/appliances.</p>
<p>One will serve as the <em>Edge</em> router, connecting to the Internet and also via BGP to the <strong>Net Server</strong>. The <strong>Net Server</strong> is a CentOS 8 Virtual Machine acting as a route server, syslog collector and TACACS+ server (detailed in <a href="/posts/ansible-for-networking-part-2-lab">The Lab Environment</a>)</p>
<p>The other will be an <em>internal</em> router, performing <strong>core</strong> functions (i.e. internal routing rather than external).</p>
<p>This lab is based upon the Juniper vSRX platform (virtualised SRX firewalls).</p>
<h3 id="edge-router">Edge router</h3>
<p>The edge router will run the following: -</p>
<ul>
<li><strong>External BGP</strong> (eBGP) to the <strong>Net Server</strong>
<ul>
<li>Advertising internal networks</li>
</ul>
</li>
<li><strong>Internal BGP</strong> (iBGP) to the <strong>Internal</strong> router
<ul>
<li>Advertising any routes received from the <strong>Net Server</strong></li>
<li>Advertising a default route (for internet access)</li>
</ul>
</li>
<li><strong>OSPF</strong>
<ul>
<li>Advertising loopbacks and internal networks between both routers</li>
</ul>
</li>
<li><strong>IPv4</strong> and <strong>IPv6</strong> routing
<ul>
<li>Using OSPFv3 (for <strong>IPv6</strong> support)</li>
<li>Using the IPv6 <em>Address Family</em> for BGP</li>
</ul>
</li>
<li><strong>SNMPv3</strong> for monitoring</li>
<li><strong>IPv4 NAT</strong> to allow internet access
<ul>
<li>I cannot run IPv6 for internet access, as my current ISP does not support IPv6</li>
</ul>
</li>
<li><strong>Logging</strong> via Syslog to the <strong>Net Server</strong></li>
<li><strong>Authentication, Authorization and Accounting</strong> (AAA) via TACACS+ to the <strong>Net Server</strong></li>
<li><strong>Zones</strong> to place interfaces in, for zone-based firewalling</li>
<li><strong>Firewall Rules</strong> to allow traffic to/from the <strong>Net Server</strong>, and between the two routers</li>
</ul>
<p>As we are using firewalls rather than routers, we are able to leverage a lot of firewall features (like zones and full firewall rules) compared to most router platforms.</p>
<h3 id="internal-router">Internal router</h3>
<p>The internal router runs a subset of the functions that the edge router does: -</p>
<ul>
<li><strong>Internal BGP</strong> (iBGP) to the <strong>Edge</strong> router
<ul>
<li>Receiving any routes received from the <strong>Net Server</strong></li>
<li>Receiving a default route (for internet access)</li>
</ul>
</li>
<li><strong>OSPF</strong>
<ul>
<li>Advertising loopbacks and internal networks between both routers</li>
</ul>
</li>
<li><strong>IPv4</strong> and <strong>IPv6</strong> routing
<ul>
<li>Using OSPFv3 (for <strong>IPv6</strong> support)</li>
<li>Using the IPv6 <em>Address Family</em> for BGP</li>
</ul>
</li>
<li><strong>SNMPv3</strong> for monitoring</li>
<li><strong>Logging</strong> via Syslog to the <strong>Net Server</strong></li>
<li><strong>Authentication, Authorization and Accounting</strong> (AAA) via TACACS+ to the <strong>Net Server</strong></li>
<li><strong>Zones</strong> to place interfaces in, for zone-based firewalling</li>
<li><strong>Firewall Rules</strong> to allow traffic to/from the <strong>Net Server</strong>, and between the two routers</li>
</ul>
<p>No firewalling or filtering was used on the internal router previously, but in this case we are going to use it to show some of the concepts of Juniper firewalling and services.</p>
<h2 id="netconf">Netconf</h2>
<p>Nearly all of the Juniper Ansible modules use <code>netconf</code> rather than the <code>network_cli</code> module. Netconf uses XML over SSH to interact with the destination host rather than using CLI commands over SSH, <code>network_cli</code> utilizing the latter.</p>
<p>This means that the data being returned to Ansible is in a structured format (e.g. error messages are in a consistent format and consistent structure). By contrast, The <code>network_cli</code> connection plugin is interpreting the error messages returned. This can result in issues with modules if the vendor changes the text in an error message for example.</p>
<p>When using modules, this makes very little difference to the tasks you&rsquo;ll create. Where it does make a difference is the error messages in response to any failed tasks or plays. You may need to use the <code>-vvvv</code> option with <code>ansible-playbook</code> to see the error messages, as they are much more verbose.</p>
<p>Again, Python is not running on the destination hosts themselves, so the host running Ansible is proxying the XML requests via itself.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>To be able to manage a JunOS device with Ansible, a few manual configuration steps are required, and also some changes to the default Ansible connection configuration is required.</p>
<h3 id="ansible-configuration">Ansible Configuration</h3>
<p>The following defaults are required to use Ansible with JunOS: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">ansible_user</span>: ansible
<span style="color:#66d9ef">ansible_connection</span>: netconf 
<span style="color:#66d9ef">ansible_network_os</span>: junos
<span style="color:#66d9ef">ansible_ssh_pass</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">            $ANSIBLE_VAULT;1.1;AES256</span>
            <span style="color:#ae81ff">383</span><span style="color:#75715e">###REDACTED###############################################################566</span>
            <span style="color:#ae81ff">363</span><span style="color:#75715e">###REDACTED###############################################################366</span>
            <span style="color:#ae81ff">343</span><span style="color:#75715e">###REDACTED###############################################################565</span>
            <span style="color:#ae81ff">326</span><span style="color:#75715e">###REDACTED###############################################################362</span>
            <span style="color:#ae81ff">3431</span>
</code></pre></div><p>There is no <strong>enable</strong> mode like in Cisco IOS. Privilege escalation is controlled at a user or group level, without the need for an additional mode to run certain commands. Therefore we do not need to supply any of the <code>ansible_become</code> statements as we do for IOS.</p>
<p>The <code>ansible_connection</code> plugin has changed to <code>netconf</code>. There are only a couple of JunOS modules which support <code>network_cli</code>, and one of them is used to configure <code>netconf</code> for you!</p>
<p>Also, Ansible relies on the Python module <code>ncclient</code>, so you&rsquo;ll need to install this either with your chosen operating systems package manager, or with <code>pip install ncclient</code></p>
<h4 id="caveats">Caveats</h4>
<p>The <code>ansible_ssh_pass</code> may not be required in your environment. JunOS does support public SSH keys (for both standard SSH connectivity and <code>netconf</code>).</p>
<p>I recently decided to move the network lab to a dedicated machine (a Dell Optiplex 3020, which I will detail in another post) running Ubuntu 18.04.4. This means I can now run the network labs without having a portable space heater on my lap!</p>
<p>However, despite running the same Ansible version and <code>ncclient</code> version as my Manjaro-based laptop, the Ubuntu machine could not connect via <code>netconf</code> using SSH keys. I suspect this is due to Manjaro using Python 3.8, compared to Ubuntu 18.04.4&rsquo;s Python 3.6. Ubuntu 20.04 is not far away as I write this post, so hopefully when I upgrade, this issue will be resolved.</p>
<h3 id="juniper-junos-configuration">Juniper JunOS Configuration</h3>
<p>To allow Ansible access to manage a JunOS device via <code>netconf</code> you need to create a user with <code>super-user</code> privileges and enable <code>netconf</code>. Also when you try to commit for the first time on a new JunOS device (or one with the default configuration), you&rsquo;ll be prompted for a password for the <code>root</code> account.</p>
<p>If you login from the console (<code>virsh console vsrx-01</code> in my case), you can login to the device with the username <code>root</code> and no password.</p>
<p>Below shows how to enable all of this: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">## The root account is placed into the FreeBSD shell by default
## Type cli to get into the JunOS shell
root@% cli
root@&gt; 

## Go into configuration mode
root@&gt; configure
Entering configuration mode

[edit]
root@#

## Add the user
root@# set system login user ansible authentication plain-text-password
New password:
Retype new password:

[edit]
root@# set system login user ansible class super-user

## Enable netconf
[edit]
root@# set system services netconf ssh port 830

## Configure a root password 
[edit]
root@# set system root-authentication plain-text-password

## Add an IP to the management interface
[edit]
root@# set interfaces fxp0 unit 0 family inet address 10.15.30.33/24

## Set the hostname
[edit]
root@# set system host-name junos-01 

## Commit the configuration
[edit]
root@# commit
</code></pre></div><p>As you can see, the syntax style is very different from Cisco IOS. The configuration appears more verbose, but it also separates configuration into well defined sections.</p>
<p>For example, all system configuration is defined within the <code>set system</code> syntax, whereas all protocol configuration is prefixed by set protocol (e.g. <code>set protocol bgp</code>, <code>set protocol ospf</code>).</p>
<p>Once the above is committed, add the device into your Ansible inventory. My inventory file looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#66d9ef">[junos]</span>
<span style="color:#a6e22e">junos-01 ansible_host</span><span style="color:#f92672">=</span><span style="color:#e6db74">10.15.30.33</span>
<span style="color:#a6e22e">junos-02 ansible_host</span><span style="color:#f92672">=</span><span style="color:#e6db74">10.15.30.34</span>
</code></pre></div><h4 id="enabling-ipv6-flows">Enabling IPv6 Flows</h4>
<p>To allow firewalling of IPv6 traffic, you need to enable the <strong>flow-based</strong> option for IPv6 forwarding. This means that rather than trying to evaluate every single packet, traffic is matched in <strong>flows</strong>.</p>
<p>For example, if you have traffic that has the same source IP, destination IP, source port and destination port, this would be considered a flow of traffic. The flow is established when the first packet is sent, and then subsequent packets match the flow and are treated the identically to the first packet.</p>
<p>Without this, every subsequent packet would have to go through every single firewall rule again to find a match. With flows enabled, only the first packet in the session needs to go through the evaluation of the rules.</p>
<p>This saves time on every packet, and also cuts down on processor usage, as you are reducing the amount of CPU cycles required to evaluate every single packet traversing the device.</p>
<p>To enable this for IPv6, you enter the command <code>set security forwarding-options family inet6 mode flow-based</code>. This does require rebooting the device after it is enabled, so it would be best to do this before applying any of the subsequent playbooks and configuration.</p>
<p>This is already enabled by default for IPv4, so you do not need to set this manually.</p>
<h4 id="verification">Verification</h4>
<p>Can we contact both devices?</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ansible junos -m junos_facts --ask-vault-pass | grep -i hostname
Vault password: 
        <span style="color:#e6db74">&#34;ansible_net_hostname&#34;</span>: <span style="color:#e6db74">&#34;junos-01&#34;</span>,
        <span style="color:#e6db74">&#34;ansible_net_hostname&#34;</span>: <span style="color:#e6db74">&#34;junos-02&#34;</span>,
</code></pre></div><h2 id="setup">Setup</h2>
<p>The setup is identical to the IOS lab, with a management interface to access the devices, a VLAN bridge for inter-device communication, and an interface on the edge router attached to the KVM NAT bridge for DHCP/Internet access.</p>
<h3 id="vlans-ip-addressing-and-autonomous-system-numbers">VLANs, IP addressing and Autonomous System numbers</h3>
<p>The ID chosen for Juniper JunOS is <code>02</code>.</p>
<h4 id="vlans">VLANs</h4>
<p>The VLANs used will be: -</p>
<ul>
<li>VLAN102 between the <em>edge</em> router and <em>netsvr-01</em></li>
<li>VLAN202 between the <em>edge</em> router and <em>internal</em> router</li>
</ul>
<h4 id="ip-addressing">IP Addressing</h4>
<ul>
<li>IPv4 Subnet on VLAN102: <code>10.100.102.0/24</code>
<ul>
<li><em>edge</em> router - <code>10.100.102.253/24</code></li>
<li><em>netsvr-01</em> - <code>10.100.102.254/24</code></li>
</ul>
</li>
<li>IPv4 Subnet on VLAN202: 10.100.202.0/24
<ul>
<li><em>edge</em> router - <code>10.100.202.254/24</code></li>
<li><em>internal</em> router - <code>10.100.202.253/24</code></li>
</ul>
</li>
<li>IPv6 Subnet on VLAN102: <code>2001:db8:102::/64</code>
<ul>
<li><em>edge</em> router - <code>2001:db8:102::f/64</code></li>
<li><em>netsvr-01</em> - <code>2001:db8:102:ffff/64</code></li>
</ul>
</li>
<li>IPv6 Subnet on VLAN202: 2001:db8:202::/64
<ul>
<li><em>edge</em> router - <code>2001:db8:202::a/64</code></li>
<li><em>internal</em> router - <code>2001:db8:202:f/64</code></li>
</ul>
</li>
<li>IPv4 Loopback Addressing
<ul>
<li><em>edge</em> router - <code>192.0.2.102/32</code></li>
<li><em>internal</em> router - <code>192.0.2.202/32</code></li>
</ul>
</li>
<li>IPv6 Loopback Address
<ul>
<li><em>edge</em> router - <code>2001:db8:902:beef::1/128</code></li>
<li><em>internal</em> router - <code>2001:db8:902:beef::2/128</code></li>
</ul>
</li>
</ul>
<h4 id="bgp-autonomous-system">BGP Autonomous System</h4>
<p>The BGP <a href="https://en.wikipedia.org/wiki/Autonomous_system_(Internet)">Autonomous System</a> number will be <code>AS65102</code>.</p>
<h2 id="configuration">Configuration</h2>
<h3 id="system-tasks">System tasks</h3>
<p>As per the Cisco IOS lab, this role removes unneeded banners, creates a new banner, and enables <strong>syslog</strong> logging to the <strong>netsvr-01</strong> machine.</p>
<p>We do not need to enable any form of password encryption, as passwords are encrypted by default in JunOS configuration.</p>
<h4 id="playbook">Playbook</h4>
<p>The contents of the Playbook can be seen below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for system</span>
- <span style="color:#66d9ef">name</span>: Set hostname
  <span style="color:#66d9ef">junos_system</span>:
    <span style="color:#66d9ef">hostname</span>: <span style="color:#e6db74">&#34;{{ inventory_hostname }}&#34;</span>

- <span style="color:#66d9ef">name</span>: Remove unneeded banners
  <span style="color:#66d9ef">junos_banner</span>:
    <span style="color:#66d9ef">banner</span>: <span style="color:#e6db74">&#34;{{ item }}&#34;</span>
    <span style="color:#66d9ef">state</span>: absent
  <span style="color:#66d9ef">loop</span>:
  - motd

- <span style="color:#66d9ef">name</span>: Update login banner
  <span style="color:#66d9ef">junos_banner</span>:
    <span style="color:#66d9ef">banner</span>: login
    <span style="color:#66d9ef">text</span>: <span style="color:#e6db74">|
</span><span style="color:#e6db74">      ----------------------------------------</span>
      <span style="color:#e6db74">|
</span><span style="color:#e6db74">      | This banner was generated by Ansible </span>
      <span style="color:#e6db74">|
</span><span style="color:#e6db74">      ----------------------------------------</span>
      <span style="color:#e6db74">|
</span><span style="color:#e6db74">      | You are logged into {{ inventory_hostname }}</span>
      <span style="color:#e6db74">| 
</span><span style="color:#e6db74">      ----------------------------------------</span>
    <span style="color:#66d9ef">state</span>: present

- <span style="color:#66d9ef">name</span>: Configure syslog
  <span style="color:#66d9ef">junos_logging</span>:
    <span style="color:#66d9ef">dest</span>: host
    <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;{{ log_host }}&#34;</span>
    <span style="color:#66d9ef">level</span>: info
    <span style="color:#66d9ef">facility</span>: any
    <span style="color:#66d9ef">state</span>: present
</code></pre></div><p>In this, we also set the hostname of the device, if not already set. This playbook is remarkably similar to the IOS playbook. Anywhere that we have used an <code>ios_*</code> module before, we use a <code>junos_*</code> module (e.g. <code>junos_banner</code> instead of <code>ios_banner</code>).</p>
<h5 id="setting-hostname">Setting Hostname</h5>
<p><strong>Ansible module</strong>: <code>junos_system</code></p>
<p>This task sets the hostname of the device. The generated configuration is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set system host-name junos-01
</code></pre></div><h5 id="removing-unneeded-banners">Removing unneeded banners</h5>
<p><strong>Ansible module</strong>: <code>junos_banner</code></p>
<p>This removes any banners that are set, other than the login banner. In this case, it just removes the <code>motd</code> banner.</p>
<h5 id="update-the-login-banner">Update the login banner</h5>
<p><strong>Ansible module</strong>: <code>junos_banner</code></p>
<p>This task generates a banner for when you login to a device, which references the hostname. As in the Cisco IOS version, you could use a template file to generate this, especially if you have to provide specific information legally or for compliance reasons.</p>
<p>The generated configuration looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set system login message &#34;----------------------------------------\n|\n| This banner was generated by Ansible \n|\n----------------------------------------\n|\n| You are logged into junos-01\n| \n----------------------------------------&#34;
</code></pre></div><p>When you login to the device, this looks like: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ssh ansible@10.15.30.33
----------------------------------------
|
| This banner was generated by Ansible 
|
----------------------------------------
|
| You are logged into junos-01
| 
----------------------------------------
Password:
</code></pre></div><h5 id="configure-syslog">Configure syslog</h5>
<p><strong>Ansible module</strong>: <code>junos_logging</code></p>
<p>This task configures logging to syslog, using a variable called <code>log_host</code>. This variable is defined in the <code>group_vars</code> file: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ cat group_vars/junos | grep -i log
log_host: 10.100.101.254
</code></pre></div><p>We also use the facility <code>any</code>, and the level of <code>info</code>. The facility refers to what kind of logs are being sent (it could be all logs, or it could be those specific to VPNs for example). The level refers to the verbosity of the logs being sent (the higher the level, the more logging will be sent).</p>
<p>The generated configuration is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set system syslog host 10.100.102.254 any info
</code></pre></div><h3 id="interfaces">Interfaces</h3>
<p>This role configures all the interfaces being used on the JunOS device. As mentioned, all Layer 3 configuration (i.e. IP addressing) is configured under a <strong>unit</strong>, so effectively every interface with an IP address is a sub-interface.</p>
<h4 id="playbook-1">Playbook</h4>
<p>The contents of the Playbook are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for interfaces</span>
- <span style="color:#66d9ef">name</span>: Configure subinterfaces first
  <span style="color:#66d9ef">junos_config</span>:
    <span style="color:#66d9ef">src</span>: subints.j2
    <span style="color:#66d9ef">src_format</span>: set

- <span style="color:#66d9ef">name</span>: Configure interfaces - Status and Descriptions
  <span style="color:#66d9ef">junos_interfaces</span>:
    <span style="color:#66d9ef">config</span>:
      - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;{{ item.junos_if }}&#34;</span>
        <span style="color:#66d9ef">description</span>: <span style="color:#e6db74">&#34;{{ item.desc }}&#34;</span>
        <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;{{ item.enabled }}&#34;</span>
  <span style="color:#66d9ef">when</span>: item.unit == <span style="color:#ae81ff">0</span>
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>

- <span style="color:#66d9ef">name</span>: Configure subinterfaces - Descriptions
  <span style="color:#66d9ef">junos_config</span>:
    <span style="color:#66d9ef">src</span>: descs.j2
    <span style="color:#66d9ef">src_format</span>: set

- <span style="color:#66d9ef">name</span>: Configure interfaces - L3 IPv4
  <span style="color:#66d9ef">junos_l3_interfaces</span>:
    <span style="color:#66d9ef">config</span>:
      - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;{{ item.junos_if }}&#34;</span>
        <span style="color:#66d9ef">unit</span>: <span style="color:#e6db74">&#34;{{ item.unit }}&#34;</span>
        <span style="color:#66d9ef">ipv4</span>:
        - <span style="color:#66d9ef">address</span>: <span style="color:#e6db74">&#34;{{ item.ipv4_addr }}&#34;</span>
  <span style="color:#66d9ef">when</span>: 
    - item.ipv4_addr is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>

- <span style="color:#66d9ef">name</span>: Configure interfaces - L3 IPv6
  <span style="color:#66d9ef">junos_l3_interfaces</span>:
    <span style="color:#66d9ef">config</span>:
      - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;{{ item.junos_if }}&#34;</span>
        <span style="color:#66d9ef">unit</span>: <span style="color:#e6db74">&#34;{{ item.unit }}&#34;</span>
        <span style="color:#66d9ef">ipv6</span>:
        - <span style="color:#66d9ef">address</span>: <span style="color:#e6db74">&#34;{{ item.ipv6_addr }}&#34;</span>
  <span style="color:#66d9ef">when</span>:
    - item.ipv6_addr is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
</code></pre></div><p>What you&rsquo;ll notice here is that we have a task for setting interface status and descriptions, but also one just below it for setting the descriptions of sub-interfaces too.</p>
<p>This is necessary because unfortunately the <code>junos_interfaces</code> module does not support <strong>units</strong>, so you cannot set the descriptions on them.</p>
<h5 id="configuring-sub-interfaces">Configuring sub-interfaces</h5>
<p><strong>Ansible module</strong>: <code>junos_config</code></p>
<p>As mentioned above, the <code>junos_interfaces</code> module (at the time of writing) does not support sub-interfaces (and therefore adding VLANs to an underlying interface).</p>
<p>Instead, we will use the <code>junos_config</code> module. This module is functionally identical to the <code>ios_config</code> module, in that you supply a configuration template or individual lines of configuration to be applied.</p>
<p>The <code>src_format</code> option is used to specify the format of configuration used in your templates. You can use <code>xml</code>, <code>text</code>, <code>json</code> or <code>set</code>. The <code>set</code> option is configuration like <code>set interface *****</code>.</p>
<p>The template being applied is below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% for interface in interfaces %}
{% if interface['subint'] is defined %}
set interfaces  {{ interface['junos_if'] }} vlan-tagging
{% for vlan in interface['subint']['vlans'] %}
set interfaces {{ interface['junos_if'] }} unit {{ vlan }} vlan-id {{ vlan }}
{% endfor %}
{% endif %}
{% endfor %} 
</code></pre><p>This template sets the underlying interface (i.e. the interface which passes VLANs) to <code>vlan-tagging</code> mode (what Cisco would call a &ldquo;trunked&rdquo; port). It also creates the interface <strong>units</strong>, associating them with the VLANs defined in our <code>host_vars</code>.</p>
<p>Our <code>host_vars</code> look like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;ge-0/0/0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">0</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;VLAN Bridge&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">subint</span>:
      <span style="color:#66d9ef">vlans</span>:
      - <span style="color:#ae81ff">102</span>
      - <span style="color:#ae81ff">202</span>
</code></pre></div><p>The configuration this generates is: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set interfaces ge-0/0/0 vlan-tagging
set interfaces ge-0/0/0 unit 102 vlan-id 102
set interfaces ge-0/0/0 unit 202 vlan-id 202
</code></pre></div><p>This means that when we apply configuration (e.g. IP address) to <code>ge0/0/0.102</code> and <code>ge-0/0/0.202</code>, they will use VLAN tags <strong>102</strong> and <strong>202</strong> respectively.</p>
<p>An unfortunate caveat is that we also need to create <code>unit 0</code> with a VLAN ID when <code>vlan-tagging</code> is used. Our configuration template says that the VLAN ID matches the unit ID, but VLAN <code>0</code> is not a valid VLAN ID.</p>
<p>Therefore, we have to manually add <code>set interfaces ge-0/0/0 unit 0 vlan-id 1</code> to make sure that the rest of the configuration works as expected. Once the Ansible JunOS modules support units properly, we can remove this element of manual configuration.</p>
<h5 id="status-and-descriptions-not-subinterfaces">Status and descriptions (not subinterfaces)</h5>
<p><strong>Ansible module</strong>: <code>junos_interfaces</code></p>
<p>The descriptions and statuses are of our physical interfaces (rather than the logical interface units) are controlled with this task.</p>
<p>This is done by only matching for interfaces in our <code>host_vars</code> that have a <strong>unit</strong> of <code>0</code> (i.e. the base/default unit for the interface). For example, the below is a summarized version of the <code>host_vars</code> for <code>junos-01</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;fxp0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">0</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Management&#34;</span>
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;ge-0/0/0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">0</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;VLAN Bridge&#34;</span>
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;ge-0/0/0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">102</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To netsvr&#34;</span>
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;ge-0/0/0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">202</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To junos-02&#34;</span>
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;ge-0/0/1&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To the Internet&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">0</span>
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;lo0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">0</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
</code></pre></div><p>We therefore apply configuration to: -</p>
<ul>
<li><code>fxp0</code></li>
<li><code>ge-0/0/0</code></li>
<li><code>ge-0/0/1</code></li>
<li><code>lo0</code></li>
</ul>
<p>The following configuration is generated: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set interfaces ge-0/0/0 description &#34;VLAN Bridge&#34;
set interfaces ge-0/0/1 description &#34;To the Internet&#34;
set interfaces fxp0 description Management
set interfaces lo0 description Loopback
</code></pre></div><p>The descriptions are not applied to <code>unit 0</code> of each interface because of the lack of <strong>unit</strong> support in <code>junos_interfaces</code>. For this we need another task.</p>
<h5 id="status-and-descriptions-subinterfaces">Status and descriptions (subinterfaces)</h5>
<p><strong>Ansible module</strong>: <code>junos_config</code></p>
<p>This module is used to update the descriptions of every sub-interface. The template looks like the following: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% for interface in interfaces %}
set interfaces {{ interface['junos_if'] }} unit {{ interface['unit'] }} description &quot;{{ interface['desc'] }}&quot;
{% endfor %} 
</code></pre><p>With the <code>host_vars</code> mentioned in the previous task, this would generate the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set interfaces ge-0/0/0 unit 0 description &#34;VLAN Bridge&#34;
set interfaces ge-0/0/0 unit 102 description &#34;To netsvr&#34;
set interfaces ge-0/0/0 unit 202 description &#34;To junos-02&#34;
set interfaces ge-0/0/1 unit 0 description &#34;To the Internet&#34;
set interfaces fxp0 unit 0 description Management
set interfaces lo0 unit 0 description Loopback
</code></pre></div><p>If you compare this to the previous task, we unfortunately create duplicate descriptions (i.e. on the underlying physical interface, and <code>unit 0</code>). We could run this module only on interfaces that have non-zero <strong>units</strong>, but this would create issues for the next task.</p>
<h5 id="ipv4-addressing">IPv4 addressing</h5>
<p><strong>Ansible module</strong>: <code>junos_l3_interfaces</code></p>
<p>This works similarly to the Cisco IOS task, in that it applies an IPv4 address to an interface (to whatever <strong>unit</strong> is defined in our <code>host_vars</code>).</p>
<p>However, there is a caveat. The JunOS module appears to make an attempt at gathering the existing configuration on the interface <strong>units</strong> before making any changes. If a <strong>unit</strong> do not exist to apply the addresses to (i.e. no previous task created them), then the module fails.</p>
<p>This differs from Cisco IOS. Our Cisco IOS task created sub-interfaces if they did not exist during the process of adding IPv4 (or IPv6) addressing to them.</p>
<p>This unfortunately means that until the <code>junos_interfaces</code> module supports <strong>units</strong>, or the <code>junos_l3_interfaces</code> module can create interfaces, we need a series of workarounds to allow us to use VLANs, descriptions on all our interfaces and IP addressing.</p>
<p>The generated configuration from this module is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set interfaces ge-0/0/0 unit 102 family inet address 10.100.102.253/24
set interfaces ge-0/0/0 unit 202 family inet address 10.100.202.254/24
set interfaces ge-0/0/1 unit 0 family inet dhcp
set interfaces fxp0 unit 0 family inet address 10.15.30.33/24
set interfaces lo0 unit 0 family inet address 192.0.2.102/32
</code></pre></div><p>Unlike Cisco IOS, JunOS uses the CIDR address format (i.e. <code>X.X.X.X/Y</code>) natively, rather than using subnet mask.</p>
<p>It also supports the <code>dhcp</code> keyword, just like the Cisco IOS module does.</p>
<h5 id="ipv6-addressing">IPv6 addressing</h5>
<p><strong>Ansible module</strong>: <code>junos_l3_interfaces</code></p>
<p>This is identical to the above task, except we are applying IPv6 addressing.</p>
<p>The output of the task is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set interfaces ge-0/0/0 unit 102 family inet6 address 2001:db8:102::f/64
set interfaces ge-0/0/0 unit 202 family inet6 address 2001:db8:202::a/64
set interfaces lo0 unit 0 family inet6 address 2001:db8:902:beef::1/128
</code></pre></div><h4 id="verification-1">Verification</h4>
<p><strong>junos-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show IPs (IPv4 and IPv6)
ansible@junos-01&gt; show interfaces terse | match &#34;inet|inet6&#34; 
ge-0/0/0.102            up    up   inet     10.100.102.253/24
                                   inet6    2001:db8:102::f/64
ge-0/0/0.202            up    up   inet     10.100.202.254/24
                                   inet6    2001:db8:202::a/64
ge-0/0/1.0              up    up   inet     192.168.122.23/24
fxp0.0                  up    up   inet     10.15.30.33/24  
lo0.0                   up    up   inet     192.0.2.102         --&gt; 0/0
                                   inet6    2001:db8:902:beef::1

! Show interface statuses and descriptions
!! Notice the duplicate descriptions
ansible@junos-01&gt; show interfaces descriptions 
Interface       Admin Link Description
ge-0/0/0        up    up   VLAN Bridge
ge-0/0/0.0      up    up   VLAN Bridge
ge-0/0/0.102    up    up   To netsvr
ge-0/0/0.202    up    up   To junos-02
ge-0/0/1        up    up   To the Internet
ge-0/0/1.0      up    up   To the Internet
fxp0            up    up   Management
fxp0.0          up    up   Management
lo0             up    up   Loopback
lo0.0           up    up   Loopback

! Ping to netsvr-01 on IPv4 and IPv6
ansible@junos-01&gt; ping 10.100.102.254 
PING 10.100.102.254 (10.100.102.254): 56 data bytes
64 bytes from 10.100.102.254: icmp_seq=0 ttl=64 time=5.430 ms
^C
--- 10.100.102.254 ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max/stddev = 5.430/5.430/5.430/0.000 ms

ansible@junos-01&gt; ping 2001:db8:102::ffff 
PING6(56=40+8+8 bytes) 2001:db8:102::f --&gt; 2001:db8:102::ffff
16 bytes from 2001:db8:102::ffff, icmp_seq=0 hlim=64 time=7.451 ms
16 bytes from 2001:db8:102::ffff, icmp_seq=1 hlim=64 time=0.425 ms
^C
--- 2001:db8:102::ffff ping6 statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/std-dev = 0.425/3.938/7.451/3.513 ms

! Ping to junos-02 on IPv4 and IPv6
ansible@junos-01&gt; ping 10.100.202.253 
PING 10.100.202.253 (10.100.202.253): 56 data bytes
64 bytes from 10.100.202.253: icmp_seq=0 ttl=64 time=0.547 ms
^C
--- 10.100.202.253 ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.547/0.547/0.547/0.000 ms

ansible@junos-01&gt; ping 2001:db8:202::f    
PING6(56=40+8+8 bytes) 2001:db8:202::a --&gt; 2001:db8:202::f
16 bytes from 2001:db8:202::f, icmp_seq=0 hlim=64 time=0.529 ms
16 bytes from 2001:db8:202::f, icmp_seq=1 hlim=64 time=0.513 ms
^C
--- 2001:db8:202::f ping6 statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/std-dev = 0.513/0.521/0.529/0.008 ms
</code></pre></div><p><strong>junos-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show IPs (IPv4 and IPv6)
ansible@junos-02&gt; show interfaces terse | match &#34;inet|inet6&#34; 
ge-0/0/0.202            up    up   inet     10.100.202.253/24
                                   inet6    2001:db8:202::f/64
sp-0/0/0.0              up    up   inet    
                                   inet6   
sp-0/0/0.16383          up    up   inet    
em0.0                   up    up   inet     128.0.0.1/2     
em1.32768               up    up   inet     192.168.1.2/24  
fxp0.0                  up    up   inet     10.15.30.34/24  
lo0.0                   up    up   inet     192.0.2.202         --&gt; 0/0
                                   inet6    2001:db8:902:beef::2
lo0.16384               up    up   inet     127.0.0.1           --&gt; 0/0
lo0.16385               up    up   inet     10.0.0.1            --&gt; 0/0

! Show interface statuses and descriptions
!! Notice the duplicate descriptions
ansible@junos-02&gt; show interfaces descriptions 
Interface       Admin Link Description
ge-0/0/0        up    up   VLAN Bridge
ge-0/0/0.0      up    up   VLAN Bridge
ge-0/0/0.202    up    up   To junos-01
fxp0            up    up   Management
fxp0.0          up    up   Management
lo0             up    up   Loopback
lo0.0           up    up   Loopback

! Ping to junos-01 on IPv4 and IPv6
ansible@junos-02&gt; ping 10.100.202.253 
PING 10.100.202.253 (10.100.202.253): 56 data bytes
64 bytes from 10.100.202.253: icmp_seq=0 ttl=64 time=0.645 ms
^C
--- 10.100.202.253 ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.645/0.645/0.645/0.000 ms

ansible@junos-02&gt; ping 2001:db8:202::a   
PING6(56=40+8+8 bytes) 2001:db8:202::f --&gt; 2001:db8:202::a
16 bytes from 2001:db8:202::a, icmp_seq=0 hlim=64 time=1.724 ms
16 bytes from 2001:db8:202::a, icmp_seq=1 hlim=64 time=1.380 ms
16 bytes from 2001:db8:202::a, icmp_seq=2 hlim=64 time=1.272 ms
^C
--- 2001:db8:202::a ping6 statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max/std-dev = 1.272/1.459/1.724/0.193 ms
</code></pre></div><p>Looking good so far!</p>
<h3 id="firewall">Firewall</h3>
<p>As we&rsquo;re using the Juniper vSRX images, we can make use of zone-based firewalling. In the Cisco IOS lab, we only used access lists, and only used them on the interface between the <strong>edge</strong> router and <strong>netsvr-01</strong>.</p>
<p>In this, we are building firewall policies, placing all used interfaces in zones, and applying rules on both the <strong>edge</strong> router (<strong>junos-01</strong>) and the <strong>internal</strong> router (<strong>junos-02</strong>).</p>
<p>A point to note with firewalling compared to access lists is that most firewalls are stateful. This means that you only need to define a rule for traffic flowing from source to destination. A &ldquo;state&rdquo; (i.e. an entry of where the traffic is coming from and going to) is created, and will automatically match return traffic.</p>
<p>For example, if you SSH to a server, your SSH client will use a randomized source port (e.g. <code>TCP47846</code>), and a destination port of <code>TCP22</code> (unless SSH is running on a different port of course!). The return traffic from the server would have a source port of <code>TCP22</code> and a destination port of <code>TCP47846</code>. A stateful firewall is able to track this, and hence rules are not required to be defined in both directions.</p>
<h4 id="playbook-2">Playbook</h4>
<p>The contents of the playbook are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for firewall</span>
- <span style="color:#66d9ef">name</span>: Remove default firewall config
  <span style="color:#66d9ef">junos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - delete security zones security-zone trust
      - delete security zones security-zone untrust
      - delete security screen ids-option untrust-screen
      - delete security policies from-zone trust to-zone trust policy default-permit
      - delete security policies from-zone trust to-zone untrust policy default-permit
  <span style="color:#66d9ef">tags</span>:
    - firewall

- <span style="color:#66d9ef">name</span>: Define Firewall Zones
  <span style="color:#66d9ef">junos_config</span>:
    <span style="color:#66d9ef">src</span>: zones.j2
  <span style="color:#66d9ef">when</span>:
    - zones is defined
  <span style="color:#66d9ef">tags</span>:
    - firewall

- <span style="color:#66d9ef">name</span>: Add interfaces to zones
  <span style="color:#66d9ef">junos_config</span>:
    <span style="color:#66d9ef">src</span>: int_zones.j2
  <span style="color:#66d9ef">tags</span>:
    - firewall

- <span style="color:#66d9ef">name</span>: Define Addresses
  <span style="color:#66d9ef">junos_config</span>:
    <span style="color:#66d9ef">src</span>: addressbook.j2
  <span style="color:#66d9ef">when</span>:
    - addressbook is defined
  <span style="color:#66d9ef">tags</span>:
    - firewall

- <span style="color:#66d9ef">name</span>: Define Applications
  <span style="color:#66d9ef">junos_config</span>:
    <span style="color:#66d9ef">src</span>: apps.j2
  <span style="color:#66d9ef">when</span>:
    - apps is defined
  <span style="color:#66d9ef">tags</span>:
    - firewall

- <span style="color:#66d9ef">name</span>: Define Global Policies
  <span style="color:#66d9ef">junos_config</span>:
    <span style="color:#66d9ef">src</span>: globalpolicy.j2
    <span style="color:#66d9ef">update</span>: replace
  <span style="color:#66d9ef">when</span>:
    - fw_policies is defined
  <span style="color:#66d9ef">tags</span>:
    - firewall

- <span style="color:#66d9ef">name</span>: Define Zone Policies
  <span style="color:#66d9ef">junos_config</span>:
    <span style="color:#66d9ef">src</span>: policy.j2
    <span style="color:#66d9ef">update</span>: replace
  <span style="color:#66d9ef">when</span>:
    - fw_policies is defined
  <span style="color:#66d9ef">tags</span>:
    - firewall
</code></pre></div><p>JunOS uses the following terms when it comes to zone-based firewalling: -</p>
<ul>
<li><strong>Zones</strong> - A logical grouping of one or multiple interfaces (e.g. <strong>internal</strong>, <strong>edge</strong>, <strong>finance</strong>)</li>
<li><strong>Address books</strong> - The IP addresses (v4 or v6, or can be DNS-based) of the hosts/ranges to firewall</li>
<li><strong>Applications</strong> - The applications (e.g. SSH, SIP, IPSec) that will be matched</li>
<li><strong>Policies</strong> - The policies that are applied for traffic traversing between, or inside a zone</li>
</ul>
<p>Firewall policies are applied at the zone-level, allowing multiple interfaces to be covered by the same policies. This allows you to group all of your internal-facing interfaces into one logical zone, or all your external peering/transit interfaces into one logical zone. This makes applying firewalling to multiple interfaces much easier.</p>
<p>Also, JunOS has the ability to apply <strong>Global</strong> policies. These match any traffic traversing the firewall, no matter the source or destination zone. This can be useful for traffic that will always be allowed (for example, <a href="http://shouldiblockicmp.com/">always allowing ICMP</a>).</p>
<p>All tasks use the <code>junos_config</code> module, as no Ansible module currently exists for SRX firewalling.</p>
<h5 id="removing-default-firewall-configuration">Removing default firewall configuration</h5>
<p><strong>Ansible module</strong>: <code>junos_config</code></p>
<p>JunOS SRXs have  a few zones and features configured and enabled by default. These include a <code>trust</code> zone, an <code>untrust</code> zone, some default firewall policies (allow any interface in the <code>trust</code> zone to speak to any interface in either the <code>trust</code> or <code>untrust</code> zone) and an IDS (<strong>I</strong>ntrustion <strong>D</strong>etection <strong>S</strong>ystem) enabled.</p>
<p>This task removes all of the defaults so that we can define our own zones and policies.</p>
<h5 id="defining-zones">Defining Zones</h5>
<p><strong>Ansible module</strong>: <code>junos_config</code></p>
<p>This task defines the firewall zones, and places interfaces into them. The <code>host_vars</code> used as part of this are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">zones</span>:
  - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;edge&#34;</span>
    <span style="color:#66d9ef">host_traffic</span>:
      <span style="color:#66d9ef">protocols</span>:
        - bgp
      <span style="color:#66d9ef">services</span>:
        - ping
        - traceroute
  - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;internet&#34;</span>
    <span style="color:#66d9ef">nat</span>: 
      <span style="color:#66d9ef">role</span>: <span style="color:#e6db74">&#34;outside&#34;</span>
    <span style="color:#66d9ef">host_traffic</span>:
      <span style="color:#66d9ef">services</span>:
        - ping
        - traceroute
        - dhcp
  - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;internal&#34;</span>
    <span style="color:#66d9ef">nat</span>:
      <span style="color:#66d9ef">role</span>: <span style="color:#e6db74">&#34;inside&#34;</span>
    <span style="color:#66d9ef">host_traffic</span>:
      <span style="color:#66d9ef">protocols</span>:
        - bgp
        - ospf
        - ospf3
      <span style="color:#66d9ef">services</span>:
        - ping
        - traceroute
</code></pre></div><p>Our template looks like the below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% for zone in zones %}
set security zones security-zone {{ zone['name'] }}
set security address-book {{ zone['name'] }} attach zone {{ zone['name'] }}
{% if zone['host_traffic'] is defined %}
{% if zone['host_traffic']['protocols'] is defined %}
{% for protocol in zone['host_traffic']['protocols'] %}
set security zones security-zone {{ zone['name'] }} host-inbound-traffic protocols {{ protocol }} 
{% endfor %}
{% endif %}
{% if zone['host_traffic']['services'] is defined %}
{% for service in zone['host_traffic']['services'] %}
set security zones security-zone {{ zone['name'] }} host-inbound-traffic system-services {{ service }} 
{% endfor %}
{% endif %}
{% endif %}
{% endfor %}
</code></pre><p>First we loop through the <code>zones</code>. For each zone that is defined, we create it, and also create an associated <code>address-book</code> (i.e. the list of hosts we&rsquo;ll be firewalling).</p>
<p>After that, we configure <code>host-inbound-traffic</code>. This is any traffic that destined for firewall itself (rather than traversing through it). It could be routing protocol traffic, pings or DHCP. Without this, BGP sessions will not form, OSPF will not work and we will not get any DHCP leases.</p>
<p>The two sections deal with <code>protocols</code> and <code>system-services</code>. For routing protocols, or VRRP (<strong>V</strong>irtual <strong>R</strong>outer <strong>R</strong>edundancy <strong>P</strong>rotocol) for highly-available virtual IPs, these would go into the <code>protocols</code> section. For anything like pings, DHCP, SSH, VPNs, these go in <code>system-services</code>.</p>
<p>You can find the full list available on a JunOS device by typing in <code>set security-zones security zone test host-inbound-traffic protocols ?</code> or <code>set security-zones security zone test host-inbound-traffic system-services ?</code> from configuration mode.</p>
<p>The generated configuration, based upon our <code>host_vars</code>, is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set security zones security-zone edge host-inbound-traffic system-services ping
set security zones security-zone edge host-inbound-traffic system-services traceroute
set security zones security-zone edge host-inbound-traffic system-services dhcp
set security zones security-zone edge host-inbound-traffic protocols bgp
set security zones security-zone internal host-inbound-traffic system-services ping
set security zones security-zone internal host-inbound-traffic system-services traceroute
set security zones security-zone internal host-inbound-traffic protocols bgp
set security zones security-zone internal host-inbound-traffic protocols ospf
set security zones security-zone internal host-inbound-traffic protocols ospf3
set security zones security-zone internet host-inbound-traffic system-services ping
set security zones security-zone internet host-inbound-traffic system-services traceroute
set security zones security-zone internet host-inbound-traffic system-services dhcp
</code></pre></div><h5 id="adding-interfaces-to-zones">Adding interfaces to zones</h5>
<p><strong>Ansible module</strong>: <code>junos_config</code></p>
<p>This task adds the interfaces to the relevant zones. The <code>host_vars</code> used for this are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;fxp0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">0</span>
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;ge-0/0/0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">0</span>
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;ge-0/0/0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">102</span>
    <span style="color:#66d9ef">if_zone</span>: <span style="color:#e6db74">&#34;edge&#34;</span>
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;ge-0/0/0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">202</span>
    <span style="color:#66d9ef">if_zone</span>: <span style="color:#e6db74">&#34;internal&#34;</span>
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;ge-0/0/1&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">0</span>
    <span style="color:#66d9ef">if_zone</span>: <span style="color:#e6db74">&#34;internet&#34;</span>
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;lo0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">0</span>
    <span style="color:#66d9ef">if_zone</span>: <span style="color:#e6db74">&#34;internal&#34;</span>
</code></pre></div><p>The template looks like the below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% for interface in interfaces %}
{% if interface['if_zone'] is defined %}
set security zones security-zone {{ interface['if_zone'] }} interfaces {{ interface['junos_if'] }}.{{ interface['unit'] }}
{% endif %}
{% endfor %}
</code></pre><p>The template loops through our interfaces, and if a zone is defined, it configures the interface as part of the zone. Notice in the above <code>host_vars</code> that we do not add the <code>fxp0</code> interface (our management interface) or the underlying <code>ge-0/0/0</code> interface (which carries VLANs) as part of a zone.</p>
<p>The generated configuration looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set security zones security-zone edge interfaces ge-0/0/0.102
set security zones security-zone internal interfaces ge-0/0/0.202
set security zones security-zone internal interfaces lo0.0
set security zones security-zone internet interfaces ge-0/0/1.0
</code></pre></div><h5 id="define-addresses">Define addresses</h5>
<p><strong>Ansible modules</strong>: <code>junos_config</code></p>
<p>This task defines all the hosts/ranges that will be firewalled. It can either apply them to the <strong>global</strong> address book (and can therefore be used in any zone), or to a zone-specific address book, at which point they can only be used for rules/policies referencing that zone.</p>
<p>The separation may seem strange, but it makes it cleaner and easier to find what hosts belong to what zone in configuration, rather than just one big set of hosts.</p>
<p>The template used is below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% for address in addressbook %}
{% if 'global' in address['zone'] %}
set security address-book global address {{ address['name'] }} {{ address['ip'] }}
{% if address['set'] is defined %}
set security address-book global address-set {{ address['set'] }} address {{ address['name'] }} 
{% endif %}
{% else %}
set security address-book {{ address['zone'] }} address {{ address['name'] }} {{ address['ip'] }}
{% if address['set'] is defined %}
{% for addr_set in address['set'] %}
set security address-book {{ address['zone'] }} address-set {{ addr_set }} address {{ address['name'] }}
{% endfor %}
{% endif %}
{% endif %}
{% endfor %}
</code></pre><p>In the above, we define addresses, and also something called an <code>address-set</code>. This is a group of addresses that should be firewalled the same (say, multiple web servers, or multiple user groups). An address can also be part of multiple sets.</p>
<p>Our <code>host_vars</code> for this are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">addressbook</span>:
  - <span style="color:#66d9ef">zone</span>: edge
    <span style="color:#66d9ef">name</span>: netsvr
    <span style="color:#66d9ef">ip</span>: <span style="color:#ae81ff">10.100.102.254</span>
    <span style="color:#66d9ef">set</span>:
      - external_bgp_peers
      - netsvr-direct
  - <span style="color:#66d9ef">zone</span>: edge
    <span style="color:#66d9ef">name</span>: netsvr-v6
    <span style="color:#66d9ef">ip</span>: <span style="color:#e6db74">&#34;2001:db8:102::ffff/128&#34;</span>
    <span style="color:#66d9ef">set</span>:
      - external_bgp_peers
      - netsvr-direct
  - <span style="color:#66d9ef">zone</span>: edge
    <span style="color:#66d9ef">name</span>: netsvr-lo
    <span style="color:#66d9ef">ip</span>: <span style="color:#ae81ff">192.0.2.1</span>
    <span style="color:#66d9ef">set</span>:
    - netsvr-loop
  - <span style="color:#66d9ef">zone</span>: edge
    <span style="color:#66d9ef">name</span>: netsvr-lo-v6
    <span style="color:#66d9ef">ip</span>: <span style="color:#e6db74">&#34;2001:db8:999:beef::1/128&#34;</span>
    <span style="color:#66d9ef">set</span>: 
    - netsvr-loop
  - <span style="color:#66d9ef">zone</span>: internal
    <span style="color:#66d9ef">name</span>: internal-rtr
    <span style="color:#66d9ef">ip</span>: <span style="color:#ae81ff">192.0.2.202</span>
    <span style="color:#66d9ef">set</span>: 
    - internal_bgp_peers
    - internal-rtr-loop
  - <span style="color:#66d9ef">zone</span>: internal
    <span style="color:#66d9ef">name</span>: internal-rtr-v6
    <span style="color:#66d9ef">ip</span>: <span style="color:#e6db74">&#34;2001:db8:902:beef::2/128&#34;</span>
    <span style="color:#66d9ef">set</span>: 
    - internal_bgp_peers
    - internal-rtr-loop
</code></pre></div><p>This would then generate the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set security address-book edge address netsvr 10.100.102.254/32
set security address-book edge address netsvr-v6 2001:db8:102::ffff/128
set security address-book edge address netsvr-lo 192.0.2.1/32
set security address-book edge address netsvr-lo-v6 2001:db8:999:beef::1/128
set security address-book edge address-set external_bgp_peers address netsvr
set security address-book edge address-set external_bgp_peers address netsvr-v6
set security address-book edge address-set netsvr-direct address netsvr
set security address-book edge address-set netsvr-direct address netsvr-v6
set security address-book edge address-set netsvr-loop address netsvr-lo
set security address-book edge address-set netsvr-loop address netsvr-lo-v6
set security address-book internal address internal-rtr 192.0.2.202/32
set security address-book internal address internal-rtr-v6 2001:db8:902:beef::2/128
set security address-book internal address-set internal_bgp_peers address internal-rtr
set security address-book internal address-set internal_bgp_peers address internal-rtr-v6
set security address-book internal address-set internal-rtr-loop address internal-rtr
set security address-book internal address-set internal-rtr-loop address internal-rtr-v6
</code></pre></div><p>As you can see, some of the addresses are in multiple <code>address-set</code>s. With this, we can apply firewall policies that cover all of our external BGP peers, or all of our internal BGP peers.</p>
<h5 id="defining-applications">Defining applications</h5>
<p><strong>Ansible module</strong>: <code>junos_config</code></p>
<p>In JunOS, <strong>applications</strong> are the ports and/or protocols that will be matched by a firewall policy.</p>
<p>Many applications are already defined by default, so you may not need to add any yourself. In fact in this lab, I have used &ldquo;well-known&rdquo; (i.e. common) applications, so this task will actually not configure anything during this lab.</p>
<p>The template looks like the below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% for app in apps %}
set applications application {{ app['name'] }} destination-port {{ app['port'] }}
set applications application {{ app['name'] }} protocol {{ app['proto'] }}
{% endfor %} 
</code></pre><p>This loops through a list of <code>apps</code> in our <code>host_vars</code>. For each in the list, we configure the name, port and protocol.</p>
<h5 id="global-policies">Global Policies</h5>
<p><strong>Ansible module</strong>: <code>junos_config</code></p>
<p>This task configures <strong>global</strong> policies. The policies tie together the source and/or destination of traffic, the applications to match, and the action (i.e. permit or deny). These are applied <strong>globally</strong> (i.e. across all zones, rather than being zone specific).</p>
<p>The template looks like the below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% for policy in fw_policies %}
{% if 'global' in policy['zone'] %}
delete security policies global policy {{ policy['name'] }}
{% if 'any' in policy['source'] %}
set security policies global policy {{ policy['name'] }}  match source-address any
{% else %}
{% for s_addr in policy['source'] %}
set security policies global policy {{ policy['name'] }}  match source-address {{ s_addr }}
{% endfor %}
{% endif %}
{% if 'any' in policy['destination'] %}
set security policies global policy {{ policy['name'] }}  match destination-address any
{% else %}
{% for d_addr in policy['destination'] %}
set security policies global policy {{ policy['name'] }}  match destination-address {{ d_addr }}
{% endfor %}
{% endif %}
{% if 'any' in policy['apps'] %}
set security policies global policy {{ policy['name'] }} match application any
{% else %}
{% for app in policy['apps'] %}
set security policies global policy {{ policy['name'] }} match application {{ app }}
{% endfor %}
{% endif %}
set security policies global policy {{ policy['name'] }} then {{ policy['action'] }}
{% endif %}
{% endfor %}
</code></pre><p>First, we go through our <code>fw_policies</code> list (defined in our <code>host_vars</code>) and then match any policy which has a <code>zone</code> of <strong>global</strong>. We then delete the policy, to remove any existing configuration. This stops us from adding to existing policies, potentially allowing hosts through that no longer exist or that no longer should have access. Each policy is rebuilt.</p>
<p>After that, we check to see the source addresses defined. If the source address is <strong>any</strong>, then any source is matched by this policy. Otherwise, we loop through a list of addresses (or address-sets, both are applicable) and define them as part of the policy. This matches where the traffic originates from.</p>
<p>We then do the same, but for the destination addresses. This matches where the traffic is destined for.</p>
<p>Next we match applications. They can be <strong>any</strong> application (i.e. any destination port/protocol), or based upon the list of applications in our <code>host_vars</code>.</p>
<p>Finally, we then say whether the policy accepts or rejects the traffic (the <code>action</code>).</p>
<p>In our <code>host_vars</code>, we have the following policy that is applied at the <strong>global</strong> level: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">fw_policies</span>:
  - <span style="color:#66d9ef">name</span>: all_icmp
    <span style="color:#66d9ef">zone</span>: global
    <span style="color:#66d9ef">source</span>: any
    <span style="color:#66d9ef">destination</span>: any
    <span style="color:#66d9ef">action</span>: permit
    <span style="color:#66d9ef">apps</span>:
      - junos-ping
      - junos-pingv6
</code></pre></div><p>This generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set security policies global policy all_icmp match source-address any
set security policies global policy all_icmp match destination-address any
set security policies global policy all_icmp match application junos-ping
set security policies global policy all_icmp match application junos-pingv6
set security policies global policy all_icmp then permit
</code></pre></div><h5 id="define-zone-policies">Define zone policies</h5>
<p><strong>Ansible module</strong>: <code>junos_config</code></p>
<p>This task is very similar to the previous task, except they are zone-specific (rather than applied across all zones). By default, traffic will not be allowed between zones unless a policy is defined to allow it, nor will traffic be allowed between interfaces in the same zone.</p>
<p>The latter may seem odd, but you may want to apply a common policy of what users are allowed to access of your company&rsquo;s resources without necessarily allowing them to access each other&rsquo;s workstations.</p>
<p>The template for this looks like the below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% for policy in fw_policies %}
{% if 'global' not in policy['zone'] %}
delete security policies from-zone {{ policy['from_zone'] }} to-zone {{ policy['to_zone'] }} policy {{ policy['name'] }}
{% if 'any' in policy['source'] %}
set security policies from-zone {{ policy['from_zone'] }} to-zone {{ policy['to_zone'] }} policy {{ policy['name'] }}  match source-address any
{% else %}
{% for s_addr in policy['source'] %}
set security policies from-zone {{ policy['from_zone'] }} to-zone {{ policy['to_zone'] }} policy {{ policy['name'] }}  match source-address {{ s_addr }}
{% endfor %}
{% endif %}
{% if 'any' in policy['destination'] %}
set security policies from-zone {{ policy['from_zone'] }} to-zone {{ policy['to_zone'] }} policy {{ policy['name'] }}  match destination-address any
{% else %}
{% for d_addr in policy['destination'] %}
set security policies from-zone {{ policy['from_zone'] }} to-zone {{ policy['to_zone'] }} policy {{ policy['name'] }}  match destination-address {{ d_addr }}
{% endfor %}
{% endif %}
{% if 'any' in policy['apps'] %}
set security policies from-zone {{ policy['from_zone'] }} to-zone {{ policy['to_zone'] }} policy {{ policy['name'] }} match application any 
{% else %}
{% for app in policy['apps'] %}
set security policies from-zone {{ policy['from_zone'] }} to-zone {{ policy['to_zone'] }} policy {{ policy['name'] }} match application {{ app }}
{% endfor %}
{% endif %}
set security policies from-zone {{ policy['from_zone'] }} to-zone {{ policy['to_zone'] }} policy {{ policy['name'] }} then {{ policy['action'] }}
{% endif %}
{% endfor %}
</code></pre><p>The above is effectively an extended version of the <strong>global</strong> policy task. We now also include the <code>from_zone</code> and <code>to_zone</code> variables. As before, we also delete the policy at the start, so that we are not just updating an existing policy (and potentially leaving old hosts/applications in the policy), and rebuild it.</p>
<p>The <code>host_vars</code> that are referenced by this task are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">fw_policies</span>:
  - <span style="color:#66d9ef">name</span>: a_tacacs
    <span style="color:#66d9ef">zone</span>: internal
    <span style="color:#66d9ef">from_zone</span>: internal
    <span style="color:#66d9ef">to_zone</span>: edge
    <span style="color:#66d9ef">source</span>: any
    <span style="color:#66d9ef">destination</span>: 
      - netsvr-loop
    <span style="color:#66d9ef">action</span>: permit
    <span style="color:#66d9ef">apps</span>:
      - junos-tacacs
  - <span style="color:#66d9ef">name</span>: a_syslog
    <span style="color:#66d9ef">zone</span>: internal
    <span style="color:#66d9ef">from_zone</span>: internal
    <span style="color:#66d9ef">to_zone</span>: edge
    <span style="color:#66d9ef">source</span>: any
    <span style="color:#66d9ef">destination</span>: 
      - netsvr-direct
    <span style="color:#66d9ef">action</span>: permit
    <span style="color:#66d9ef">apps</span>:
      - junos-syslog
  - <span style="color:#66d9ef">name</span>: a_ext_bgp
    <span style="color:#66d9ef">zone</span>: edge
    <span style="color:#66d9ef">from_zone</span>: edge
    <span style="color:#66d9ef">to_zone</span>: edge
    <span style="color:#66d9ef">source</span>: 
      - external_bgp_peers
    <span style="color:#66d9ef">destination</span>: any
    <span style="color:#66d9ef">action</span>: permit
    <span style="color:#66d9ef">apps</span>:
      - junos-bgp
  - <span style="color:#66d9ef">name</span>: a_ext_bgp_host
    <span style="color:#66d9ef">zone</span>: edge
    <span style="color:#66d9ef">from_zone</span>: edge
    <span style="color:#66d9ef">to_zone</span>: junos-host
    <span style="color:#66d9ef">source</span>: 
      - external_bgp_peers
    <span style="color:#66d9ef">destination</span>: any
    <span style="color:#66d9ef">action</span>: permit
    <span style="color:#66d9ef">apps</span>:
      - junos-bgp
  - <span style="color:#66d9ef">name</span>: a_int_bgp
    <span style="color:#66d9ef">zone</span>: internal
    <span style="color:#66d9ef">from_zone</span>: internal
    <span style="color:#66d9ef">to_zone</span>: internal
    <span style="color:#66d9ef">source</span>: 
      - internal_bgp_peers
    <span style="color:#66d9ef">destination</span>: any
    <span style="color:#66d9ef">action</span>: permit
    <span style="color:#66d9ef">apps</span>:
      - junos-bgp
  - <span style="color:#66d9ef">name</span>: a_int_bgp_host
    <span style="color:#66d9ef">zone</span>: internal
    <span style="color:#66d9ef">from_zone</span>: internal
    <span style="color:#66d9ef">to_zone</span>: junos-host
    <span style="color:#66d9ef">source</span>: 
      - internal_bgp_peers
    <span style="color:#66d9ef">destination</span>: any
    <span style="color:#66d9ef">action</span>: permit
    <span style="color:#66d9ef">apps</span>:
      - junos-bgp
  - <span style="color:#66d9ef">name</span>: a_internet_routing
    <span style="color:#66d9ef">zone</span>: internal
    <span style="color:#66d9ef">from_zone</span>: internal
    <span style="color:#66d9ef">to_zone</span>: internal
    <span style="color:#66d9ef">source</span>: any
    <span style="color:#66d9ef">destination</span>: any
    <span style="color:#66d9ef">action</span>: permit
    <span style="color:#66d9ef">apps</span>: any
</code></pre></div><p>This then builds the following firewall policies: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set security policies from-zone internal to-zone edge policy a_tacacs match source-address any
set security policies from-zone internal to-zone edge policy a_tacacs match destination-address netsvr-loop
set security policies from-zone internal to-zone edge policy a_tacacs match application junos-tacacs
set security policies from-zone internal to-zone edge policy a_tacacs then permit
set security policies from-zone internal to-zone edge policy a_syslog match source-address any
set security policies from-zone internal to-zone edge policy a_syslog match destination-address netsvr-direct
set security policies from-zone internal to-zone edge policy a_syslog match application junos-syslog
set security policies from-zone internal to-zone edge policy a_syslog then permit
set security policies from-zone edge to-zone edge policy a_ext_bgp match source-address external_bgp_peers
set security policies from-zone edge to-zone edge policy a_ext_bgp match destination-address any
set security policies from-zone edge to-zone edge policy a_ext_bgp match application junos-bgp
set security policies from-zone edge to-zone edge policy a_ext_bgp then permit
set security policies from-zone edge to-zone junos-host policy a_ext_bgp_host match source-address external_bgp_peers
set security policies from-zone edge to-zone junos-host policy a_ext_bgp_host match destination-address any
set security policies from-zone edge to-zone junos-host policy a_ext_bgp_host match application junos-bgp
set security policies from-zone edge to-zone junos-host policy a_ext_bgp_host then permit
set security policies from-zone internal to-zone internal policy a_int_bgp match source-address internal_bgp_peers
set security policies from-zone internal to-zone internal policy a_int_bgp match destination-address any
set security policies from-zone internal to-zone internal policy a_int_bgp match application junos-bgp
set security policies from-zone internal to-zone internal policy a_int_bgp then permit
set security policies from-zone internal to-zone internal policy a_internet_routing match source-address any
set security policies from-zone internal to-zone internal policy a_internet_routing match destination-address any
set security policies from-zone internal to-zone internal policy a_internet_routing match application any
set security policies from-zone internal to-zone internal policy a_internet_routing then permit
set security policies from-zone internal to-zone junos-host policy a_int_bgp_host match source-address internal_bgp_peers
set security policies from-zone internal to-zone junos-host policy a_int_bgp_host match destination-address any
set security policies from-zone internal to-zone junos-host policy a_int_bgp_host match application junos-bgp
set security policies from-zone internal to-zone junos-host policy a_int_bgp_host then permit
</code></pre></div><h4 id="outcomes">Outcomes</h4>
<p>While our <code>host_vars</code> are quite lengthy, we still save significantly on the amount of configuration we have to define.</p>
<p>To compare, the total JunOS configuration contains 538 different words/elements, whereas our <code>host_vars</code> contain 210 different words/elements. This will grow exponentially as more rules are added.</p>
<h4 id="verification-2">Verification</h4>
<p>Now we can verify whether the access lists are working: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show the hit count on the policies
ansible@junos-01&gt; show security policies hit-count    
Logical system: root-logical-system
 Index   From zone        To zone           Name           Policy count
 1       junos-global     junos-global      all_icmp       0            
 2       edge             junos-host        a_ext_bgp_host 0            
 3       edge             edge              a_ext_bgp      0            
 4       internal         junos-host        a_int_bgp_host 1            
 5       internal         edge              a_tacacs       2            
 6       internal         edge              a_syslog       1            
 7       internal         internal          a_int_bgp      1            
 8       internal         internal          a_internet_routing 0            

! What happens if we try to SSH from the netsvr?

$ ssh 10.100.102.253
^C

! Can we still ping it?
$ ping 10.100.102.253
PING 10.100.102.253 (10.100.102.253) 56(84) bytes of data.
64 bytes from 10.100.102.253: icmp_seq=1 ttl=64 time=12.1 ms
64 bytes from 10.100.102.253: icmp_seq=2 ttl=64 time=0.364 ms
^C
--- 10.100.102.253 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 2ms
rtt min/avg/max/mdev = 0.364/6.233/12.103/5.870 ms
</code></pre></div><p>Looks like it works!</p>
<h3 id="routing">Routing</h3>
<p>For routing, we use BGP, OSPF and OSPFv3. OSPF is used for internal IPv4 networks, OSPFv3 for internal IPv6 networks, and BGP for external routing.</p>
<h4 id="main-playbook">Main Playbook</h4>
<p>The main playbook looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for routing</span>
<span style="color:#75715e">##</span>
- <span style="color:#66d9ef">name</span>: Include OSPF routing
  <span style="color:#66d9ef">include</span>: ospf.yml

- <span style="color:#66d9ef">name</span>: Include OSPFv3 routing
  <span style="color:#66d9ef">include</span>: ospfv3.yml

- <span style="color:#66d9ef">name</span>: Include BGP routing
  <span style="color:#66d9ef">include</span>: bgp.yml
</code></pre></div><p>As before, we are separating out the BGP, OSPF and OSPFv3 playbooks. This allows us to separate out all the tasks, rather than having them in one big playbook.</p>
<h4 id="ospf-playbook">OSPF Playbook</h4>
<p>The OSPF playbook itself looks like: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for routing</span>
<span style="color:#75715e">##</span>
- <span style="color:#66d9ef">name</span>: OSPF Process - Router ID
  <span style="color:#66d9ef">junos_config</span>:
    <span style="color:#66d9ef">lines</span>: 
      - <span style="color:#e6db74">&#34;set routing-options router-id {{ router_id }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf
    - ospf_v4

- <span style="color:#66d9ef">name</span>: OSPF Interfaces
  <span style="color:#66d9ef">junos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - set protocols ospf area {{ item.ospf.area }} interface {{ item.junos_if }}.{{ item.unit }}
  <span style="color:#66d9ef">when</span>: item.ospf is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf
    - ospf_v4

- <span style="color:#66d9ef">name</span>: OSPF Interfaces - Passive
  <span style="color:#66d9ef">junos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - set protocols ospf area {{ item.ospf.area }} interface {{ item.junos_if }}.{{ item.unit }} passive 
  <span style="color:#66d9ef">when</span>: 
    - item.ospf is defined
    - item.ospf.passive is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf
    - ospf_v4
</code></pre></div><p>If you compare this to the Cisco IOS playbook, you&rsquo;ll notice that we aren&rsquo;t using the <code>parents</code> option for the <code>junos_config</code> modules. This is because within JunOS, all commands are available from the base hierarchy, rather than needing to be within certain hierarchical levels to apply certain commands.</p>
<p>While the commands themselves look more verbose, the tasks themselves require far fewer options.</p>
<p>As with IOS, there are no OSPF modules for JunOS, so we will be using <code>junos_config</code> again.</p>
<h5 id="router-id">Router ID</h5>
<p><strong>Ansible module</strong>: <code>junos_config</code></p>
<p>This task sets the router ID. One point to note here is that the router ID is global, so this applies for OSPF, OSPFv3, BGP, IS-IS and anything else that uses a router ID.</p>
<p>We source the ID from our <code>host_vars</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">router_id</span>: <span style="color:#ae81ff">192.0.2.201</span>
</code></pre></div><p>This generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set routing-options router-id 192.0.2.201
</code></pre></div><h5 id="ospf-interfaces">OSPF Interfaces</h5>
<p><strong>Ansible module</strong>: <code>junos_config</code></p>
<p>This task goes through our list of interfaces, and if the <code>ospf</code> field is defined, it enables OSPF for that interface.</p>
<p>This is defined in our <code>host_vars</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;ge-0/0/0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">102</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To netsvr&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">if_zone</span>: <span style="color:#e6db74">&#34;edge&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.100.102.253/24&#34;</span>
    <span style="color:#66d9ef">ipv6_addr</span>: <span style="color:#e6db74">&#34;2001:db8:102::f/64&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;ge-0/0/0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">202</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To junos-02&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.100.202.254/24&#34;</span>
    <span style="color:#66d9ef">ipv6_addr</span>: <span style="color:#e6db74">&#34;2001:db8:202::a/64&#34;</span>
    <span style="color:#66d9ef">if_zone</span>: <span style="color:#e6db74">&#34;internal&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;ge-0/0/1&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To the Internet&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">0</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;dhcp&#34;</span>
    <span style="color:#66d9ef">if_zone</span>: <span style="color:#e6db74">&#34;internet&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;lo0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">0</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;192.0.2.102/32&#34;</span>
    <span style="color:#66d9ef">ipv6_addr</span>: <span style="color:#e6db74">&#34;2001:db8:902:beef::1/128&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
</code></pre></div><p>Based upon the above, we would generate: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set protocols ospf area 0.0.0.0 interface ge-0/0/0.102
set protocols ospf area 0.0.0.0 interface ge-0/0/0.202
set protocols ospf area 0.0.0.0 interface ge-0/0/1.0
set protocols ospf area 0.0.0.0 interface lo0.0 
</code></pre></div><h5 id="ospf-interfaces---passive">OSPF Interfaces - Passive</h5>
<p><strong>Ansible modules</strong>: <code>junos_config</code></p>
<p>This is similar to the above, except for any interface with the <code>passive</code> field under OSPF, we also generate the passive configuration.</p>
<p>The <code>host_vars</code> relevant to this are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;ge-0/0/0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">102</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To netsvr&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">if_zone</span>: <span style="color:#e6db74">&#34;edge&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.100.102.253/24&#34;</span>
    <span style="color:#66d9ef">ipv6_addr</span>: <span style="color:#e6db74">&#34;2001:db8:102::f/64&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;ge-0/0/1&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To the Internet&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">0</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;dhcp&#34;</span>
    <span style="color:#66d9ef">if_zone</span>: <span style="color:#e6db74">&#34;internet&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;lo0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">0</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;192.0.2.102/32&#34;</span>
    <span style="color:#66d9ef">ipv6_addr</span>: <span style="color:#e6db74">&#34;2001:db8:902:beef::1/128&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
</code></pre></div><p>This would then generate the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set protocols ospf area 0.0.0.0 interface ge-0/0/0.102 passive
set protocols ospf area 0.0.0.0 interface ge-0/0/1.0 passive
set protocols ospf area 0.0.0.0 interface lo0.0 passive
</code></pre></div><h5 id="verification-3">Verification</h5>
<p>After this, we should be able to see OSPF routes on both the <strong>edge</strong> router and the <strong>internal</strong> router: -</p>
<p><strong>junos-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show OSPF interfaces
ansible@junos-01&gt; show ospf interface 
Interface           State   Area            DR ID           BDR ID          Nbrs
ge-0/0/0.102        DRother 0.0.0.0         0.0.0.0         0.0.0.0            0
ge-0/0/0.202        DR      0.0.0.0         192.0.2.102     192.0.2.202        1
ge-0/0/1.0          DRother 0.0.0.0         0.0.0.0         0.0.0.0            0
lo0.0               DRother 0.0.0.0         0.0.0.0         0.0.0.0            0

! Show OSPF neighbours
ansible@junos-01&gt; show ospf neighbor     
Address          Interface              State     ID               Pri  Dead
10.100.202.253   ge-0/0/0.202           Full      192.0.2.202      128    36

! Show routing table
ansible@junos-01&gt; show route protocol ospf   

inet.0: 13 destinations, 13 routes (13 active, 0 holddown, 0 hidden)
+ = Active Route, - = Last Active, * = Both

192.0.2.202/32     *[OSPF/10] 00:29:36, metric 1
                    &gt;  to 10.100.202.253 via ge-0/0/0.202
224.0.0.5/32       *[OSPF/10] 00:33:11, metric 1
                       MultiRecv

! Can we ping?
ansible@junos-01&gt; ping 192.0.2.202    
PING 192.0.2.202 (192.0.2.202): 56 data bytes
64 bytes from 192.0.2.202: icmp_seq=0 ttl=64 time=10.032 ms
64 bytes from 192.0.2.202: icmp_seq=1 ttl=64 time=0.562 ms
^C
--- 192.0.2.202 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.562/5.297/10.032/4.735 ms
</code></pre></div><p>Why do we have a route for <code>224.0.0.5/32</code>? OSPF uses <a href="https://en.wikipedia.org/wiki/Multicast">Multicast</a> to find OSPF neighbours on Ethernet networks, and the reserved multicast address for OSPF is <code>224.0.0.5/32</code>. Technically it is not an advertised route, but it is still a route specific to OSPF.</p>
<p><strong>junos-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show OSPF interfaces
yetiops@junos-02&gt; show ospf interface 
Interface           State   Area            DR ID           BDR ID          Nbrs
ge-0/0/0.202        BDR     0.0.0.0         192.0.2.102     192.0.2.202        1
lo0.0               DRother 0.0.0.0         0.0.0.0         0.0.0.0            0

! Show OSPF neighbours
yetiops@junos-02&gt; show ospf neighbor 
Address          Interface              State     ID               Pri  Dead
10.100.202.254   ge-0/0/0.202           Full      192.0.2.102      128    35

! Show routing table
yetiops@junos-02&gt; show route protocol ospf   

inet.0: 11 destinations, 17 routes (11 active, 0 holddown, 3 hidden)
+ = Active Route, - = Last Active, * = Both

10.100.102.0/24    *[OSPF/10] 00:33:15, metric 2
                    &gt; to 10.100.202.254 via ge-0/0/0.202
192.0.2.102/32     *[OSPF/10] 00:33:15, metric 1
                    &gt; to 10.100.202.254 via ge-0/0/0.202
192.168.122.0/24   *[OSPF/10] 00:33:15, metric 2
                    &gt; to 10.100.202.254 via ge-0/0/0.202
224.0.0.5/32       *[OSPF/10] 00:35:11, metric 1
                      MultiRecv

! Can we ping?
yetiops@junos-02&gt; ping 192.0.2.102 
PING 192.0.2.102 (192.0.2.102): 56 data bytes
64 bytes from 192.0.2.102: icmp_seq=0 ttl=64 time=22.991 ms
64 bytes from 192.0.2.102: icmp_seq=1 ttl=64 time=44.280 ms
^C
--- 192.0.2.102 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 22.991/33.636/44.280/10.645 ms

yetiops@junos-02&gt; ping 10.100.102.253 
PING 10.100.102.253 (10.100.102.253): 56 data bytes
64 bytes from 10.100.102.253: icmp_seq=0 ttl=64 time=23.840 ms
64 bytes from 10.100.102.253: icmp_seq=1 ttl=64 time=1.241 ms
^C
--- 10.100.102.253 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 1.241/12.540/23.840/11.300 ms
</code></pre></div><p>It&rsquo;s all looking good!</p>
<h4 id="ospfv3-playbook">OSPFv3 Playbook</h4>
<p>As noted, we are using OSPFv3 for IPv6 internal routing. We could also use it for IPv4, but not every vendors supports this option. We will use it for IPv6 only for now (for interoperability reasons).</p>
<p>The playbook for OSPFv3 has fewer tasks that for OSPF, as we are not settings the router ID as part of it.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for routing</span>
<span style="color:#75715e">##</span>

- <span style="color:#66d9ef">name</span>: OSPFv3 Interfaces
  <span style="color:#66d9ef">junos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - set protocols ospf3 area {{ item.ospf.area }} interface {{ item.junos_if }}.{{ item.unit }} 
  <span style="color:#66d9ef">when</span>: item.ospfv3 is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf
    - ospf_v6

- <span style="color:#66d9ef">name</span>: OSPFv3 Interfaces - Passive
  <span style="color:#66d9ef">junos_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - set protocols ospf3 area {{ item.ospf.area }} interface {{ item.junos_if }}.{{ item.unit }} passive
  <span style="color:#66d9ef">when</span>: 
    - item.ospfv3 is defined
    - item.ospfv3.passive is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - ospf
    - ospf_v6
</code></pre></div><p>The tasks are fundamentally the same as the tasks for enabling OSPF. The only major difference is that we use <code>ospf3</code> instead of <code>ospf</code> in the commands.</p>
<p>The <code>host_vars</code> we used are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;ge-0/0/0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">102</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To netsvr&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">if_zone</span>: <span style="color:#e6db74">&#34;edge&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.100.102.253/24&#34;</span>
    <span style="color:#66d9ef">ipv6_addr</span>: <span style="color:#e6db74">&#34;2001:db8:102::f/64&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;ge-0/0/0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">202</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To junos-02&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.100.202.254/24&#34;</span>
    <span style="color:#66d9ef">ipv6_addr</span>: <span style="color:#e6db74">&#34;2001:db8:202::a/64&#34;</span>
    <span style="color:#66d9ef">if_zone</span>: <span style="color:#e6db74">&#34;internal&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;lo0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">0</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">if_zone</span>: <span style="color:#e6db74">&#34;internal&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;192.0.2.102/32&#34;</span>
    <span style="color:#66d9ef">ipv6_addr</span>: <span style="color:#e6db74">&#34;2001:db8:902:beef::1/128&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
</code></pre></div><p>The following configuration is generated: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set protocols ospf3 area 0.0.0.0 interface ge-0/0/0.102 passive
set protocols ospf3 area 0.0.0.0 interface ge-0/0/0.202
set protocols ospf3 area 0.0.0.0 interface lo0.0 passive
</code></pre></div><h5 id="verification-4">Verification</h5>
<p>We&rsquo;ll follow the same steps as we did for OSPF: -</p>
<p><strong>junos-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show OSPFv3 interfaces
ansible@junos-01&gt; show ospf3 interface   
Interface           State   Area            DR ID           BDR ID          Nbrs
ge-0/0/0.102        DR      0.0.0.0         192.0.2.102     0.0.0.0            0
ge-0/0/0.202        DR      0.0.0.0         192.0.2.102     0.0.0.0            0
lo0.0               DR      0.0.0.0         192.0.2.102     0.0.0.0            0

! Show OSPFv3 neighbours
ansible@junos-01&gt; show ospf3 neighbor     
ID               Interface              State     Pri   Dead
192.0.2.202      ge-0/0/0.202           Full      128     38
  Neighbor-address fe80::5254:0:cac2:2ecb

! Show routing table
ansible@junos-01&gt; show route protocol ospf3 

inet.0: 13 destinations, 13 routes (13 active, 0 holddown, 0 hidden)

inet6.0: 12 destinations, 14 routes (12 active, 0 holddown, 0 hidden)
+ = Active Route, - = Last Active, * = Both

2001:db8:902:beef::2/128
                   *[OSPF3/10] 00:01:31, metric 1
                    &gt;  to fe80::5254:0:cac2:2ecb via ge-0/0/0.202
ff02::5/128        *[OSPF3/10] 00:05:08, metric 1
                       MultiRecv

! Ping!
ansible@junos-01&gt; ping 2001:db8:902:beef::2                                
PING6(56=40+8+8 bytes) 2001:db8:202::a --&gt; 2001:db8:902:beef::2
16 bytes from 2001:db8:902:beef::2, icmp_seq=0 hlim=64 time=23.113 ms
16 bytes from 2001:db8:902:beef::2, icmp_seq=1 hlim=64 time=0.901 ms
^C
--- 2001:db8:902:beef::2 ping6 statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/std-dev = 0.901/12.007/23.113/11.106 ms
</code></pre></div><p>As in OSPF, we also have a multicast address for OSPFv3. In this case, it is <code>ff02::5/128</code>. Again, this is not advertised by OSPFv3 but is specific to the protocol.</p>
<p><strong>junos-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show OSPFv3 interfaces
ansible@junos-02&gt; show ospf3 interface 
Interface           State   Area            DR ID           BDR ID          Nbrs
ge-0/0/0.202        DR      0.0.0.0         192.0.2.202     192.0.2.102        1
lo0.0               DRother 0.0.0.0         0.0.0.0         0.0.0.0            0

! Show OSPFv3 neighbours
ansible@junos-02&gt; show ospf3 neighbor 
ID               Interface              State     Pri   Dead
192.0.2.102      ge-0/0/0.202           Full      128     32
  Neighbor-address fe80::5254:0:cafe:ea97

! Show routing table
ansible@junos-02&gt; show route protocol ospf3 

inet.0: 11 destinations, 17 routes (11 active, 0 holddown, 3 hidden)

inet6.0: 10 destinations, 14 routes (10 active, 0 holddown, 2 hidden)
+ = Active Route, - = Last Active, * = Both

2001:db8:102::/64  *[OSPF3/10] 00:07:03, metric 2
                    &gt;  to fe80::5254:0:cafe:ea97 via ge-0/0/0.202
2001:db8:902:beef::1/128
                   *[OSPF3/10] 00:07:03, metric 1
                    &gt;  to fe80::5254:0:cafe:ea97 via ge-0/0/0.202
ff02::5/128        *[OSPF3/10] 00:13:01, metric 1
                       MultiRecv

! Ping!
ansible@junos-02&gt; ping 2001:db8:902:beef::1    
PING6(56=40+8+8 bytes) 2001:db8:202::f --&gt; 2001:db8:902:beef::1
16 bytes from 2001:db8:902:beef::1, icmp_seq=0 hlim=64 time=0.585 ms
16 bytes from 2001:db8:902:beef::1, icmp_seq=1 hlim=64 time=17.187 ms
^C
--- 2001:db8:902:beef::1 ping6 statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/std-dev = 0.585/8.886/17.187/8.301 ms

ansible@junos-02&gt; ping 2001:db8:102::f       
PING6(56=40+8+8 bytes) 2001:db8:202::f --&gt; 2001:db8:102::f
16 bytes from 2001:db8:102::f, icmp_seq=0 hlim=64 time=12.074 ms
16 bytes from 2001:db8:102::f, icmp_seq=1 hlim=64 time=11.933 ms
16 bytes from 2001:db8:102::f, icmp_seq=2 hlim=64 time=1.510 ms
^C
--- 2001:db8:102::f ping6 statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max/std-dev = 1.510/8.506/12.074/4.947 ms
</code></pre></div><h4 id="bgp-playbook">BGP Playbook</h4>
<p>The BGP playbook is where we configure our internal and external BGP peers. Unlike Cisco IOS, there are no JunOS Ansible modules for BGP. Instead, we must use the <code>junos_config</code> module.</p>
<p>We are also applying prefix lists and route policies to only allow certain routes to be passed. In day to day BGP configuration, you would configure these all the time, so it makes sense to include tasks to configure them.</p>
<p>The playbook itself looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
- <span style="color:#66d9ef">name</span>: Configure Prefix Lists 
  <span style="color:#66d9ef">junos_config</span>:
    <span style="color:#66d9ef">src</span>: prefixlists.j2
  <span style="color:#66d9ef">when</span>:
    - route_policies is defined
    - route_policies.prefix_lists is defined
  <span style="color:#66d9ef">tags</span>:
    - bgp
    - bgp_v4
    - bgp_v6

- <span style="color:#66d9ef">name</span>: Configure Route Policies 
  <span style="color:#66d9ef">junos_config</span>:
    <span style="color:#66d9ef">src</span>: policies.j2
  <span style="color:#66d9ef">when</span>:
    - route_policies is defined
    - route_policies.rp is defined
  <span style="color:#66d9ef">tags</span>:
    - bgp
    - bgp_v4
    - bgp_v6

- <span style="color:#66d9ef">name</span>: Configure BGP Peers
  <span style="color:#66d9ef">junos_config</span>:
    <span style="color:#66d9ef">src</span>: bgp_group.j2
  <span style="color:#66d9ef">when</span>:
    - bgp is defined
  <span style="color:#66d9ef">tags</span>:
    - bgp
    - bgp_v4
    - bgp_v6
</code></pre></div><p>What is interesting here is that we do not need to separate our IPv4 and IPv6 configuration. Rather than requiring configuration in different address families (i.e. Cisco IOS style), JunOS allows you to configure IPv4 and IPv6 within the same group and same hierarchical level.</p>
<h5 id="prefix-lists">Prefix Lists</h5>
<p><strong>Ansible module</strong>: <code>junos_config</code></p>
<p>Prefix lists are used to match a range of IP addresses. They can be exact matches (i.e. <code>192.168.0.0/24</code>), or you can match on a longer or shorter prefix length.</p>
<p>As an example, you can say that if the routes received have a subnet mask of a <strong>/24</strong> or longer (i.e. /25, /26, /27 etc), then accept them. You would tend to use something like this when accepting routes from internal routers, or customer connections.</p>
<p>If you are configuring transit (i.e. receiving the full Internet routing table from a provider) or peering (e.g. an Internet Exchange like <a href="https://linx.net">LINX</a> or <a href="https://www.ams-ix.net/ams">AMS-IX</a>) connections, then you would likely only advertise routes that are <code>/24</code> or shorter (i.e. /24, /23, /22 etc). This is because most transit providers do not accept more specific routes than this, to ensure that the global routing table is still a manageable size. If transit providers accepted everything down to <code>/32</code> routes, the internet routing table would be orders of magnitude larger than it is. It is around 800,000 routes (at the time of writing this, which is already too large for some older (yet still widely used) routers.</p>
<p>The configuration template looks like the below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% for prefix_list in route_policies['prefix_lists'] %}
delete policy-options prefix-list {{ prefix_list['name'] }}
{% for address in prefix_list['addresses'] %}
set policy-options prefix-list {{ prefix_list['name'] }} {{ address }}
{% endfor %}
set policy-options prefix-list {{ prefix_list['name'] }}_v4 apply-path &quot;policy-options prefix-list {{ prefix_list['name'] }} &lt;[0-9]*.[0-9]*.[0-9]*.[0-9]*&gt;&quot;
set policy-options prefix-list {{ prefix_list['name'] }}_v6 apply-path &quot;policy-options prefix-list {{ prefix_list['name'] }} &lt;*:*:*&gt;&quot;
{% endfor %}
</code></pre><p>First we delete the existing prefix-list. This allows us to rebuild it with the correct routes in it, rather than adding more and then manually removing old entries.</p>
<p>After that, we loop through our list of addresses, and add them to our prefix-list.</p>
<p>The next two lines create two dynamic prefix-lists, in that any IPv4 entries will create a prefix list of <code>$NAME_v4</code>, and any IPv6 entries will create a prefix list of <code>$NAME_v6</code>. Rather than needing to create a separate prefix list ourselves, we can make use of JunOS feature, in this case the <code>apply-path</code> command (and some regular expressions), to build them for us.</p>
<p>The prefixes are sourced from our <code>host_vars</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">route_policies</span>:
  <span style="color:#66d9ef">prefix_lists</span>:
    - <span style="color:#66d9ef">name</span>: internal_nets
      <span style="color:#66d9ef">addresses</span>:
         - <span style="color:#ae81ff">192.0.2.102</span>/<span style="color:#ae81ff">32</span>
         - <span style="color:#ae81ff">192.0.2.202</span>/<span style="color:#ae81ff">32</span>
         - <span style="color:#ae81ff">10.100.202.0</span>/<span style="color:#ae81ff">24</span>
         - <span style="color:#e6db74">&#34;2001:db8:902:beef::1/128&#34;</span>
         - <span style="color:#e6db74">&#34;2001:db8:902:beef::2/128&#34;</span>
         - <span style="color:#e6db74">&#34;2001:db8:902::/64&#34;</span>
    - <span style="color:#66d9ef">name</span>: external_nets
      <span style="color:#66d9ef">addresses</span>:
         - <span style="color:#ae81ff">192.0.2.1</span>/<span style="color:#ae81ff">32</span>
         - <span style="color:#e6db74">&#34;2001:db8:999:beef::1/128&#34;</span>
         - <span style="color:#ae81ff">10.100.102.0</span>/<span style="color:#ae81ff">24</span>
</code></pre></div><p>The generated configuration would therefore look like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set policy-options prefix-list internal_nets_v4 apply-path &#34;policy-options prefix-list internal_nets &lt;[0-9]*.[0-9]*.[0-9]*.[0-9]*&gt;&#34;
set policy-options prefix-list internal_nets_v6 apply-path &#34;policy-options prefix-list internal_nets &lt;*:*:*&gt;&#34;
set policy-options prefix-list external_nets_v4 apply-path &#34;policy-options prefix-list external_nets &lt;[0-9]*.[0-9]*.[0-9]*.[0-9]*&gt;&#34;
set policy-options prefix-list external_nets_v6 apply-path &#34;policy-options prefix-list external_nets &lt;*:*:*&gt;&#34;
set policy-options prefix-list internal_nets 10.100.202.0/24
set policy-options prefix-list internal_nets 192.0.2.102/32
set policy-options prefix-list internal_nets 192.0.2.202/32
set policy-options prefix-list internal_nets 2001:db8:902::/64
set policy-options prefix-list internal_nets 2001:db8:902:beef::1/128
set policy-options prefix-list internal_nets 2001:db8:902:beef::2/128
set policy-options prefix-list external_nets 10.100.102.0/24
set policy-options prefix-list external_nets 192.0.2.1/32
set policy-options prefix-list external_nets 2001:db8:999:beef::1/128
</code></pre></div><h5 id="route-policies">Route Policies</h5>
<p><strong>Ansible module</strong>: <code>junos_config</code></p>
<p>Route policies are used to apply our chosen actions to the routes advertised to or received from our BGP neighbours. They can also be used with other protocols too, but for our purposes we are using them specifically with BGP.</p>
<p>Our template looks like the below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% for policy in route_policies['rp'] %}
delete policy-options policy-statement {{ policy['name'] }}
{% if policy['from']['protocols'] is defined and policy['from']['pfx_list'] is not defined %}
{% for protocol in policy['from']['protocols'] %}
set policy-options policy-statement {{ policy['name'] }} from protocol {{ protocol }}
set policy-options policy-statement {{ policy['name'] }} then {{ policy['action'] }}
{% endfor %}
{% endif %}
{% if policy['from']['pfx_list'] is defined %}
{% if policy['from']['protocols'] is defined %}
{% for protocol in policy['from']['protocols'] %}
set policy-options policy-statement {{ policy['name'] }} term v4 from protocol {{ protocol }}
{% endfor %}
{% endif %}
set policy-options policy-statement {{ policy['name'] }} term v4 from prefix-list {{ policy['from']['pfx_list'] }}_v4
set policy-options policy-statement {{ policy['name'] }} term v4 then {{ policy['action'] }}
{% if policy['from']['protocols'] is defined %}
{% for protocol in policy['from']['protocols'] %}
set policy-options policy-statement {{ policy['name'] }} term v6 from protocol {{ protocol }}
{% endfor %}
{% endif %}
set policy-options policy-statement {{ policy['name'] }} term v6 from prefix-list {{ policy['from']['pfx_list'] }}_v6
set policy-options policy-statement {{ policy['name'] }} term v6 then {{ policy['action'] }}
{% endif %}
{% if policy['from']['protocols'] is not defined and policy['from']['pfx_list'] is not defined %}
set policy-options policy-statement {{ policy['name'] }} then {{ policy['action'] }}
{% endif %}
{% endfor %}
</code></pre><p>There is a lot happening in this template. First, we go through our list of route policies (in our <code>host_vars</code>) and delete the existing policy (to remove old/outdated configuration).</p>
<p>Once this is done, we generate the new policies. Our options are: -</p>
<ul>
<li>No prefix-lists matched and matching a protocol (e.g. <strong>OSPF</strong>, <strong>IS-IS</strong>)
<ul>
<li>We can also match against <strong>static</strong> (static routes) or <strong>direct</strong> (directly connected networks)</li>
</ul>
</li>
<li>Prefix-lists matched and matching a protocol</li>
<li>Prefix-lists matched and not matching a protocol</li>
<li>No protocols or prefix-lists defined</li>
</ul>
<p>What this means is that our policies can do the following: -</p>
<ul>
<li>Import all routes from another protocol</li>
<li>Import some routes from another protocol, using prefix lists to limit which routes</li>
<li>Import routes solely based upon a prefix list</li>
<li>Purely allow all routes or deny all routes</li>
</ul>
<p>Also, we apply terms (i.e. different sections of the policy) for IPv4 and IPv6. If you attempt to apply route policy without these to an IPv4 peer, then it will fail as the policy also containers IPv6 prefix-lists. The same applies for an IPv6 peer when a route policy containers IPv4 prefix-lists. Using the <code>term v4</code> and <code>term v6</code> statements allows the correct prefix-lists to be applied to the correct peers.</p>
<p>To help explain, we will look at our <code>host_vars</code>, and then go through each policy that is generated.</p>
<p><strong>junos-01 host_vars</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">route_policies</span>:
  <span style="color:#66d9ef">rp</span>:
    - <span style="color:#66d9ef">name</span>: external_networks
      <span style="color:#66d9ef">from</span>:
        <span style="color:#66d9ef">protocols</span>:
        - bgp
        <span style="color:#66d9ef">pfx_list</span>: external_nets
      <span style="color:#66d9ef">action</span>: accept
    - <span style="color:#66d9ef">name</span>: internal_networks
      <span style="color:#66d9ef">from</span>:
        <span style="color:#66d9ef">protocols</span>:
        - direct
        - ospf
        - ospf3
        <span style="color:#66d9ef">pfx_list</span>: internal_nets
      <span style="color:#66d9ef">action</span>: accept
    - <span style="color:#66d9ef">name</span>: dhcp_default
      <span style="color:#66d9ef">from</span>:
        <span style="color:#66d9ef">protocols</span>:
        - access-internal
      <span style="color:#66d9ef">action</span>: accept
    - <span style="color:#66d9ef">name</span>: deny-all
      <span style="color:#66d9ef">action</span>: reject
</code></pre></div><p>For the policy named <code>external_networks</code>, if the routes are received from BGP, and match the prefix-list <code>external_nets</code>, then the routes are accepted.</p>
<p>For the policy named <code>internal_networks</code>, routes from OSPF, OSPFv3 and directly connected networks are accepted that match the <code>internal_nets</code> prefix list.</p>
<p>For the policy named <code>dhcp_default</code>, all routes from the protocol <strong>access-internal</strong> are accepted. This is the protocol JunOS uses when a default route is received via DHCP.</p>
<p>For the policy <code>deny-all</code>, all routes are rejected, no matter the protocol or prefix.</p>
<p>The actual configuration that is generated looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set policy-options policy-statement deny-all then reject
set policy-options policy-statement dhcp_default from protocol access-internal
set policy-options policy-statement dhcp_default then accept
set policy-options policy-statement external_networks term v4 from protocol bgp
set policy-options policy-statement external_networks term v4 from prefix-list external_nets_v4
set policy-options policy-statement external_networks term v4 then accept
set policy-options policy-statement external_networks term v6 from protocol bgp
set policy-options policy-statement external_networks term v6 from prefix-list external_nets_v6
set policy-options policy-statement external_networks term v6 then accept
set policy-options policy-statement internal_networks term v4 from protocol direct
set policy-options policy-statement internal_networks term v4 from protocol ospf
set policy-options policy-statement internal_networks term v4 from protocol ospf3
set policy-options policy-statement internal_networks term v4 from prefix-list internal_nets_v4
set policy-options policy-statement internal_networks term v4 then accept
set policy-options policy-statement internal_networks term v6 from protocol direct
set policy-options policy-statement internal_networks term v6 from protocol ospf
set policy-options policy-statement internal_networks term v6 from protocol ospf3
set policy-options policy-statement internal_networks term v6 from prefix-list internal_nets_v6
set policy-options policy-statement internal_networks term v6 then accept
</code></pre></div><h5 id="configuring-bgp-peers">Configuring BGP peers</h5>
<p><strong>Ansible module</strong>: <code>junos_config</code></p>
<p>This task creates the BGP peers. JunOS requires that all peers are part of a group. This differs from IOS, in that Cisco does allow you to configure <strong>peer groups</strong>, but a BGP peer does not have to be part of one.</p>
<p>The template used to generate the peers is below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">set protocols bgp local-as {{ bgp['local_as'] }}
{% for group in bgp['groups'] %}
delete protocols bgp group {{ group['name'] }}
set protocols bgp group {{ group['name'] }} type {{ group['type'] }}
set protocols bgp group {{ group['name'] }} description &quot;{{ group['desc'] }}&quot;
set protocols bgp group {{ group['name'] }} hold-time {{ group['hold'] }}
set protocols bgp group {{ group['name'] }} log-updown
{% for policy in group['policies']['import'] %}
set protocols bgp group {{ group['name'] }} import {{ policy }}
{% endfor %}
{% for policy in group['policies']['export'] %}
set protocols bgp group {{ group['name'] }} export {{ policy }}
{% endfor %}
{% for neighbour in group['neighbours'] %}
{% if 'external' in group['type'] %}
set protocols bgp group {{ group['name'] }} neighbor {{ neighbour['peer'] }} description &quot;{{ neighbour['desc'] }}&quot;
set protocols bgp group {{ group['name'] }} neighbor {{ neighbour['peer'] }} peer-as {{ neighbour['remote_as'] }}
{% else %}
set protocols bgp group {{ group['name'] }} neighbor {{ neighbour['peer'] }} description &quot;{{ neighbour['desc'] }}&quot;
set protocols bgp group {{ group['name'] }} neighbor {{ neighbour['peer'] }} local-address &quot;{{ neighbour['loc_ip'] }}&quot;
{% endif %}
{% endfor %}
{% endfor %}
</code></pre><p>With this, we are setting our local autonomous system, building a group (and setting up our base parameters), and then add our peers to it. This is also where we apply our <strong>import</strong> and <strong>export</strong> policies. We then will add our BGP peers, and will specify either the neighbour&rsquo;s autonomous system number (if it is an external neighbour) or the address that the BGP session is formed from (e.g. from our loopback IP address).</p>
<p>The below <code>host_vars</code> are used for this: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">bgp</span>:
  <span style="color:#66d9ef">local_as</span>: <span style="color:#ae81ff">65102</span>
  <span style="color:#66d9ef">groups</span>:
    - <span style="color:#66d9ef">name</span>: NETSVR
      <span style="color:#66d9ef">type</span>: external
      <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Peering to NETSVR&#34;</span>
      <span style="color:#66d9ef">hold</span>: <span style="color:#ae81ff">30</span>
      <span style="color:#66d9ef">policies</span>:
        <span style="color:#66d9ef">import</span>:
          - external_networks
          - deny-all
        <span style="color:#66d9ef">export</span>:
          - internal_networks
          - deny-all
      <span style="color:#66d9ef">neighbours</span>:
        - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">10.100.102.254</span>
          <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65430</span>
          <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;netsvr-01 IPv4&#34;</span>
        - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:102::ffff&#34;</span>
          <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65430</span>
          <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;netsvr-01 IPv6&#34;</span>
    - <span style="color:#66d9ef">name</span>: IBGP
      <span style="color:#66d9ef">type</span>: internal
      <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;IBGP BGP Peering&#34;</span>
      <span style="color:#66d9ef">hold</span>: <span style="color:#ae81ff">30</span>
      <span style="color:#66d9ef">policies</span>:
        <span style="color:#66d9ef">export</span>:
          - dhcp_default
          - external_networks
          - deny-all
        <span style="color:#66d9ef">import</span>:
          - internal_networks
          - deny-all
      <span style="color:#66d9ef">neighbours</span>:
        - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">192.0.2.202</span>
          <span style="color:#66d9ef">loc_ip</span>: <span style="color:#ae81ff">192.0.2.102</span>
          <span style="color:#66d9ef">default_originate</span>: <span style="color:#66d9ef">true</span>
          <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;junos-02 IPv4&#34;</span>
        - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:902:beef::2&#34;</span>
          <span style="color:#66d9ef">loc_ip</span>: <span style="color:#e6db74">&#34;2001:db8:902:beef::1&#34;</span>
          <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;junos-02 IPv6&#34;</span>
</code></pre></div><p>This will then generate the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set protocols bgp group NETSVR type external
set protocols bgp group NETSVR description &#34;Peering to NETSVR&#34;
set protocols bgp group NETSVR hold-time 30
set protocols bgp group NETSVR log-updown
set protocols bgp group NETSVR import external_networks
set protocols bgp group NETSVR import deny-all
set protocols bgp group NETSVR export internal_networks
set protocols bgp group NETSVR export deny-all
set protocols bgp group NETSVR neighbor 10.100.102.254 description &#34;netsvr-01 IPv4&#34;
set protocols bgp group NETSVR neighbor 10.100.102.254 peer-as 65430
set protocols bgp group NETSVR neighbor 2001:db8:102::ffff description &#34;netsvr-01 IPv6&#34;
set protocols bgp group NETSVR neighbor 2001:db8:102::ffff peer-as 65430
set protocols bgp group IBGP type internal
set protocols bgp group IBGP description &#34;IBGP BGP Peering&#34;
set protocols bgp group IBGP hold-time 30
set protocols bgp group IBGP log-updown
set protocols bgp group IBGP import internal_networks
set protocols bgp group IBGP import deny-all
set protocols bgp group IBGP export dhcp_default
set protocols bgp group IBGP export external_networks
set protocols bgp group IBGP export deny-all
set protocols bgp group IBGP neighbor 192.0.2.202 description &#34;junos-02 IPv4&#34;
set protocols bgp group IBGP neighbor 192.0.2.202 local-address 192.0.2.102
set protocols bgp group IBGP neighbor 2001:db8:902:beef::2 description &#34;junos-02 IPv6&#34;
set protocols bgp group IBGP neighbor 2001:db8:902:beef::2 local-address 2001:db8:902:beef::1
set protocols bgp local-as 65102
</code></pre></div><p>As noted, we do not need to configure our IPv4 and IPv6 neighbours in different address families or sections of configuration. They can all be configure within the same group.</p>
<h6 id="redistribution">Redistribution</h6>
<p>A point to note with JunOS is that redistribution from another protocol works entirely differently from Cisco IOS. In Cisco IOS, redistribution imports routes from one protocol into another (say, importing your OSPF routes into BGP).</p>
<p>This is not the case for JunOS. Instead, when you apply an <strong>export</strong> policy, it is applied to the routes in your routing table. Those that match the policy are then advertised to the BGP neighbours in the group.</p>
<p>What this means is that you can selectively advertise routes to peers, rather than in IOS where you would be redistributing the routes from another protocol, and then filtering what each neighbour advertises.</p>
<p>Because of this, we do not need to add specific configuration for redistribution (as we do in Cisco IOS).</p>
<h5 id="verification-5">Verification</h5>
<p>After all of the above has run, we should have BGP sessions up over IPv4 and IPv6, as well as routes received and sent to <strong>netsvr-01</strong> BGP route server.</p>
<p><strong>junos-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show BGP neighbours on IPv4 and IPv6
ansible@junos-01&gt; show bgp summary
Threading mode: BGP I/O
Groups: 2 Peers: 4 Down peers: 0
Table          Tot Paths  Act Paths Suppressed    History Damp State    Pending
inet.0
                       1          1          0          0          0          0
inet6.0
                       1          1          0          0          0          0
Peer                     AS      InPkt     OutPkt    OutQ   Flaps Last Up/Dwn State|#Active/Received/Accepted/Damped...
10.100.102.254        65430        302        330       0       0       49:31 Establ
  inet.0: 1/1/1/0
192.0.2.202           65102        327        328       0       0       48:46 Establ
  inet.0: 0/0/0/0
2001:db8:102::ffff       65430        301        329       0       0       49:24 Establ
  inet6.0: 1/1/1/0
2001:db8:902:beef::2       65102        325        322       0       0       48:00 Establ
  inet6.0: 0/0/0/0

! Show BGP routes
ansible@junos-01&gt; show route protocol bgp

inet.0: 13 destinations, 13 routes (13 active, 0 holddown, 0 hidden)
+ = Active Route, - = Last Active, * = Both

192.0.2.1/32       *[BGP/170] 00:50:08, MED 0, localpref 100
                      AS path: 65430 I, validation-state: unverified
                    &gt;  to 10.100.102.254 via ge-0/0/0.102

inet6.0: 12 destinations, 12 routes (12 active, 0 holddown, 0 hidden)
+ = Active Route, - = Last Active, * = Both

2001:db8:999:beef::1/128
                   *[BGP/170] 00:50:01, MED 0, localpref 100
                      AS path: 65430 I, validation-state: unverified
                    &gt;  to 2001:db8:102::ffff via ge-0/0/0.102

! Ping the netsvr Loopback (192.0.2.1 and 2001:DB8:999:BEEF::1)
ansible@junos-01&gt; ping 192.0.2.1
PING 192.0.2.1 (192.0.2.1): 56 data bytes
64 bytes from 192.0.2.1: icmp_seq=0 ttl=64 time=0.963 ms
64 bytes from 192.0.2.1: icmp_seq=1 ttl=64 time=0.556 ms
^C
--- 192.0.2.1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.556/0.760/0.963/0.203 ms

ansible@junos-01&gt; ping 2001:db8:999:beef::1
PING6(56=40+8+8 bytes) 2001:db8:102::f --&gt; 2001:db8:999:beef::1
16 bytes from 2001:db8:999:beef::1, icmp_seq=0 hlim=64 time=0.934 ms
16 bytes from 2001:db8:999:beef::1, icmp_seq=1 hlim=64 time=0.542 ms
16 bytes from 2001:db8:999:beef::1, icmp_seq=2 hlim=64 time=0.526 ms
^C
--- 2001:db8:999:beef::1 ping6 statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max/std-dev = 0.526/0.667/0.934/0.189 ms
</code></pre></div><p><strong>junos-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show BGP neighbours on IPv4 and IPv6
ansible@junos-02&gt; show bgp summary
Threading mode: BGP I/O
Groups: 1 Peers: 2 Down peers: 0
Table          Tot Paths  Act Paths Suppressed    History Damp State    Pending
inet.0
                       2          2          0          0          0          0
inet6.0
                       1          1          0          0          0          0
Peer                     AS      InPkt     OutPkt    OutQ   Flaps Last Up/Dwn State|#Active/Received/Accepted/Damped...
192.0.2.102           65102         11          8       0       0          59 Establ
  inet.0: 2/2/2/0
2001:db8:902:beef::1       65102          5          2       0       0          13 Establ
  inet6.0: 1/1/1/0

! Show BGP routes
ansible@junos-02&gt; show route protocol bgp

inet.0: 11 destinations, 11 routes (11 active, 0 holddown, 0 hidden)
+ = Active Route, - = Last Active, * = Both

0.0.0.0/0          *[BGP/170] 00:01:21, MED 0, localpref 100, from 192.0.2.102
                      AS path: I, validation-state: unverified
                    &gt;  to 10.100.202.254 via ge-0/0/0.202
192.0.2.1/32       *[BGP/170] 00:01:21, MED 0, localpref 100, from 192.0.2.102
                      AS path: 65430 I, validation-state: unverified
                    &gt;  to 10.100.202.254 via ge-0/0/0.202

inet6.0: 10 destinations, 10 routes (10 active, 0 holddown, 0 hidden)
+ = Active Route, - = Last Active, * = Both

2001:db8:999:beef::1/128
                   *[BGP/170] 00:00:35, MED 0, localpref 100, from 2001:db8:902:beef::1
                      AS path: 65430 I, validation-state: unverified
                    &gt;  to fe80::5254:0:cafe:ea97 via ge-0/0/0.202

! Ping the netsvr Loopback (192.0.2.1 and 2001:DB8:999:BEEF::1)
ansible@junos-02&gt; ping 192.0.2.1
PING 192.0.2.1 (192.0.2.1): 56 data bytes
64 bytes from 192.0.2.1: icmp_seq=0 ttl=63 time=1.764 ms
64 bytes from 192.0.2.1: icmp_seq=1 ttl=63 time=0.678 ms
64 bytes from 192.0.2.1: icmp_seq=2 ttl=63 time=0.732 ms
^C
--- 192.0.2.1 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.678/1.058/1.764/0.500 ms

ansible@junos-02&gt; ping 2001:db8:999:beef::1 source 2001:db8:902:beef::2
PING6(56=40+8+8 bytes) 2001:db8:902:beef::2 --&gt; 2001:db8:999:beef::1
16 bytes from 2001:db8:999:beef::1, icmp_seq=0 hlim=63 time=2.212 ms
16 bytes from 2001:db8:999:beef::1, icmp_seq=1 hlim=63 time=2.049 ms
16 bytes from 2001:db8:999:beef::1, icmp_seq=2 hlim=63 time=0.875 ms
^C
--- 2001:db8:999:beef::1 ping6 statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max/std-dev = 0.875/1.712/2.212/0.596 ms
</code></pre></div><p>All looking good!</p>
<h3 id="snmp">SNMP</h3>
<p>In this section, we enable SNMP, so that we can monitor the routers. Again, no native Ansible module exists, so we are using the <code>junos_config</code> module.</p>
<h4 id="playbook-3">Playbook</h4>
<p>The contents of the playbook are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for snmp</span>
- <span style="color:#66d9ef">name</span>: Enable SNMPv3
  <span style="color:#66d9ef">junos_config</span>:
    <span style="color:#66d9ef">src</span>: snmpv3.j2
  <span style="color:#66d9ef">tags</span>:
    - snmp
</code></pre></div><h4 id="template">Template</h4>
<p>The template for this module looks like the below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">delete snmp
set snmp contact &quot;{{ snmp['contact'] }}&quot;
set snmp location &quot;{{ snmp['location'] }}&quot;
set snmp v3 usm local-engine user {{ snmp['user'] }} authentication-sha authentication-password {{ snmp['auth_key'] }}
set snmp v3 usm local-engine user {{ snmp['user'] }} privacy-aes128 privacy-password {{ snmp['priv_key'] }}
set snmp v3 vacm security-to-group security-model usm security-name {{ snmp['user'] }} group {{ snmp['group'] }}
set snmp v3 vacm access group {{ snmp['group'] }} default-context-prefix security-model any security-level authentication read-view all
set snmp v3 vacm access group {{ snmp['group'] }} default-context-prefix security-model any security-level authentication write-view all
set snmp view all oid .1
</code></pre><p>We don&rsquo;t need to use any loops for this, so the template is almost identical to the generated configuration (other than some variables that will be replaced). If you wanted more than one SNMPv3 user and/or group, then you would need to add loops to this.</p>
<p>We use <code>group_vars</code> for this, rather than <code>host_vars</code>, as the same SNMPv3 user is used on both the <strong>edge</strong> router and the <strong>internal</strong> router</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">snmp</span>:
  <span style="color:#66d9ef">location</span>: Yeti Home
  <span style="color:#66d9ef">contact</span>: The Hairy One
  <span style="color:#66d9ef">user</span>: yetiops
  <span style="color:#66d9ef">group</span>: yetiops_group
  <span style="color:#66d9ef">auth_key</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">            $ANSIBLE_VAULT;1.1;AES256</span>
            <span style="color:#ae81ff">383</span><span style="color:#75715e">###REDACTED###############################################################566</span>
            <span style="color:#ae81ff">363</span><span style="color:#75715e">###REDACTED###############################################################366</span>
            <span style="color:#ae81ff">343</span><span style="color:#75715e">###REDACTED###############################################################565</span>
            <span style="color:#ae81ff">326</span><span style="color:#75715e">###REDACTED###############################################################362</span>
            <span style="color:#ae81ff">3431</span>
  <span style="color:#66d9ef">priv_key</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">            $ANSIBLE_VAULT;1.1;AES256</span>
            <span style="color:#ae81ff">383</span><span style="color:#75715e">###REDACTED###############################################################566</span>
            <span style="color:#ae81ff">363</span><span style="color:#75715e">###REDACTED###############################################################366</span>
            <span style="color:#ae81ff">343</span><span style="color:#75715e">###REDACTED###############################################################565</span>
            <span style="color:#ae81ff">326</span><span style="color:#75715e">###REDACTED###############################################################362</span>
            <span style="color:#ae81ff">3431</span>
</code></pre></div><p>We use Ansible Vault again, so that we can store our credentials in version control, without storing them unencrypted.</p>
<p>The generated configuration looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set snmp location &#34;Yeti Home&#34;
set snmp contact &#34;The Hairy One&#34;
set snmp v3 usm local-engine user yetiops authentication-sha authentication-password ###REDACTED###
set snmp v3 usm local-engine user yetiops privacy-aes128 privacy-password ###REDACTED###
set snmp v3 vacm security-to-group security-model usm security-name yetiops group yetiops_group
set snmp v3 vacm access group yetiops_group default-context-prefix security-model any security-level authentication read-view all
set snmp v3 vacm access group yetiops_group default-context-prefix security-model any security-level authentication write-view all
set snmp view all oid .1
</code></pre></div><h4 id="verification-6">Verification</h4>
<p>To check whether this is working, you will need either some form of monitoring system, or you can use something like <code>snmpwalk</code> to check: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">! snmpwalk to junos-01
$ snmpwalk -v3 -u yetiops -a SHA -A <span style="color:#75715e">###AUTH-KEY### -x AES -X ###PRIV-KEY### -l authPriv 10.15.30.33</span>
iso.3.6.1.2.1.1.1.0 <span style="color:#f92672">=</span> STRING: <span style="color:#e6db74">&#34;Juniper Networks, Inc. vsrx internet router, kernel JUNOS 19.2R1.8, Build date: 2019-06-21 21:03:26 UTC Copyright (c) 1996-2019 Juniper Networks, Inc.&#34;</span>
iso.3.6.1.2.1.1.2.0 <span style="color:#f92672">=</span> OID: iso.3.6.1.4.1.2636.1.1.1.2.129
iso.3.6.1.2.1.1.3.0 <span style="color:#f92672">=</span> Timeticks: <span style="color:#f92672">(</span>124672<span style="color:#f92672">)</span> 0:20:46.72
iso.3.6.1.2.1.1.4.0 <span style="color:#f92672">=</span> STRING: <span style="color:#e6db74">&#34;The Hairy One&#34;</span>
iso.3.6.1.2.1.1.5.0 <span style="color:#f92672">=</span> STRING: <span style="color:#e6db74">&#34;junos-01&#34;</span>
iso.3.6.1.2.1.1.6.0 <span style="color:#f92672">=</span> STRING: <span style="color:#e6db74">&#34;Yeti Home&#34;</span>
iso.3.6.1.2.1.1.7.0 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">4</span>
iso.3.6.1.2.1.2.1.0 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">37</span>
iso.3.6.1.2.1.2.2.1.1.1 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">1</span>
iso.3.6.1.2.1.2.2.1.1.4 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">4</span>
iso.3.6.1.2.1.2.2.1.1.5 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">5</span>
iso.3.6.1.2.1.2.2.1.1.6 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">6</span>
iso.3.6.1.2.1.2.2.1.1.7 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">7</span>

! snmpwalk to junos-02
$ snmpwalk -v3 -u yetiops -a SHA -A <span style="color:#75715e">###AUTH-KEY### -x AES -X ###PRIV-KEY### -l authPriv 10.15.30.34</span>
iso.3.6.1.2.1.1.1.0 <span style="color:#f92672">=</span> STRING: <span style="color:#e6db74">&#34;Juniper Networks, Inc. vsrx internet router, kernel JUNOS 19.2R1.8, Build date: 2019-06-21 21:03:26 UTC Copyright (c) 1996-2019 Juniper Networks, Inc.&#34;</span>
iso.3.6.1.2.1.1.2.0 <span style="color:#f92672">=</span> OID: iso.3.6.1.4.1.2636.1.1.1.2.129
iso.3.6.1.2.1.1.3.0 <span style="color:#f92672">=</span> Timeticks: <span style="color:#f92672">(</span>132339<span style="color:#f92672">)</span> 0:22:03.39
iso.3.6.1.2.1.1.4.0 <span style="color:#f92672">=</span> STRING: <span style="color:#e6db74">&#34;The Hairy One&#34;</span>
iso.3.6.1.2.1.1.5.0 <span style="color:#f92672">=</span> STRING: <span style="color:#e6db74">&#34;junos-02&#34;</span>
iso.3.6.1.2.1.1.6.0 <span style="color:#f92672">=</span> STRING: <span style="color:#e6db74">&#34;Yeti Home&#34;</span>
iso.3.6.1.2.1.1.7.0 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">4</span>
iso.3.6.1.2.1.2.1.0 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">34</span>
iso.3.6.1.2.1.2.2.1.1.1 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">1</span>
iso.3.6.1.2.1.2.2.1.1.4 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">4</span>
iso.3.6.1.2.1.2.2.1.1.5 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">5</span>
iso.3.6.1.2.1.2.2.1.1.6 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">6</span>
iso.3.6.1.2.1.2.2.1.1.7 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">7</span>
</code></pre></div><p>All looks good again!</p>
<h3 id="nat">NAT</h3>
<p>In this section, we are allowing the <strong>internal</strong> router to reach the internet, via the <strong>edge</strong> router. The <strong>edge</strong> router has a default route to the internet that is learned from DHCP.</p>
<h4 id="playbook-4">Playbook</h4>
<p>The playbook looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for nat</span>
- <span style="color:#66d9ef">name</span>: Apply NAT Overload
  <span style="color:#66d9ef">junos_config</span>:
    <span style="color:#66d9ef">src</span>: nat-overload.j2
  <span style="color:#66d9ef">tags</span>:
  - nat
</code></pre></div><p>Again, we are using <code>junos_config</code> for this, as no Ansible module exists for NAT on JunOS.</p>
<h4 id="template-1">Template</h4>
<p>The template looks like the below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">{% if 'edge' in rtr_role %}
delete security nat
{% for zone in zones %}
{% if zone['nat'] is defined %}
set security nat source rule-set internet-nat description &quot;Internet-facing NAT&quot;
{% if 'inside' in zone['nat']['role'] %}
set security nat source rule-set internet-nat from zone {{ zone['name'] }}
{% endif %}
{% if 'outside' in zone['nat']['role'] %}
set security nat source rule-set internet-nat to zone {{ zone['name'] }}
{% endif %}
{% endif %}
{% endfor %}
set security nat source rule-set internet-nat rule snat-out match destination-address 0.0.0.0/0
set security nat source rule-set internet-nat rule snat-out then source-nat interface
{% endif %}
</code></pre><p>The above template, like in previous tasks, removes all the existing NAT configuration. Unlike in Cisco IOS where you apply access lists, NAT configuration and define NAT on your interfaces, in JunOS all the configuration is in one place. NAT is also applied between zones, rather than specifying individual interfaces.</p>
<p>Our <code>host_vars</code> for this looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">zones</span>:
  - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;internet&#34;</span>
    <span style="color:#66d9ef">nat</span>:
      <span style="color:#66d9ef">role</span>: <span style="color:#e6db74">&#34;outside&#34;</span>
  - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;internal&#34;</span>
    <span style="color:#66d9ef">nat</span>:
      <span style="color:#66d9ef">role</span>: <span style="color:#e6db74">&#34;inside&#34;</span>
</code></pre></div><p>This generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set security nat source rule-set internet-nat description &#34;Internet-facing NAT&#34;
set security nat source rule-set internet-nat from zone internal
set security nat source rule-set internet-nat to zone internet
set security nat source rule-set internet-nat rule snat-out match destination-address 0.0.0.0/0
set security nat source rule-set internet-nat rule snat-out then source-nat interface
</code></pre></div><h4 id="verification-7">Verification</h4>
<p>We can now test from the <strong>internal</strong> router, to see if it can reach the internet: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Can we reach the internet?
ansible@junos-02&gt; ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: icmp_seq=0 ttl=52 time=22.939 ms
64 bytes from 8.8.8.8: icmp_seq=1 ttl=52 time=18.661 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=52 time=20.108 ms
^C
--- 8.8.8.8 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max/stddev = 18.661/20.569/22.939/1.777 ms

ansible@junos-02&gt; ping 1.1.1.1
PING 1.1.1.1 (1.1.1.1): 56 data bytes
64 bytes from 1.1.1.1: icmp_seq=0 ttl=55 time=16.593 ms
64 bytes from 1.1.1.1: icmp_seq=1 ttl=55 time=30.943 ms
^C
--- 1.1.1.1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 16.593/23.768/30.943/7.175 ms

ansible@junos-02&gt; ping 1.1.1.1 source 192.0.2.202
PING 1.1.1.1 (1.1.1.1): 56 data bytes
64 bytes from 1.1.1.1: icmp_seq=0 ttl=55 time=14.568 ms
64 bytes from 1.1.1.1: icmp_seq=1 ttl=55 time=14.456 ms
^C
--- 1.1.1.1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 14.456/14.512/14.568/0.056 ms

! What does this look like on the edge router?
ansible@junos-01&gt; show security nat source rule snat-out
source NAT rule: snat-out               Rule-set: internet-nat
  Rule-Id                    : 1
  Rule position              : 1
  From zone                  : internal
  To zone                    : internet
    Destination addresses    : 0.0.0.0         - 255.255.255.255
  Action                        : interface
    Persistent NAT type         : N/A
    Persistent NAT mapping type : address-port-mapping
    Inactivity timeout          : 0
    Max session number          : 0
  Translation hits           : 68
    Successful sessions      : 68
    Failed sessions          : 0
  Number of sessions         : 5
</code></pre></div><p>All looking good, the translation hits showing every time that NAT is being used.</p>
<h3 id="aaa">AAA</h3>
<p>The final task is AAA (<strong>A</strong>uthentication, <strong>A</strong>uthorization and <strong>A</strong>ccounting). This will run against the <code>tac_plus</code> instance on the <strong>netsvr-01</strong> machine.</p>
<p>This will allow central management of our users, as well as providing logs of commands being run.</p>
<p>Again, no Ansible module exists for AAA, so we are using the <code>junos_config</code> module.</p>
<h4 id="playbook-5">Playbook</h4>
<p>The playbook is one task again, applying a template: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for aaa</span>
- <span style="color:#66d9ef">name</span>: Enable TACACS<span style="color:#e6db74">+
</span><span style="color:#e6db74">  junos_config:</span>
    <span style="color:#66d9ef">src</span>: tacacs.j2
  <span style="color:#66d9ef">tags</span>: 
  - aaa
</code></pre></div><h4 id="template-2">Template</h4>
<p>The template used can be seen below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">set system authentication-order password
set system authentication-order tacplus
set system tacplus-server {{ tacacs['ipv4'] }} secret {{ tacacs['secret'] }} 
set system login class super-user-local idle-timeout 3600
set system login class super-user-local permissions all
set system login user remote full-name &quot;Any remote&quot;
set system login user remote uid 2002
set system login user remote class super-user-local
set system accounting events login
set system accounting events change-log
set system accounting events interactive-commands
set system accounting destination tacplus server {{ tacacs['ipv4'] }} secret {{ tacacs['secret'] }} 
</code></pre><p>Compared to the IOS template, there are far fewer commands here to enable TACACS+ authentication. We also set the <code>authentication-order</code>, to allow users defined locally (i.e. those on the JunOS router itself) access before querying <code>tac_plus</code>. This is useful in situations where <code>tac_plus</code> may be functioning incorrectly, but JunOS still thinks it is available.</p>
<p>Again, from our <code>group_vars</code>, we apply the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">tacacs</span>:
  <span style="color:#66d9ef">ipv4</span>: <span style="color:#ae81ff">192.0.2.1</span>
  <span style="color:#66d9ef">secret</span>: supersecret
</code></pre></div><p>This then generates the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">set system authentication-order password
set system authentication-order tacplus
set system tacplus-server 192.0.2.1 secret &#34;$9$7lV2ajHmPT3wYfz6/OBcylMWx-VYJZjs2&#34;
set system login class super-user-local idle-timeout 3600
set system login class super-user-local permissions all
set system login user remote full-name &#34;Any remote&#34;
set system login user remote uid 2002
set system login user remote class super-user-local
</code></pre></div><h4 id="verification-8">Verification</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Can we login with the yetiops user?
$ ssh yetuops@10.15.30.34
----------------------------------------
|
| This banner was generated by Ansible 
|
----------------------------------------
|
| You are logged into junos-02
| 
----------------------------------------
Password:
--- JUNOS 19.2R1.8 Kernel 64-bit XEN JNPR-11.0-20190517.f0321c3_buil
yetiops@junos-02&gt; 

! What about a user that doesn&#39;t exist?
$ ssh jeff@10.15.30.34                                                                                     
----------------------------------------
|
| This banner was generated by Ansible 
|
----------------------------------------
|
| You are logged into junos-02
| 
----------------------------------------
Password:
Password:
Password:

! What do see in our accounting log?
Mar 31 19:41:28	10.100.102.253	yetiops	0	10.15.30.1	start	task_id=1	service=shell	session_pid = 8488	cmd=login
Mar 31 19:41:33	10.100.102.253	yetiops	0	10.15.30.1	stop	task_id=2	service=shell	session_pid = 8488	cmd=show route &lt;cr&gt;
Mar 31 19:41:33	10.100.102.253	yetiops	0	10.15.30.1	stop	task_id=3	service=shell	session_pid = 8488	cmd=configure &lt;cr&gt;
Mar 31 19:41:33	10.100.102.253	yetiops	0	10.15.30.1	stop	task_id=4	service=shell	session_pid = 8488	cmd=exit &lt;cr&gt;

! What about if the TACACS+ server goes away?
[stuh84@netsvr-01 /var/log] $ sudo systemctl stop tac_plus
[stuh84@netsvr-01 /var/log] $ sudo systemctl status tac_plus
tac_plus.service - LSB: TACACS+ server based on Cisco source release
   Loaded: loaded (/etc/rc.d/init.d/tac_plus; generated)
   Active: inactive (dead) since Tue 2020-03-31 14:42:38 EDT; 3s ago

$ ssh ansible@10.15.30.34
----------------------------------------
|
| This banner was generated by Ansible 
|
----------------------------------------
|
| You are logged into junos-02
| 
----------------------------------------
Password:
Last login: Tue Mar 31 18:05:40 2020 from 10.15.30.1
--- JUNOS 19.2R1.8 Kernel 64-bit XEN JNPR-11.0-20190517.f0321c3_buil
ansible@junos-02&gt; 
</code></pre></div><p>All looks good!</p>
<h3 id="parent-playbook">Parent playbook</h3>
<p>The parent playbook (i.e. the playbook that brings all the roles together) is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
- <span style="color:#66d9ef">hosts</span>: junos
  <span style="color:#66d9ef">gather_facts</span>: <span style="color:#66d9ef">false</span>
  <span style="color:#66d9ef">tasks</span>:
    - <span style="color:#66d9ef">import_role</span>:
        <span style="color:#66d9ef">name</span>: system
    - <span style="color:#66d9ef">import_role</span>:
        <span style="color:#66d9ef">name</span>: interfaces
    - <span style="color:#66d9ef">import_role</span>:
        <span style="color:#66d9ef">name</span>: firewall
    - <span style="color:#66d9ef">import_role</span>:
        <span style="color:#66d9ef">name</span>: routing
    - <span style="color:#66d9ef">import_role</span>:
        <span style="color:#66d9ef">name</span>: snmp
    - <span style="color:#66d9ef">import_role</span>:
        <span style="color:#66d9ef">name</span>: nat
    - <span style="color:#66d9ef">import_role</span>:
        <span style="color:#66d9ef">name</span>: aaa
</code></pre></div><p>We do not need to have a task that saves the configuration after running, because the changes are committed as part of each task.</p>
<h4 id="other-options-for-committing-configuration">Other options for committing configuration</h4>
<p>JunOS will not let you commit changes that are missing vital parts of configuration, or that are malformed. If you choose, you can also use the following values to control your commits: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for aaa</span>
- <span style="color:#66d9ef">name</span>: Enable TACACS<span style="color:#e6db74">+
</span><span style="color:#e6db74">  junos_config:</span>
    <span style="color:#66d9ef">src</span>: tacacs.j2
    <span style="color:#66d9ef">commit</span>: <span style="color:#ae81ff">5</span>
    <span style="color:#66d9ef">check_commit</span>: yes
  <span style="color:#66d9ef">tags</span>: 
  - aaa
</code></pre></div><p>What the above will do is: -</p>
<ul>
<li>Run a <code>commit check</code> to ensure that the candidate configuration being committed is valid
<ul>
<li>This is quicker than running through the full commit process</li>
</ul>
</li>
<li>Use the <code>commit confirmed 5</code> option to say that unless <code>commit</code> is supplied again with 5 minutes, the configuration reverts to its previous state.</li>
</ul>
<p>You can either commit the changes yourself, or you can run a task like the below to confirm the commit: -</p>
<pre><code>- name: confirm a previous commit
  junos_config:
    confirm_commit: yes
</code></pre><p>You could do this further down a playbook, or it could be invoked later. This gives time for any routing protocol changes to converge, or for a potentially broken change to take effect, rather than committing straight away and losing access to the device.</p>
<p>The commit system is one of my favourite JunOS features, as well as the ability to preview your changes before committing (<code>show | compare</code> within configuration mode).</p>
<h4 id="role-order">Role Order</h4>
<p>The role order is very similar to how I explained for IOS. The main difference here is that we are using stateful firewalling rather than access lists. The justification for the role order is detailed within <a href="/posts/ansible-for-networking-part-3-cisco-ios">Part 3 - Cisco IOS</a>. To summarize: -</p>
<ul>
<li><code>system</code> - Sets up logging, hostnames and banners
<ul>
<li>This ensure we have logging ready for if any of the other roles fail (that the Ansible debug output cannot help with)</li>
</ul>
</li>
<li><code>interfaces</code> - This is a prerequisite for most of the following tasks</li>
<li><code>firewall</code> - Apply before routing so that the device is not open to the world when publicly routable</li>
<li><code>routing</code> - Routing is required for NAT and AAA to function</li>
<li><code>snmp</code> - No dependency on any service, so this can go anywhere</li>
<li><code>nat</code> - Apply this after routing, otherwise the <strong>internal</strong> router has no default route to reach external destinations anyway</li>
<li><code>aaa</code> - It depends upon routing, and if configured incorrectly it can break login sessions</li>
</ul>
<p>Running <code>aaa</code> last means that all other configuration is complete, so that if login sessions do break, you at least have a fully configured router (and therefore able to access via other means) before this happens.</p>
<h2 id="artifacts">Artifacts</h2>
<p>The final directory structure looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">$ tree -L 2
.
├── ansible.cfg
├── ansible.log
├── group_vars
│   └── junos
├── host_vars
│   ├── junos-01.yml
│   └── junos-02.yml
├── inventory
├── junos.yaml
└── roles
    ├── aaa
    ├── firewall
    ├── interfaces
    ├── lldp
    ├── nat
    ├── routing
    ├── snmp
    └── system

11 directories, 7 files
</code></pre></div><p>The final contents of our <code>group_vars</code> are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">ansible_user</span>: ansible
<span style="color:#66d9ef">ansible_connection</span>: netconf 
<span style="color:#66d9ef">ansible_network_os</span>: junos
<span style="color:#66d9ef">ansible_ssh_pass</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">                  $ANSIBLE_VAULT;1.1;AES256</span>
                  <span style="color:#ae81ff">383</span><span style="color:#75715e">###REDACTED###############################################################566</span>
                  <span style="color:#ae81ff">363</span><span style="color:#75715e">###REDACTED###############################################################366</span>
                  <span style="color:#ae81ff">343</span><span style="color:#75715e">###REDACTED###############################################################565</span>
                  <span style="color:#ae81ff">326</span><span style="color:#75715e">###REDACTED###############################################################362</span>
                  <span style="color:#ae81ff">3431</span>
<span style="color:#66d9ef">log_host</span>: <span style="color:#ae81ff">10.100.101.254</span>
<span style="color:#66d9ef">tacacs</span>:
  <span style="color:#66d9ef">ipv4</span>: <span style="color:#ae81ff">192.0.2.1</span>
  <span style="color:#66d9ef">secret</span>: supersecret
<span style="color:#66d9ef">snmp</span>:
  <span style="color:#66d9ef">location</span>: Yeti Home
  <span style="color:#66d9ef">contact</span>: The Hairy One
  <span style="color:#66d9ef">user</span>: yetiops
  <span style="color:#66d9ef">group</span>: yetiops_group
  <span style="color:#66d9ef">auth_key</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">            $ANSIBLE_VAULT;1.1;AES256</span>
            <span style="color:#ae81ff">383</span><span style="color:#75715e">###REDACTED###############################################################566</span>
            <span style="color:#ae81ff">363</span><span style="color:#75715e">###REDACTED###############################################################366</span>
            <span style="color:#ae81ff">343</span><span style="color:#75715e">###REDACTED###############################################################565</span>
            <span style="color:#ae81ff">326</span><span style="color:#75715e">###REDACTED###############################################################362</span>
            <span style="color:#ae81ff">3431</span>
  <span style="color:#66d9ef">priv_key</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">            $ANSIBLE_VAULT;1.1;AES256</span>
            <span style="color:#ae81ff">386</span><span style="color:#75715e">###REDACTED###############################################################764</span>
            <span style="color:#ae81ff">613</span><span style="color:#75715e">###REDACTED###############################################################630</span>
            <span style="color:#ae81ff">646</span><span style="color:#75715e">###REDACTED###############################################################331</span>
            <span style="color:#ae81ff">376</span><span style="color:#75715e">###REDACTED###############################################################137</span>
            <span style="color:#ae81ff">3563</span>
</code></pre></div><p>The final contents of our <code>host_vars</code> are: -</p>
<p><strong>junos-01.yaml</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">router_id</span>: <span style="color:#ae81ff">192.0.2.102</span>
<span style="color:#66d9ef">rtr_role</span>: edge
<span style="color:#66d9ef">addressbook</span>:
  - <span style="color:#66d9ef">zone</span>: edge
    <span style="color:#66d9ef">name</span>: netsvr
    <span style="color:#66d9ef">ip</span>: <span style="color:#ae81ff">10.100.102.254</span>
    <span style="color:#66d9ef">set</span>:
      - external_bgp_peers
      - netsvr-direct
  - <span style="color:#66d9ef">zone</span>: edge
    <span style="color:#66d9ef">name</span>: netsvr-v6
    <span style="color:#66d9ef">ip</span>: <span style="color:#e6db74">&#34;2001:db8:102::ffff/128&#34;</span>
    <span style="color:#66d9ef">set</span>:
      - external_bgp_peers
      - netsvr-direct
  - <span style="color:#66d9ef">zone</span>: edge
    <span style="color:#66d9ef">name</span>: netsvr-lo
    <span style="color:#66d9ef">ip</span>: <span style="color:#ae81ff">192.0.2.1</span>
    <span style="color:#66d9ef">set</span>:
    - netsvr-loop
  - <span style="color:#66d9ef">zone</span>: edge
    <span style="color:#66d9ef">name</span>: netsvr-lo-v6
    <span style="color:#66d9ef">ip</span>: <span style="color:#e6db74">&#34;2001:db8:999:beef::1/128&#34;</span>
    <span style="color:#66d9ef">set</span>: 
    - netsvr-loop
  - <span style="color:#66d9ef">zone</span>: internal
    <span style="color:#66d9ef">name</span>: internal-rtr
    <span style="color:#66d9ef">ip</span>: <span style="color:#ae81ff">192.0.2.202</span>
    <span style="color:#66d9ef">set</span>: 
    - internal_bgp_peers
    - internal-rtr-loop
  - <span style="color:#66d9ef">zone</span>: internal
    <span style="color:#66d9ef">name</span>: internal-rtr-v6
    <span style="color:#66d9ef">ip</span>: <span style="color:#e6db74">&#34;2001:db8:902:beef::2/128&#34;</span>
    <span style="color:#66d9ef">set</span>: 
    - internal_bgp_peers
    - internal-rtr-loop
<span style="color:#66d9ef">fw_policies</span>:
  - <span style="color:#66d9ef">name</span>: all_icmp
    <span style="color:#66d9ef">zone</span>: global
    <span style="color:#66d9ef">source</span>: any
    <span style="color:#66d9ef">destination</span>: any
    <span style="color:#66d9ef">action</span>: permit
    <span style="color:#66d9ef">apps</span>:
      - junos-ping
  - <span style="color:#66d9ef">name</span>: a_tacacs
    <span style="color:#66d9ef">zone</span>: internal
    <span style="color:#66d9ef">from_zone</span>: internal
    <span style="color:#66d9ef">to_zone</span>: edge
    <span style="color:#66d9ef">source</span>: any
    <span style="color:#66d9ef">destination</span>: 
      - netsvr-loop
    <span style="color:#66d9ef">action</span>: permit
    <span style="color:#66d9ef">apps</span>:
      - junos-tacacs
  - <span style="color:#66d9ef">name</span>: a_syslog
    <span style="color:#66d9ef">zone</span>: internal
    <span style="color:#66d9ef">from_zone</span>: internal
    <span style="color:#66d9ef">to_zone</span>: edge
    <span style="color:#66d9ef">source</span>: any
    <span style="color:#66d9ef">destination</span>: 
      - netsvr-direct
    <span style="color:#66d9ef">action</span>: permit
    <span style="color:#66d9ef">apps</span>:
      - junos-syslog
  - <span style="color:#66d9ef">name</span>: a_ext_bgp
    <span style="color:#66d9ef">zone</span>: edge
    <span style="color:#66d9ef">from_zone</span>: edge
    <span style="color:#66d9ef">to_zone</span>: edge
    <span style="color:#66d9ef">source</span>: 
      - external_bgp_peers
    <span style="color:#66d9ef">destination</span>: any
    <span style="color:#66d9ef">action</span>: permit
    <span style="color:#66d9ef">apps</span>:
      - junos-bgp
  - <span style="color:#66d9ef">name</span>: a_ext_bgp_host
    <span style="color:#66d9ef">zone</span>: edge
    <span style="color:#66d9ef">from_zone</span>: edge
    <span style="color:#66d9ef">to_zone</span>: junos-host
    <span style="color:#66d9ef">source</span>: 
      - external_bgp_peers
    <span style="color:#66d9ef">destination</span>: any
    <span style="color:#66d9ef">action</span>: permit
    <span style="color:#66d9ef">apps</span>:
      - junos-bgp
  - <span style="color:#66d9ef">name</span>: a_int_bgp
    <span style="color:#66d9ef">zone</span>: internal
    <span style="color:#66d9ef">from_zone</span>: internal
    <span style="color:#66d9ef">to_zone</span>: internal
    <span style="color:#66d9ef">source</span>: 
      - internal_bgp_peers
    <span style="color:#66d9ef">destination</span>: any
    <span style="color:#66d9ef">action</span>: permit
    <span style="color:#66d9ef">apps</span>:
      - junos-bgp
  - <span style="color:#66d9ef">name</span>: a_int_bgp_host
    <span style="color:#66d9ef">zone</span>: internal
    <span style="color:#66d9ef">from_zone</span>: internal
    <span style="color:#66d9ef">to_zone</span>: junos-host
    <span style="color:#66d9ef">source</span>: 
      - internal_bgp_peers
    <span style="color:#66d9ef">destination</span>: any
    <span style="color:#66d9ef">action</span>: permit
    <span style="color:#66d9ef">apps</span>:
      - junos-bgp
  - <span style="color:#66d9ef">name</span>: a_internet_routing
    <span style="color:#66d9ef">zone</span>: internal
    <span style="color:#66d9ef">from_zone</span>: internal
    <span style="color:#66d9ef">to_zone</span>: internal
    <span style="color:#66d9ef">source</span>: any
    <span style="color:#66d9ef">destination</span>: any
    <span style="color:#66d9ef">action</span>: permit
    <span style="color:#66d9ef">apps</span>: any
<span style="color:#66d9ef">route_policies</span>:
  <span style="color:#66d9ef">prefix_lists</span>:
    - <span style="color:#66d9ef">name</span>: internal_nets
      <span style="color:#66d9ef">addresses</span>:
         - <span style="color:#ae81ff">192.0.2.102</span>/<span style="color:#ae81ff">32</span>
         - <span style="color:#ae81ff">192.0.2.202</span>/<span style="color:#ae81ff">32</span>
         - <span style="color:#ae81ff">10.100.202.0</span>/<span style="color:#ae81ff">24</span>
         - <span style="color:#e6db74">&#34;2001:db8:902:beef::1/128&#34;</span>
         - <span style="color:#e6db74">&#34;2001:db8:902:beef::2/128&#34;</span>
         - <span style="color:#e6db74">&#34;2001:db8:902::/64&#34;</span>
    - <span style="color:#66d9ef">name</span>: external_nets
      <span style="color:#66d9ef">addresses</span>:
         - <span style="color:#ae81ff">192.0.2.1</span>/<span style="color:#ae81ff">32</span>
         - <span style="color:#e6db74">&#34;2001:db8:999:beef::1/128&#34;</span>
         - <span style="color:#ae81ff">10.100.102.0</span>/<span style="color:#ae81ff">24</span>
  <span style="color:#66d9ef">rp</span>:
    - <span style="color:#66d9ef">name</span>: external_networks
      <span style="color:#66d9ef">from</span>:
        <span style="color:#66d9ef">protocols</span>: 
        - bgp
        <span style="color:#66d9ef">pfx_list</span>: external_nets
      <span style="color:#66d9ef">action</span>: accept
    - <span style="color:#66d9ef">name</span>: internal_networks
      <span style="color:#66d9ef">from</span>:
        <span style="color:#66d9ef">protocols</span>:
        - direct
        - ospf
        - ospf3
        <span style="color:#66d9ef">pfx_list</span>: internal_nets
      <span style="color:#66d9ef">action</span>: accept
    - <span style="color:#66d9ef">name</span>: dhcp_default
      <span style="color:#66d9ef">from</span>:
        <span style="color:#66d9ef">protocols</span>: 
        - access-internal
      <span style="color:#66d9ef">action</span>: accept
    - <span style="color:#66d9ef">name</span>: deny-all 
      <span style="color:#66d9ef">action</span>: reject
<span style="color:#66d9ef">bgp</span>:
  <span style="color:#66d9ef">local_as</span>: <span style="color:#ae81ff">65102</span>
  <span style="color:#66d9ef">groups</span>:
    - <span style="color:#66d9ef">name</span>: NETSVR
      <span style="color:#66d9ef">type</span>: external
      <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Peering to NETSVR&#34;</span>
      <span style="color:#66d9ef">hold</span>: <span style="color:#ae81ff">30</span>
      <span style="color:#66d9ef">policies</span>:
        <span style="color:#66d9ef">import</span>:
          - external_networks
          - deny-all
        <span style="color:#66d9ef">export</span>: 
          - internal_networks
          - deny-all
      <span style="color:#66d9ef">neighbours</span>:
        - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">10.100.102.254</span>
          <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65430</span>
          <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;netsvr-01 IPv4&#34;</span>
        - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:102::ffff&#34;</span>
          <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65430</span>
          <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;netsvr-01 IPv6&#34;</span>
    - <span style="color:#66d9ef">name</span>: IBGP
      <span style="color:#66d9ef">type</span>: internal
      <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;IBGP BGP Peering&#34;</span>
      <span style="color:#66d9ef">hold</span>: <span style="color:#ae81ff">30</span>
      <span style="color:#66d9ef">policies</span>:
        <span style="color:#66d9ef">export</span>:
          - dhcp_default
          - external_networks
          - deny-all
        <span style="color:#66d9ef">import</span>: 
          - internal_networks
          - deny-all
      <span style="color:#66d9ef">neighbours</span>:
        - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">192.0.2.202</span>
          <span style="color:#66d9ef">loc_ip</span>: <span style="color:#ae81ff">192.0.2.102</span>
          <span style="color:#66d9ef">default_originate</span>: <span style="color:#66d9ef">true</span>
          <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;junos-02 IPv4&#34;</span>
        - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:902:beef::2&#34;</span>
          <span style="color:#66d9ef">loc_ip</span>: <span style="color:#e6db74">&#34;2001:db8:902:beef::1&#34;</span>
          <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;junos-02 IPv6&#34;</span>
<span style="color:#66d9ef">zones</span>:
  - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;edge&#34;</span>
    <span style="color:#66d9ef">host_traffic</span>:
      <span style="color:#66d9ef">protocols</span>:
        - bgp
      <span style="color:#66d9ef">services</span>:
        - ping
        - traceroute
  - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;internet&#34;</span>
    <span style="color:#66d9ef">nat</span>: 
      <span style="color:#66d9ef">role</span>: <span style="color:#e6db74">&#34;outside&#34;</span>
    <span style="color:#66d9ef">host_traffic</span>:
      <span style="color:#66d9ef">services</span>:
        - ping
        - traceroute
        - dhcp
  - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;internal&#34;</span>
    <span style="color:#66d9ef">nat</span>:
      <span style="color:#66d9ef">role</span>: <span style="color:#e6db74">&#34;inside&#34;</span>
    <span style="color:#66d9ef">host_traffic</span>:
      <span style="color:#66d9ef">protocols</span>:
        - bgp
        - ospf
        - ospf3
      <span style="color:#66d9ef">services</span>:
        - ping
        - traceroute
<span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;fxp0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">0</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Management&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.15.30.33/24&#34;</span>
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;ge-0/0/0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">0</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;VLAN Bridge&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">subint</span>:
      <span style="color:#66d9ef">vlans</span>:
      - <span style="color:#ae81ff">102</span>
      - <span style="color:#ae81ff">202</span>
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;ge-0/0/0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">102</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To netsvr&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">if_zone</span>: <span style="color:#e6db74">&#34;edge&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.100.102.253/24&#34;</span>
    <span style="color:#66d9ef">ipv6_addr</span>: <span style="color:#e6db74">&#34;2001:db8:102::f/64&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;ge-0/0/0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">202</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To junos-02&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.100.202.254/24&#34;</span>
    <span style="color:#66d9ef">ipv6_addr</span>: <span style="color:#e6db74">&#34;2001:db8:202::a/64&#34;</span>
    <span style="color:#66d9ef">if_zone</span>: <span style="color:#e6db74">&#34;internal&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;ge-0/0/1&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To the Internet&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">0</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;dhcp&#34;</span>
    <span style="color:#66d9ef">if_zone</span>: <span style="color:#e6db74">&#34;internet&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;lo0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">0</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">if_zone</span>: <span style="color:#e6db74">&#34;internal&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;192.0.2.102/32&#34;</span>
    <span style="color:#66d9ef">ipv6_addr</span>: <span style="color:#e6db74">&#34;2001:db8:902:beef::1/128&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
</code></pre></div><p><strong>junos-02.yaml</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">router_id</span>: <span style="color:#ae81ff">192.0.2.202</span>
<span style="color:#66d9ef">rtr_role</span>: internal
<span style="color:#66d9ef">addressbook</span>:
  - <span style="color:#66d9ef">zone</span>: internal
    <span style="color:#66d9ef">name</span>: edge-rtr
    <span style="color:#66d9ef">ip</span>: <span style="color:#ae81ff">192.0.2.102</span>
    <span style="color:#66d9ef">set</span>: 
    - internal_bgp_peers
    - internal-rtr-loop
  - <span style="color:#66d9ef">zone</span>: internal
    <span style="color:#66d9ef">name</span>: edge-rtr-v6
    <span style="color:#66d9ef">ip</span>: <span style="color:#e6db74">&#34;2001:db8:902:beef::1/128&#34;</span>
    <span style="color:#66d9ef">set</span>: 
    - internal_bgp_peers
    - internal-rtr-loop
<span style="color:#66d9ef">fw_policies</span>:
  - <span style="color:#66d9ef">name</span>: all_icmp
    <span style="color:#66d9ef">zone</span>: global
    <span style="color:#66d9ef">source</span>: any
    <span style="color:#66d9ef">destination</span>: any
    <span style="color:#66d9ef">action</span>: permit
    <span style="color:#66d9ef">apps</span>:
      - junos-ping
  - <span style="color:#66d9ef">name</span>: a_int_bgp
    <span style="color:#66d9ef">zone</span>: internal
    <span style="color:#66d9ef">from_zone</span>: internal
    <span style="color:#66d9ef">to_zone</span>: internal
    <span style="color:#66d9ef">source</span>: 
      - internal_bgp_peers
    <span style="color:#66d9ef">destination</span>: any
    <span style="color:#66d9ef">action</span>: permit
    <span style="color:#66d9ef">apps</span>:
      - junos-bgp
  - <span style="color:#66d9ef">name</span>: a_int_bgp_host
    <span style="color:#66d9ef">zone</span>: internal
    <span style="color:#66d9ef">from_zone</span>: internal
    <span style="color:#66d9ef">to_zone</span>: junos-host
    <span style="color:#66d9ef">source</span>: 
      - internal_bgp_peers
    <span style="color:#66d9ef">destination</span>: any
    <span style="color:#66d9ef">action</span>: permit
    <span style="color:#66d9ef">apps</span>:
      - junos-bgp
<span style="color:#66d9ef">route_policies</span>:
  <span style="color:#66d9ef">prefix_lists</span>:
    - <span style="color:#66d9ef">name</span>: external_nets
      <span style="color:#66d9ef">addresses</span>:
         - <span style="color:#ae81ff">192.0.2.1</span>/<span style="color:#ae81ff">32</span>
         - <span style="color:#e6db74">&#34;2001:db8:999:beef::1/128&#34;</span>
         - <span style="color:#ae81ff">10.100.102.0</span>/<span style="color:#ae81ff">24</span>
    - <span style="color:#66d9ef">name</span>: internal_nets
      <span style="color:#66d9ef">addresses</span>:
         - <span style="color:#ae81ff">192.0.2.102</span>/<span style="color:#ae81ff">32</span>
         - <span style="color:#ae81ff">192.0.2.202</span>/<span style="color:#ae81ff">32</span>
         - <span style="color:#ae81ff">10.100.202.0</span>/<span style="color:#ae81ff">24</span>
         - <span style="color:#e6db74">&#34;2001:db8:902:beef::1/128&#34;</span>
         - <span style="color:#e6db74">&#34;2001:db8:902:beef::2/128&#34;</span>
         - <span style="color:#e6db74">&#34;2001:db8:902::/64&#34;</span>
    - <span style="color:#66d9ef">name</span>: default_route
      <span style="color:#66d9ef">addresses</span>:
        - <span style="color:#ae81ff">0.0.0.0</span>/<span style="color:#ae81ff">0</span>
  <span style="color:#66d9ef">rp</span>:
    - <span style="color:#66d9ef">name</span>: external_networks
      <span style="color:#66d9ef">from</span>:
        <span style="color:#66d9ef">protocols</span>: 
        - bgp
        <span style="color:#66d9ef">pfx_list</span>: external_nets
      <span style="color:#66d9ef">action</span>: accept
    - <span style="color:#66d9ef">name</span>: internal_networks
      <span style="color:#66d9ef">from</span>:
        <span style="color:#66d9ef">protocols</span>:
        - bgp
        <span style="color:#66d9ef">pfx_list</span>: internal_nets
      <span style="color:#66d9ef">action</span>: accept
    - <span style="color:#66d9ef">name</span>: default_route
      <span style="color:#66d9ef">from</span>:
        <span style="color:#66d9ef">protocols</span>: 
        - bgp 
        <span style="color:#66d9ef">pfx_list</span>: default_route
      <span style="color:#66d9ef">action</span>: accept
    - <span style="color:#66d9ef">name</span>: deny-all 
      <span style="color:#66d9ef">action</span>: reject
<span style="color:#66d9ef">bgp</span>:
  <span style="color:#66d9ef">local_as</span>: <span style="color:#ae81ff">65102</span>
  <span style="color:#66d9ef">groups</span>:
    - <span style="color:#66d9ef">name</span>: IBGP
      <span style="color:#66d9ef">type</span>: internal
      <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;IBGP BGP Peering&#34;</span>
      <span style="color:#66d9ef">hold</span>: <span style="color:#ae81ff">30</span>
      <span style="color:#66d9ef">policies</span>:
        <span style="color:#66d9ef">export</span>:
          - internal_networks 
          - deny-all
        <span style="color:#66d9ef">import</span>: 
          - internal_networks
          - external_networks
          - default_route
          - deny-all
      <span style="color:#66d9ef">neighbours</span>:
        - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">192.0.2.102</span>
          <span style="color:#66d9ef">loc_ip</span>: <span style="color:#ae81ff">192.0.2.202</span>
          <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;junos-02 IPv4&#34;</span>
        - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:902:beef::1&#34;</span>
          <span style="color:#66d9ef">loc_ip</span>: <span style="color:#e6db74">&#34;2001:db8:902:beef::2&#34;</span>
          <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;junos-02 IPv6&#34;</span>
<span style="color:#66d9ef">zones</span>:
  - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;internal&#34;</span>
    <span style="color:#66d9ef">host_traffic</span>:
      <span style="color:#66d9ef">protocols</span>:
        - bgp
        - ospf
        - ospf3
      <span style="color:#66d9ef">services</span>:
        - ping
        - traceroute
<span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;fxp0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">0</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Management&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.15.30.34/24&#34;</span>
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;ge-0/0/0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">0</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;VLAN Bridge&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">subint</span>:
      <span style="color:#66d9ef">vlans</span>:
      - <span style="color:#ae81ff">202</span>
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;ge-0/0/0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">202</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To junos-01&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;10.100.202.253/24&#34;</span>
    <span style="color:#66d9ef">ipv6_addr</span>: <span style="color:#e6db74">&#34;2001:db8:202::f/64&#34;</span>
    <span style="color:#66d9ef">if_zone</span>: <span style="color:#e6db74">&#34;internal&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
  - <span style="color:#66d9ef">junos_if</span>: <span style="color:#e6db74">&#34;lo0&#34;</span>
    <span style="color:#66d9ef">unit</span>: <span style="color:#ae81ff">0</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;true&#34;</span>
    <span style="color:#66d9ef">if_zone</span>: <span style="color:#e6db74">&#34;internal&#34;</span>
    <span style="color:#66d9ef">ipv4_addr</span>: <span style="color:#e6db74">&#34;192.0.2.202/32&#34;</span>
    <span style="color:#66d9ef">ipv6_addr</span>: <span style="color:#e6db74">&#34;2001:db8:902:beef::2/128&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
</code></pre></div><p>There are significantly more variables created for this JunOS lab than for the Cisco IOS lab. However, we did not apply any routing policies (i.e. route-maps and prefix lists) on Cisco IOS. Also we are applying firewall policies on JunOS, which are more complex than access-lists.</p>
<p>If you integrate these tasks with something like <a href="https://github.com/netbox-community/netbox">Netbox</a> to supply your variables, you can build a huge estate all from a single source of truth, with little to no manual configuration involved.</p>
<h2 id="running-the-playbooks">Running the playbooks</h2>
<p>Below is an Asciinema output of my terminal when running the playbooks, so you can see them being applied: -</p>
<script id="asciicast-315386" src="https://asciinema.org/a/315386.js" async></script>
<p>As in the IOS lab, we see a lot of tasks marked as &ldquo;changed&rdquo;, despite no changes actually taking place.</p>
<p>This is an artifact of using the <code>junos_config</code> module so extensively. Also, because in some cases we are applying the <code>delete</code> option to remove existing configuration, it will mark a change . The <code>delete</code> word does not appear in the device configuration itself, so it will always be seen as a difference (and hence a change).</p>
<p>Again, using something like <code>changed_when</code> could tidy this up significantly.</p>
<h2 id="native-modules-versus-junos_config">Native modules versus <code>junos_config</code></h2>
<p>Below is a summary of how many different modules are used, and also how many in total were native modules (compared to using <code>junos_config</code>).</p>
<table>
<thead>
<tr>
<th>Module</th>
<th>Used</th>
</tr>
</thead>
<tbody>
<tr>
<td>junos_config</td>
<td>20</td>
</tr>
<tr>
<td>junos_l3_interfaces</td>
<td>2</td>
</tr>
<tr>
<td>junos_banner</td>
<td>2</td>
</tr>
<tr>
<td>junos_system</td>
<td>1</td>
</tr>
<tr>
<td>junos_logging</td>
<td>1</td>
</tr>
<tr>
<td>junos_interfaces</td>
<td>1</td>
</tr>
</tbody>
</table>
<p>As in the IOS lab, the <code>junos_config</code> module is used more than any other module. Also no native modules exist for any routing protocol. While we do still make use of some native Ansible features (like <code>when</code> and <code>loop</code>), most of our conditional logic and complexity has to be templated.</p>
<p>However, fewer steps are required to configure a device. Because of this, the time to configure both routers is around half of what it is in the Cisco IOS lab (4 minutes for IOS, 2 minutes for JunOS).</p>
<h2 id="thoughts-compared-to-ios">Thoughts compared to IOS</h2>
<p>If I compare the experience of using IOS and JunOS (generally, rather than when using Ansible), JunOS would be my preference. The commit system, the logical grouping of configuration, and sane defaults (e.g. all BGP peers must belong to a group) all contribute to a smooth experience in using JunOS day to day.</p>
<p>However when managing them with Ansible, IOS is currently the more mature option, given the amount of native modules.</p>
<p>It is quite possible to configure a BGP-speaking IOS router without much/any templating, whereas with JunOS you need to template most configuration beyond basic interfaces and system settings.</p>
<p>If you are already familiar with JunOS though, managing the devices with Ansible can save time on future deployments and repeatable tasks. You also automatically benefit from more consistent configuration, with configuration being templated rather than hand-crafted. Once you have your templates configured, you can quickly add new devices by creating a <code>host_vars</code> file and an entry in your inventory, or relying on some form of dynamic inventory/API to retrieve variables from.</p>
<h2 id="summary">Summary</h2>
<p>Despite some setbacks, and the need to create a lot of templates during this lab, I have still been impressed by what is available to automate deployments for network engineers now. The networking ecosystem for Ansible is growing considerably. Over time I expect most vendors to have modules that cater to most common use cases (i.e. firewalling, routing protocols).</p>
<p>Creating the initial templates has taken more time than it would with IOS, but at the same time it does allow some flexibility that is not always available, or planned to be, within native modules. A good example would be using the <code>apply-path</code> statements to dynamically generate prefix-lists for IPv4 and IPv6.</p>
<p>For those using JunOS, I recommend trying out Ansible to manage your devices. The benefits in terms of configuration consistency and repeatable deployments are worth it alone!</p>
<p>The final configs from the firewalls are in my <a href="https://gitlab.com/stuh84/network-automation-ansible/-/tree/master/junos/configs">Network Automation with Ansible repository</a>. The next part of this series will be configuring Arista devices, running EOS.</p>
]]></content>
        </item>
        
        <item>
            <title>Ansible for Networking - Part 3: Cisco IOS</title>
            <link>https://yetiops.net/posts/ansible-for-networking-part-3-cisco-ios/</link>
            <pubDate>Wed, 04 Mar 2020 12:36:05 +0000</pubDate>
            
            <guid>https://yetiops.net/posts/ansible-for-networking-part-3-cisco-ios/</guid>
            <description>The third part of my ongoing series of posts on Ansible for Networking will cover Cisco IOS. You can view the other posts in the series below: -
 Part 1 - Start of the series Part 2 - The Lab Environment Part 4 - Juniper JunOS Part 5 - Arista EOS Part 6 - MikroTik RouterOS Part 7 - VyOS  All the playbooks, roles and variables used in this article are available in my Network Automation with Ansible repository</description>
            <content type="html"><![CDATA[<p>The third part of my ongoing series of posts on Ansible for Networking will cover Cisco IOS. You can view the other posts in the series below: -</p>
<ul>
<li><a href="/posts/ansible-for-networking-series">Part 1 - Start of the series</a></li>
<li><a href="/posts/ansible-for-networking-part-2-lab">Part 2 - The Lab Environment</a></li>
<li><a href="/posts/ansible-for-networking-part-4-juniper-junos">Part 4 - Juniper JunOS</a></li>
<li><a href="/posts/ansible-for-networking-part-5-arista-eos">Part 5 - Arista EOS</a></li>
<li><a href="/posts/ansible-for-networking-part-6-mikrotik-routeros">Part 6 - MikroTik RouterOS</a></li>
<li><a href="/posts/ansible-for-networking-part-7-vyos">Part 7 - VyOS</a></li>
</ul>
<p>All the playbooks, roles and variables used in this article are available in my <a href="https://gitlab.com/stuh84/network-automation-ansible">Network Automation with Ansible repository</a></p>
<h2 id="why-ios">Why IOS?</h2>
<p>Anyone who has worked in the network industry long enough will have encountered Cisco equipment at some point in their career. Protocols from MPLS to VRRP all originated in some way from Cisco.</p>
<p>My networking career (like many others) started with Cisco IOS, and it is still the platform I am most familiar with.</p>
<p>Cisco offer a number of different operating systems, depending on the kit you are using. These include NX-OS (for the Nexus line of data centre switching), IOS-XR (geared more towards service provider equipment) and the ASA platform (for security). Cisco IOS is the most well-known, and probably the most deployed to this day.</p>
<p>It&rsquo;s also worth mentioning IOS-XE. IOS-XE is Cisco IOS, but with a better software architecture.</p>
<p>IOS is monolithic, in that it uses a single software process for everything (from routing to authentication), meaning a bug in NTP could taken down your routing daemons.</p>
<p>IOS-XE has an improved architecture. It is separated into multiple different software processes, isolating the blast radius of failures. However if you administer IOS-XE devices, you&rsquo;ll notice that the CLI is almost identical to IOS. If you can work with IOS, you can work with IOS-XE.</p>
<h2 id="objectives">Objectives</h2>
<p>For each vendor, I will be using Ansible to configure two routers/switches/firewalls/appliances.</p>
<p>One will serve as the <em>Edge</em> router, connecting to the Internet and also via BGP to the <strong>Net Server</strong>. The <strong>Net Server</strong> is a CentOS 8 Virtual Machine acting as a route server, syslog collector and TACACS+ server (detailed in <a href="/posts/ansible-for-networking-part-2-lab">The Lab Environment</a>)</p>
<p>The other will be an <em>internal</em> router, performing <strong>core</strong> functions (i.e. internal routing rather than external).</p>
<h3 id="edge-router">Edge router</h3>
<p>The edge router will run the following: -</p>
<ul>
<li><strong>External BGP</strong> (eBGP) to the <strong>Net Server</strong>
<ul>
<li>Advertising internal networks</li>
</ul>
</li>
<li><strong>Internal BGP</strong> (iBGP) to the <strong>Internal</strong> router
<ul>
<li>Advertising any routes received from the <strong>Net Server</strong></li>
<li>Advertising a default route (for internet access)</li>
</ul>
</li>
<li><strong>OSPF</strong>
<ul>
<li>Advertising loopbacks and internal networks between both routers</li>
</ul>
</li>
<li><strong>IPv4</strong> and <strong>IPv6</strong> routing
<ul>
<li>Using OSPFv3 (for <strong>IPv6</strong> support)</li>
<li>Using the IPv6 <em>Address Family</em> for BGP</li>
</ul>
</li>
<li><strong>SNMPv3</strong> for monitoring</li>
<li><strong>IPv4 NAT</strong> to allow internet access
<ul>
<li>I cannot run IPv6 for internet access, as my current ISP does not support IPv6</li>
</ul>
</li>
<li><strong>Logging</strong> via Syslog to the <strong>Net Server</strong></li>
<li><strong>Authentication, Authorization and Accounting</strong> (AAA) via TACACS+ to the <strong>Net Server</strong></li>
<li><strong>Access Lists</strong> on the port facing the <strong>Net Server</strong></li>
</ul>
<p>Usually you would also place some form of <code>access-list</code> or other filtering on your ports facing the internet, but this is in a lab environment and already behind a firewall.</p>
<h3 id="internal-router">Internal router</h3>
<p>The internal router runs a subset of the functions that the edge router does: -</p>
<ul>
<li><strong>Internal BGP</strong> (iBGP) to the <strong>Edge</strong> router
<ul>
<li>Receiving any routes received from the <strong>Net Server</strong></li>
<li>Receiving a default route (for internet access)</li>
</ul>
</li>
<li><strong>OSPF</strong>
<ul>
<li>Advertising loopbacks and internal networks between both routers</li>
</ul>
</li>
<li><strong>IPv4</strong> and <strong>IPv6</strong> routing
<ul>
<li>Using OSPFv3 (for <strong>IPv6</strong> support)</li>
<li>Using the IPv6 <em>Address Family</em> for BGP</li>
</ul>
</li>
<li><strong>SNMPv3</strong> for monitoring</li>
<li><strong>Logging</strong> via Syslog to the <strong>Net Server</strong></li>
<li><strong>Authentication, Authorization and Accounting</strong> (AAA) via TACACS+ to the <strong>Net Server</strong></li>
</ul>
<h2 id="ansible-network-automation">Ansible Network Automation</h2>
<p>When managing a server with Ansible (whether Linux, BSD, Windows, or otherwise), it is often done remotely over SSH (or WinRM for Windows). You also need a user on the host that has the appropriate privileges to make the changes that are detailed in your Ansible playbooks.</p>
<p>It is important to note that while Ansible is agentless (i.e. it does not require the installation of a dedicated agent/daemon on the destination machine), it does assume an installation of Python to run the tasks (or Powershell, in the case of Windows).</p>
<p>In the case of network kit, there is no guarantee Python would be installed. In fact in many cases (Cisco IOS being one of them), your access is limited to the vendor-provided command line interface (CLI).</p>
<p>To overcome this, Ansible can use a &ldquo;connection plugin&rdquo; (the method that Ansible will use to connect to a destination host) called <code>network_cli</code>. This is baked directly into Ansible itself, rather than requiring the installation of additional plugins.</p>
<h3 id="network_cli"><code>network_cli</code>?</h3>
<p>Ansible&rsquo;s most used connection plugin is the <code>ssh</code> plugin, which will make changes to the destination host over a standard SSH connection. The <code>network_cli</code> plugin differs in a number of ways.</p>
<p>The Ansible modules run on the control host (i.e. where Ansible runs from) rather than directly on the destination host. You probably wouldn&rsquo;t notice the difference in most of your tasks. Where this does make a difference is when using <code>template</code> or <code>copy</code>. These would either copy a file directly to the destination host, or generate a file from the provided template on the destination host. If you try and do this with the <code>network_cli</code> plugin, you&rsquo;ll find the file on your Ansible control host, rather than the destination.</p>
<p>When Ansible requires privilege escalation, you use the <strong>become</strong> option. This allows Ansible to use <strong>sudo</strong> (or <strong>doas</strong>) to make changes that require elevated privileges. When using <code>network_cli</code>, an additional method is available called <code>enable</code>. &ldquo;Enable&rdquo; mode is equivalent to <strong>sudo</strong> on network devices, allowing elevated privileges and the ability to configure and change the destination host.</p>
<p>Finally, Ansible modules are platform-specific. For example, the <code>ios_bgp</code> would not work on a Fortinet device. While there are some modules that only work on some platforms (the <strong>firewalld</strong> module is not going to work OpenBSD for example), most Linux and *nix-based operating systems support the standard <code>copy</code>, <code>template</code>, <code>file</code> and many other common modules.</p>
<p>The <code>network_cli</code> plugin still uses SSH to connect to the destination devices, so if you already manage your devices with SSH, moving to Ansible instead requires no additional firewall/access changes.</p>
<h3 id="other-connection-plugins">Other connection plugins</h3>
<p>While most of the network modules support <code>network_cli</code>, some of them use the <code>netconf</code> plugin instead (which is XML over SSH, rather than the CLI over SSH). This is not the case with any of the Cisco IOS modules I have used, but when I cover Juniper&rsquo;s JunOS, this will be required.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>To be able to manage a Cisco IOS device with Ansible, some steps are required to allow access to the routers, and also some changes to the default Ansible connection configuration is required.</p>
<h3 id="ansible-configuration">Ansible Configuration</h3>
<p>The following defaults are required to use Ansible with Cisco IOS: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">ansible_user</span>: ansible
<span style="color:#66d9ef">ansible_connection</span>: network_cli
<span style="color:#66d9ef">ansible_network_os</span>: ios
<span style="color:#66d9ef">ansible_ssh_pass</span>: <span style="color:#75715e">###REDACTED### </span>
<span style="color:#66d9ef">ansible_become</span>: yes
<span style="color:#66d9ef">ansible_become_method</span>: enable
<span style="color:#66d9ef">ansible_become_password</span>: <span style="color:#75715e">###REDACTED### </span>
</code></pre></div><p>The <code>ansible_user</code> and <code>ansible_become</code> options are no different than when managing a server or server(s). The additional options are: -</p>
<ul>
<li><code>ansible_connection</code> - The connection plugin used to connect to the devices (<code>network_cli</code> in this case)</li>
<li><code>ansible_network_os</code> - For a list of supported network operating systems, see <a href="https://github.com/ansible/ansible/blob/devel/docs/docsite/rst/network/user_guide/platform_index.rst">here</a></li>
<li><code>ansible_ssh_pass</code> - Cisco IOS does support public key authentication, but few places (in my experience) use it</li>
<li><code>ansible_become_method</code> - Uses <code>enable</code> for privileged access</li>
<li><code>ansible_become_password</code> - <code>enable</code> mode requires another password (unless your TACACS+ server supports putting the user directly in <code>enable</code> mode on login), so this is supplied here</li>
</ul>
<p>As you will likely need to store passwords, it would be best to use Ansible Vault to store them (more information on this the <a href="/posts/ansible-for-networking-part-2-lab">previous post</a> in this series)</p>
<h3 id="cisco-ios-configuration">Cisco IOS Configuration</h3>
<p>The prerequisites for allowing Ansible access to manage an IOS device are: -</p>
<ul>
<li>Create a user with the correct privilege level</li>
<li>Create an <code>enable</code> password</li>
<li>Give the device a <code>hostname</code> and <code>domain name</code> (SSH will not generate keys on IOS without a <code>domain name</code>)</li>
<li>Enable SSH access</li>
<li>Generate an SSH key on the device</li>
</ul>
<p>Below shows how to do all of the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">cisco&gt; enable
cisco# conf t
Enter configuration commands, one per line.  End with CNTL/Z.

! Add service password-encryption so that passwords are not shown in plain-text in the configuration
cisco<span style="color:#f92672">(</span>config<span style="color:#f92672">)</span><span style="color:#75715e"># service password-encryption</span>

! Add the ansible user - Privilege level <span style="color:#ae81ff">15</span> is equivalent to full admin rights
cisco<span style="color:#f92672">(</span>config<span style="color:#f92672">)</span><span style="color:#75715e"># username ansible privilege 15 password 0 $PASSWORD-HERE$</span>

! Create the enable password
cisco<span style="color:#f92672">(</span>config<span style="color:#f92672">)</span><span style="color:#75715e"># enable secret 0 $ENABLE-PASSWORD$</span>

! Give the device a hostname
cisco<span style="color:#f92672">(</span>config<span style="color:#f92672">)</span><span style="color:#75715e"># hostname cisco-01</span>

! Give the device a domain name
cisco-01<span style="color:#f92672">(</span>config<span style="color:#f92672">)</span><span style="color:#75715e"># ip domain name stubob.lab</span>

! Enable SSH Access
cisco-01<span style="color:#f92672">(</span>config<span style="color:#f92672">)</span><span style="color:#75715e"># line vty 0 4         &lt;---- Allows 5 remote users at once</span>
cisco-01<span style="color:#f92672">(</span>config-line<span style="color:#f92672">)</span><span style="color:#75715e"># transport input ssh &lt;--- Allows SSH, disables telnet</span>
cisco-01<span style="color:#f92672">(</span>config-line<span style="color:#f92672">)</span><span style="color:#75715e"># exit</span>
cisco-01<span style="color:#f92672">(</span>config<span style="color:#f92672">)</span><span style="color:#75715e"># ip ssh version 2</span>

! Generate SSH key - Using RSA and a modulus of <span style="color:#ae81ff">2048</span> <span style="color:#f92672">(</span>change these to suit your needs/environment<span style="color:#f92672">)</span>
cisco-01<span style="color:#f92672">(</span>config<span style="color:#f92672">)</span><span style="color:#75715e"># crypto key generate rsa modulus 2048</span> 
% The key modulus size is <span style="color:#ae81ff">2048</span> bits
% Generating <span style="color:#ae81ff">2048</span> bit RSA keys, keys will be non-exportable...
<span style="color:#f92672">[</span>OK<span style="color:#f92672">]</span> <span style="color:#f92672">(</span>elapsed time was <span style="color:#ae81ff">0</span> seconds<span style="color:#f92672">)</span>

cisco-01<span style="color:#f92672">(</span>config<span style="color:#f92672">)</span><span style="color:#75715e"># end</span>

! Write your configuration - Important, otherwise you will lose it <span style="color:#66d9ef">if</span> the devices powers off or reboots
cisco-01# copy running-config startup-config
</code></pre></div><p>Once the above is done, add the device into your Ansible inventory. My inventory file looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#66d9ef">[ios]</span>
<span style="color:#a6e22e">cisco-01 ansible_host</span><span style="color:#f92672">=</span><span style="color:#e6db74">10.15.30.23</span>
<span style="color:#a6e22e">cisco-02 ansible_host</span><span style="color:#f92672">=</span><span style="color:#e6db74">10.15.30.24</span>
</code></pre></div><p>By putting in a name as the &ldquo;inventory hostname&rdquo;, but supplying the IP as the <code>ansible_host</code>, I can refer to the devices by their name in my Playbooks and tasks, but without necessarily requiring it to match DNS.</p>
<h4 id="verification">Verification</h4>
<p>Can we contact both devices?</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ansible ios -m ios_facts | grep -i hostname
        <span style="color:#e6db74">&#34;ansible_net_hostname&#34;</span>: <span style="color:#e6db74">&#34;cisco-02&#34;</span>,
        <span style="color:#e6db74">&#34;ansible_net_hostname&#34;</span>: <span style="color:#e6db74">&#34;cisco-01&#34;</span>,

</code></pre></div><p>That will do it!</p>
<p>Another quirk of the <code>network_cli</code> connection plugin is that if you just use the <code>ping</code> module for testing (i.e. <code>ansible all -m ping</code>), it is actually the Ansible control host responding, not the routers themselves. Using the <code>ios_facts</code> module forces facts to be retrieved from the destination hosts instead (proving connectivity).</p>
<h2 id="setup">Setup</h2>
<p>Below is a quick diagram of the setup: -</p>
<p><img src="/img/ansible_networking/ios.png" alt="IOS Diagram"></p>
<p>I covered the hypervisor network setup in the previous part of this series, but for quick explanation: -</p>
<ul>
<li>Internet access is via the default KVM NAT bridge</li>
<li>Management is via an isolated KVM bridge</li>
<li>Inter-device communication is done via VLANs, tagged over a KVM bridge</li>
</ul>
<p>Using VLANs allows me to to forgo provisioning new interfaces or reconfigure existing ones to work with different kit. It also means that if and when I decide to run all of the vendors together, all the interfaces still exist.</p>
<h3 id="vlans-ip-addressing-and-autonomous-system-numbers">VLANs, IP addressing and Autonomous System numbers</h3>
<p>All of the VLANs, IPs and AS numbers are formed from an arbitrary ID, which I assign per vendor to make the addressing/configuration unique. My reasoning for this are explained in <a href="/posts/ansible-for-networking-part-2-lab/">Part 2</a>.</p>
<p>The ID chosen for Cisco IOS is <code>01</code>.</p>
<h4 id="vlans">VLANs</h4>
<p>The VLANs used will be: -</p>
<ul>
<li>VLAN101 between the <em>edge</em> router and <em>netsvr-01</em></li>
<li>VLAN201 between the <em>edge</em> router and <em>internal</em> router</li>
</ul>
<h4 id="ip-addressing">IP Addressing</h4>
<ul>
<li>IPv4 Subnet on VLAN101: <code>10.100.101.0/24</code>
<ul>
<li><em>edge</em> router - <code>10.100.101.253/24</code></li>
<li><em>netsvr-01</em> - <code>10.100.101.254/24</code></li>
</ul>
</li>
<li>IPv4 Subnet on VLAN201: 10.100.201.0/24
<ul>
<li><em>edge</em> router - <code>10.100.201.254/24</code></li>
<li><em>internal</em> router - <code>10.100.201.253/24</code></li>
</ul>
</li>
<li>IPv6 Subnet on VLAN101: <code>2001:db8:101::/64</code>
<ul>
<li><em>edge</em> router - <code>2001:db8:101::f/64</code></li>
<li><em>netsvr-01</em> - <code>2001:db8:101:ffff/64</code></li>
</ul>
</li>
<li>IPv6 Subnet on VLAN201: 2001:db8:201::/64
<ul>
<li><em>edge</em> router - <code>2001:db8:201::a/64</code></li>
<li><em>internal</em> router - <code>2001:db8:201:f/64</code></li>
</ul>
</li>
<li>IPv4 Loopback Addressing
<ul>
<li><em>edge</em> router - <code>192.0.2.101/32</code></li>
<li><em>internal</em> router - <code>192.0.2.201/32</code></li>
</ul>
</li>
<li>IPv6 Loopback Address
<ul>
<li><em>edge</em> router - <code>2001:db8:901:beef::1/128</code></li>
<li><em>internal</em> router - <code>2001:db8:901:beef::2/128</code></li>
</ul>
</li>
</ul>
<h4 id="bgp-autonomous-system">BGP Autonomous System</h4>
<p>The BGP <a href="https://en.wikipedia.org/wiki/Autonomous_system_(Internet)">Autonomous System</a> number will be <code>AS65101</code>.</p>
<h2 id="configuration">Configuration</h2>
<p>Now we have covered the prerequisites, and assigned all of our network information, we can now work on making changes to the routers.</p>
<h3 id="system-tasks">System tasks</h3>
<p>This role covers tasks that affect the system itself, rather than anything like routing or management. For this lab, I update the <strong>banners</strong> (what you would see when logging into the device, or changing modes), enabling <strong>logging</strong> to a <strong>syslog</strong> server, and also enabling password encryption (so that passwords are not stored in clear text in the configuration).</p>
<p>To do this, I have created an Ansible role called <code>system</code>, and placed it in my <code>roles</code> directory: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ tree
├── ansible.cfg
├── ansible.log
├── group_vars
│   └── ios
├── host_vars
│   ├── cisco-01.yml
│   └── cisco-02.yml
├── inventory
├── ios.yaml
└── roles
    └── system
        ├── defaults
        │   └── main.yml
        ├── files
        ├── handlers
        │   └── main.yml
        ├── meta
        │   └── main.yml
        ├── README.md
        ├── tasks
        │   └── main.yml
        ├── templates
        ├── tests
        │   ├── inventory
        │   └── test.yml
        └── vars
            └── main.yml
</code></pre></div><p>The role is created by running <code>ansible-galaxy init system</code>, which creates a common directory structure and base files to update/replace as you need.</p>
<p>I&rsquo;d like to thank <a href="https://jon.sprig.gs">Jon Spriggs</a> for pointing out that Ansible will automatically look for roles in a <code>roles/</code> directory. Previously all my roles were in the parent directory. This is a much cleaner way of separating them out!</p>
<h4 id="playbook">Playbook</h4>
<p>The contents of the Playbook can be seen below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for system</span>
- <span style="color:#66d9ef">name</span>: Remove unneeded banners
  <span style="color:#66d9ef">ios_banner</span>:
    <span style="color:#66d9ef">banner</span>: <span style="color:#e6db74">&#34;{{ item }}&#34;</span>
    <span style="color:#66d9ef">state</span>: absent
  <span style="color:#66d9ef">loop</span>:
  - motd
  - exec
  - incoming

- <span style="color:#66d9ef">name</span>: Update login banner
  <span style="color:#66d9ef">ios_banner</span>:
    <span style="color:#66d9ef">banner</span>: login
    <span style="color:#66d9ef">text</span>: <span style="color:#e6db74">|
</span><span style="color:#e6db74">      ----------------------------------------</span>
      <span style="color:#e6db74">|
</span><span style="color:#e6db74">      | This banner was generated by Ansible </span>
      <span style="color:#e6db74">|
</span><span style="color:#e6db74">      ----------------------------------------</span>
      <span style="color:#e6db74">|
</span><span style="color:#e6db74">      | You are logged into {{ inventory_hostname }}</span>
      <span style="color:#e6db74">| 
</span><span style="color:#e6db74">      ----------------------------------------</span>
    <span style="color:#66d9ef">state</span>: present

- <span style="color:#66d9ef">name</span>: Configure syslog
  <span style="color:#66d9ef">ios_logging</span>:
    <span style="color:#66d9ef">dest</span>: host
    <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;{{ log_host }}&#34;</span>
    <span style="color:#66d9ef">state</span>: present

- <span style="color:#66d9ef">name</span>: Configure log buffer
  <span style="color:#66d9ef">ios_logging</span>:
    <span style="color:#66d9ef">dest</span>: buffered
    <span style="color:#66d9ef">level</span>: informational
    <span style="color:#66d9ef">state</span>: present

- <span style="color:#66d9ef">name</span>: Enable service password-encryption
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - service password-encryption
</code></pre></div><p>This playbook achieves the following: -</p>
<h5 id="removing-unneeded-banners">Removing unneeded banners</h5>
<p><strong>Ansible module</strong>: <code>ios_banner</code></p>
<p>The first task removes the <strong>motd</strong>, <strong>exec</strong> and <strong>incoming</strong> banners (if any are set). I tend to only use <strong>login</strong> banners, so this is more personal preference.</p>
<h5 id="update-the-login-banner">Update the <strong>login</strong> banner</h5>
<p><strong>Ansible module</strong>: <code>ios_banner</code></p>
<p>The pipe after <code>text</code> allows multiline freeform text. You can also supply a template file instead, rather than placing all of this in a Playbook. Using a template would be useful in scenarios where you have a specific banner provided by your legal department.</p>
<p>The <code>{{ inventory_hostname }}</code> is an Ansible in-built variable, that matches the name defined in your Ansible inventory. In my case, these are <strong>cisco-01</strong> and <strong>cisco-02</strong>.</p>
<p>Now if you login to the device, you would see this: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ssh ansible@10.15.30.24

----------------------------------------
|
| This banner was generated by Ansible 
|
----------------------------------------
|
| You are logged into cisco-02
| 
----------------------------------------
Password: 
</code></pre></div><h5 id="configure-syslog">Configure syslog</h5>
<p><strong>Ansible module</strong>: <code>ios_logging</code></p>
<p>This task configures logging to syslog, using a variable called <code>log_host</code>. This variable is defined in my <code>group_vars</code> file: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ cat group_vars/ios | grep -i log
log_host: 10.100.101.254
</code></pre></div><p>In the IOS configuration, this creates: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">logging host 10.100.101.254
</code></pre></div><h5 id="configure-log-buffer">Configure Log Buffer</h5>
<p><strong>Ansible module</strong>: <code>ios_logging</code></p>
<p>This task adds local logging on the device, so you can run <code>show log</code> on a device and see any relevant and/or important events (e.g. BGP session resets, interface status changes etc)</p>
<p>In the IOS configuration, this creates: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">logging buffered 4096 informational
</code></pre></div><h5 id="enabling-password-encryption">Enabling password encryption</h5>
<p><strong>Ansible module</strong>: <code>ios_config</code></p>
<p>This stops passwords in the configuration file being viewed as plain text. As you can see, this uses the <code>ios_config</code> module.</p>
<h4 id="the-ios_config-module">The <code>ios_config</code> module</h4>
<p>When an Ansible module doesn&rsquo;t already exist, you can use <code>ios_config</code> to supply configuration extracts instead. For example, in the above there is no module for enabling different IOS &ldquo;services&rdquo;. This can be everything from the <code>password-encryption</code> module, to <code>tcp-keepalive-in</code> (i.e. enabling keepalives for inbound TCP sessions).</p>
<p>You have multiple options for how you can use <code>ios_config</code>. You can use the <code>line</code> option to specify the lines to be added, directly in the playbook. Alternatively you can use a template file (specifying <code>src: $FILENAME</code> rather than <code>lines</code>).</p>
<p>This makes some of the tasks less portable than others. If you decide you want to move to another vendor for your network, rather than simply targeting different modules (e.g. Arista&rsquo;s <code>eos_bgp</code> rather than Cisco&rsquo;s <code>ios_bgp</code>), you have to recreate full configuration snippets using the correct vendor syntax instead.</p>
<p>During this article (and further posts in the series) you&rsquo;ll see what modules are already available, and what you will need to recreate yourself with the <code>ios_config</code> module.</p>
<h3 id="interfaces">Interfaces</h3>
<p>The next role will configure all the interfaces on each device. This includes interface naming/descriptions, IP addressing, creation of sub-interfaces (i.e. those that use VLANs), and enable the interfaces. It also creates Loopback interfaces, so that our routing protocols can have consistent (and unique) identifiers, and allows routing not tied to a specific physical interface.</p>
<p>The role was created in the <code>roles</code> directory, using <code>ansible-galaxy init interfaces</code>.</p>
<h4 id="playbook-1">Playbook</h4>
<p>The contents of the Playbook are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for interfaces</span>
- <span style="color:#66d9ef">name</span>: Configure sub-interfaces first
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">src</span>: subints.j2

- <span style="color:#66d9ef">name</span>: Configure interfaces - Status and Descriptions
  <span style="color:#66d9ef">ios_interfaces</span>:
    <span style="color:#66d9ef">config</span>:
      - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;{{ item.ios_if }}&#34;</span>
        <span style="color:#66d9ef">description</span>: <span style="color:#e6db74">&#34;{{ item.desc }}&#34;</span>
        <span style="color:#66d9ef">enabled</span>: <span style="color:#e6db74">&#34;{{ item.enabled }}&#34;</span>
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>

- <span style="color:#66d9ef">name</span>: Configure interfaces - L3 IPv4
  <span style="color:#66d9ef">ios_l3_interfaces</span>:
    <span style="color:#66d9ef">config</span>:
      - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;{{ item.ios_if }}&#34;</span>
        <span style="color:#66d9ef">ipv4</span>:
        - <span style="color:#66d9ef">address</span>: <span style="color:#e6db74">&#34;{{ item.ipv4 }}&#34;</span>
  <span style="color:#66d9ef">when</span>: item.ipv4 is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>

- <span style="color:#66d9ef">name</span>: Configure interfaces - L3 IPv6
  <span style="color:#66d9ef">ios_l3_interfaces</span>:
    <span style="color:#66d9ef">config</span>:
      - <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;{{ item.ios_if }}&#34;</span>
        <span style="color:#66d9ef">ipv6</span>:
        - <span style="color:#66d9ef">address</span>: <span style="color:#e6db74">&#34;{{ item.ipv6 }}&#34;</span>
  <span style="color:#66d9ef">when</span>: item.ipv6 is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
</code></pre></div><p>Each task will be explained in the following sections.</p>
<h5 id="configuring-sub-interfaces">Configuring sub-interfaces</h5>
<p><strong>Ansible module</strong>: <code>ios_config</code></p>
<p>Currently, no IOS interface module (either <code>ios_interfaces</code>, <code>ios_l2_interfaces</code> or <code>ios_l3_interfaces</code>) support sub-interfaces. If you are configuring a switch, there are multiple IOS-based VLAN modules that can achieve this, but none of the modules for routers fully support sub-interfaces.</p>
<p>Confusingly, all the IOS modules mentioned can update descriptions, add IP addressing and enable the interfaces. However what they cannot do is set the <strong>encapsulation</strong> of the interface, which is required to allow a sub-interface to use VLANs.</p>
<p>The template file itself is in <code>roles/interfaces/templates/</code>, and looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-jinja" data-lang="jinja"><span style="color:#75715e">{%</span> <span style="color:#66d9ef">for</span> interface <span style="color:#66d9ef">in</span> interfaces <span style="color:#75715e">%}</span>
<span style="color:#75715e">{%</span> <span style="color:#66d9ef">if</span> interface<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;subint&#39;</span><span style="color:#f92672">]</span> <span style="color:#66d9ef">is</span> <span style="color:#a6e22e">defined</span> <span style="color:#75715e">%}</span>
<span style="color:#75715e">{%</span> <span style="color:#66d9ef">for</span> vlan <span style="color:#66d9ef">in</span> interface<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;subint&#39;</span><span style="color:#f92672">][</span><span style="color:#e6db74">&#39;vlans&#39;</span><span style="color:#f92672">]</span> <span style="color:#75715e">%}</span>
interface <span style="color:#75715e">{{</span> interface<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;ios_if&#39;</span><span style="color:#f92672">]</span> <span style="color:#75715e">}}</span>.<span style="color:#75715e">{{</span> vlan <span style="color:#75715e">}}</span>
 encapsulation dot1q <span style="color:#75715e">{{</span> vlan <span style="color:#75715e">}}</span>
<span style="color:#75715e">{%</span> <span style="color:#66d9ef">endfor</span> <span style="color:#75715e">%}</span>
<span style="color:#75715e">{%</span> <span style="color:#66d9ef">endif</span> <span style="color:#75715e">%}</span>
<span style="color:#75715e">{%</span> <span style="color:#66d9ef">endfor</span> <span style="color:#75715e">%}</span> 
</code></pre></div><p>To explain how this works, each router has a <code>host-vars</code> file specified (<code>host_vars/$HOSTNAME.yaml</code>). The contents for the interfaces section looks like:  -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;GigabitEthernet0/0&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Management&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.15.30.23/24&#34;</span>
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;GigabitEthernet0/1&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;VLAN Bridge&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">subint</span>:
      <span style="color:#66d9ef">vlans</span>:
      - <span style="color:#ae81ff">101</span>
      - <span style="color:#ae81ff">201</span>
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;GigabitEthernet0/1.101&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To netsvr&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.100.101.253/24&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:101::f/64&#34;</span>
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;GigabitEthernet0/1.201&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To cisco-02&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.100.201.254/24&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:201::a/64&#34;</span>
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;GigabitEthernet0/2&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To the Internet&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;dhcp&#34;</span>
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;loopback0&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;192.0.2.101/32&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:901:beef::1/128&#34;</span>
</code></pre></div><p>The above is a list of dictionaries (or array of maps, depending on your chosen nomenclature). When building the template, Ansible will load this list, and then loop through the contents. The logic for each interface in the list is: -</p>
<ul>
<li>If there is a field called &ldquo;subint&rdquo;, then&hellip;</li>
<li>Create sub-interfaces, with the syntax <code>interface $interface-name.$vlan</code></li>
<li>Add the line <code>encapsulation dot1q $vlan</code> to enable VLAN tagging</li>
</ul>
<p>In the above scenario, this generates: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">interface Gi0/1.101 
 encapsulation dot1q 101
interface Gi0/1.201 
 encapsulation dot1q 201
</code></pre></div><p>After this, the other interface modules (<code>ios_interfaces</code> and <code>ios_l3_interfaces</code>) can change descriptions, add IP addressing and enable these sub-interfaces.</p>
<h5 id="status-and-descriptions">Status and descriptions</h5>
<p><strong>Ansible module</strong>: <code>ios_interfaces</code></p>
<p>This task updates the description, and sets the status of the interfaces. The <code>loop</code> is used so that the task is applied to all interfaces in the aforementioned list (sourced from <code>host_vars</code>). The configuration that is generated by the above on the <em>edge</em> router (<code>cisco-01</code>) looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">interface Loopback0
 description Loopback

interface GigabitEthernet0/0
 description Management

interface GigabitEthernet0/1
 description VLAN Bridge

interface GigabitEthernet0/1.101
 description To netsvr

interface GigabitEthernet0/1.201
 description To cisco-02

interface GigabitEthernet0/2
 description To the Internet
</code></pre></div><p>If you changed the <code>enabled</code> variable on one of the interfaces to <code>false</code>, you would also get: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">interface Gi0/2
 description To the internet
 shutdown
</code></pre></div><h5 id="ipv4-addressing">IPv4 addressing</h5>
<p><strong>Ansible module</strong>: <code>ios_l3_interfaces</code></p>
<p>This task applies an IPv4 address to each interface (where enabled). As part of the task, we are using the <code>when</code> clause: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  <span style="color:#66d9ef">when</span>: item.ipv4 is defined &lt;---<span style="color:#e6db74">-
</span><span style="color:#e6db74">  loop: &#34;{{ interfaces }}&#34;</span>
</code></pre></div><p>It is important that this comes <strong>before</strong> you define your <code>loop</code>, and not after it. If you place the <code>when</code> clause after the <code>loop</code>, you will likely get an error that the variable <code>item</code> is not defined. <code>item</code> is Ansible&rsquo;s in-built variable that it uses for each element in a loop.</p>
<p>By doing this, any interface which has the field <code>ipv4</code> will have an IPv4 address added. The configuration generated by this is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">interface Loopback0
 ip address 192.0.2.101 255.255.255.255

interface GigabitEthernet0/0
 ip address 10.15.30.23 255.255.255.0

interface GigabitEthernet0/1
 no ip address

interface GigabitEthernet0/1.101
 ip address 10.100.101.253 255.255.255.0

interface GigabitEthernet0/1.201
 ip address 10.100.201.254 255.255.255.0

interface GigabitEthernet0/2
 ip address dhcp
</code></pre></div><p>There are three points to mention here: -</p>
<ul>
<li>Cisco IOS does not support the CIDR (i.e. <code>X.X.X.X/Y</code>) format of IP addressing, instead requiring full subnet masks
<ul>
<li>Thankfully, the Ansible module already translates CIDR-formatted addressing to IP and subnet mask format</li>
</ul>
</li>
<li>You can supply the <code>dhcp</code> word as an IPv4 address, rather than requiring different syntax for running the interface as a DHCP client</li>
<li>The interface <code>GigabitEthernet0/1</code> does not have an IP address, so the <code>no ip address</code> line is the default configuration for a Layer 3 interface</li>
</ul>
<h5 id="ipv6-addressing">IPv6 addressing</h5>
<p><strong>Ansible module</strong>: <code>ios_l3_interfaces</code></p>
<p>Similar to the IPv4 addressing task, this applies IPv6 addressing to all interfaces it is defined for. The only major differences are that we are looking for the <code>ipv6</code> field in our interface list, rather than <code>ipv4</code>, and that we specify the <code>ipv6</code> clause in our task to apply the address.</p>
<p>You could combine applying IPv4 and IPv6 addressing into a single task, but this would also require logic to say whether both are required, or if only one of IPv4 and IPv6 is required. I personally felt being a bit more verbose in the playbook makes it a bit easier to read (and thus easier to debug if required).</p>
<p>The resulting configuration is as such: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">interface Loopback0
 ipv6 address 2001:DB8:901:BEEF::1/128

interface GigabitEthernet0/0

interface GigabitEthernet0/1

interface GigabitEthernet0/1.101
 ipv6 address 2001:DB8:101::F/64

interface GigabitEthernet0/1.201
 ipv6 address 2001:DB8:201::A/64
</code></pre></div><p>As you can see, because IPv6 addressing is not added to every interface, some interfaces show nothing. This is to be expected, as IPv6 addressing is not expected by default in Cisco IOS.</p>
<h4 id="caveats">Caveats</h4>
<p>There is an odd quirk I have found with these tasks, specifically related to configuring Loopback interfaces. For whatever reason, when using the <code>ios_l3_interfaces</code> module, it will not actually apply configuration to a loopback if the name starts with an uppercase L (e.g. <code>Loopback0</code> or <code>Loopback2001</code>). Instead, it needs to supplied as all lowercase (see the above <code>host_vars</code>, where the <code>ios_if</code> field for the loopback is <code>loopback0</code> rather than <code>Loopback0</code>).</p>
<p>Unfortunately this means that other modules that rely on the interface name using an uppercase L (because they are matching against lines in the running configuration, which will always display it with an uppercase L) will register changes (even though the only difference is the casing).</p>
<p>Another point to note is that if you want your tasks to <strong>idempotent</strong> (i.e. where a task can be applied multiple times without making changes, other than on the initial application), you need to specify the interface names in full.</p>
<p>For those who are used to typing <code>int Gi0/1</code> to make changes to <code>GigabitEthernet0/1</code>, you&rsquo;ll need to break that habit when writing your playbooks and variables.</p>
<h4 id="verification-1">Verification</h4>
<p>All of this means nothing if it doesn&rsquo;t work, so I&rsquo;ll show some basic tests to confirm everything works as expected</p>
<p><strong>cisco-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show IPs
cisco-01#show ip int brief
Interface                  IP-Address      OK? Method Status                Protocol
GigabitEthernet0/0         10.15.30.23     YES NVRAM  up                    up      
GigabitEthernet0/1         unassigned      YES NVRAM  up                    up      
GigabitEthernet0/1.101     10.100.101.253  YES NVRAM  up                    up      
GigabitEthernet0/1.201     10.100.201.254  YES NVRAM  up                    up      
GigabitEthernet0/2         192.168.122.86  YES DHCP   up                    up      
Loopback0                  192.0.2.101     YES NVRAM  up                    up      

cisco-01#show ipv6 int brief
GigabitEthernet0/0     [up/up]
    unassigned
GigabitEthernet0/1     [up/up]
    unassigned
GigabitEthernet0/1.101 [up/up]
    FE80::5054:FF:FEDD:4D5C
    2001:DB8:101::F
GigabitEthernet0/1.201 [up/up]
    FE80::5054:FF:FEDD:4D5C
    2001:DB8:201::A
GigabitEthernet0/2     [up/up]
    unassigned
Loopback0              [up/up]
    FE80::5054:FF:FE64:DD2A
    2001:DB8:901:BEEF::1


! Show interface statuses and descriptions
cisco-01#show int description 
Interface                      Status         Protocol Description
Gi0/0                          up             up       Management
Gi0/1                          up             up       VLAN Bridge
Gi0/1.101                      up             up       To netsvr
Gi0/1.201                      up             up       To cisco-02
Gi0/2                          up             up       To the Internet
Lo0                            up             up       Loopback

! Ping to netsvr-01 on IPv4 and IPv6

cisco-01#ping 10.100.101.254
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 10.100.101.254, timeout is 2 seconds:
!!!!!

cisco-01#ping 2001:db8:101::ffff
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 2001:DB8:101::FFFF, timeout is 2 seconds:
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 1/1/1 ms

! Ping to cisco-02 on IPv4 and IPv6
cisco-01#ping 10.100.201.253    
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 10.100.201.253, timeout is 2 seconds:
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 1/1/2 ms

cisco-01#ping 2001:db8:201::f
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 2001:DB8:201::F, timeout is 2 seconds:
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 1/1/4 ms
</code></pre></div><p><strong>cisco-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show IPs
cisco-02#show ip int brief
Interface                  IP-Address      OK? Method Status                Protocol
GigabitEthernet0/0         10.15.30.24     YES NVRAM  up                    up      
GigabitEthernet0/1         unassigned      YES NVRAM  up                    up      
GigabitEthernet0/1.201     10.100.201.253  YES NVRAM  up                    up      
Loopback0                  192.0.2.201     YES NVRAM  up                    up      

cisco-02#show ipv6 int brief
GigabitEthernet0/0     [up/up]
    unassigned
GigabitEthernet0/1     [up/up]
    unassigned
GigabitEthernet0/1.201 [up/up]
    FE80::5054:FF:FE3A:2C40
    2001:DB8:201::F
Loopback0              [up/up]
    FE80::5054:FF:FE89:5EBB
    2001:DB8:901:BEEF::2

! Show interface statuses and descriptions
cisco-02#show int desc
Interface                      Status         Protocol Description
Gi0/0                          up             up       Management
Gi0/1                          up             up       VLAN Bridge
Gi0/1.201                      up             up       To cisco-01
Lo0                            up             up       Loopback

! Ping to cisco-01 on IPv4 and IPv6
cisco-02#ping 10.100.201.254
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 10.100.201.254, timeout is 2 seconds:
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 1/1/2 ms

cisco-02#ping 2001:db8:201::a
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 2001:DB8:201::A, timeout is 2 seconds:
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 1/1/2 ms
</code></pre></div><p>We will not be able to ping between the loopbacks yet, as we do not have routing in place. This will all be covered in the next section.</p>
<h3 id="routing">Routing</h3>
<p>For routing, we are using BGP, OSPF and OSPFv3. As noted, BGP will be used between the <em>edge</em> router and <em>netsvr-01</em> for, advertising internal networks to the Route Server running on <em>netsvr-01</em>. We&rsquo;ll also receive the loopback address of <em>netsvr-01</em> (<code>192.0.2.1/32</code>). We&rsquo;ll also use BGP between the <em>edge</em> router and the <em>internal</em> router to advertise networks received from <em>netsvr-01</em></p>
<p>We&rsquo;ll use OSPF (for IPv4) and OSPFv3 (for IPv6) to advertise the internal networks between the <em>edge</em> router and <em>internal</em> router. These networks will be redistributed into BGP on the <em>edge</em> router, to allow them to be advertised to the <em>netsvr-01</em>.</p>
<p>Finally, we&rsquo;ll advertise a default route from the <em>edge</em> router to the <em>internal</em> router, to allow the <em>internal</em> router to access the internet.</p>
<p>The role is created using <code>ansible-galaxy init routing</code> in the roles directory.</p>
<h4 id="main-playbook">Main Playbook</h4>
<p>The main playbook looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for routing</span>
<span style="color:#75715e">##</span>
- <span style="color:#66d9ef">name</span>: Include OSPF routing
  <span style="color:#66d9ef">include</span>: ospf.yml

- <span style="color:#66d9ef">name</span>: Include OSPFv3 routing
  <span style="color:#66d9ef">include</span>: ospfv3.yml

- <span style="color:#66d9ef">name</span>: Include BGP routing
  <span style="color:#66d9ef">include</span>: bgp.yml
</code></pre></div><p>There aren&rsquo;t many tasks here, but thats because I have separated out OSPF, OSPFv3 and BGP into their own playbooks, and include them as part of the main playbook. This helps for readability of the individual playbooks, rather than one big playbook with lots of tasks.</p>
<h4 id="ospf-playbook">OSPF Playbook</h4>
<p>The OSPF playbook itself looks like: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for routing</span>
<span style="color:#75715e">##</span>
- <span style="color:#66d9ef">name</span>: OSPF Process
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#e6db74">&#34;router ospf 1&#34;</span>

- <span style="color:#66d9ef">name</span>: OSPF Process - Router ID
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>: 
      - <span style="color:#e6db74">&#34;router-id {{ router_id }}&#34;</span>
    <span style="color:#66d9ef">parents</span>: router ospf <span style="color:#ae81ff">1</span>

- <span style="color:#66d9ef">name</span>: OSPF Interfaces
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - ip ospf <span style="color:#ae81ff">1</span> area {{ item.ospf.area }}
    <span style="color:#66d9ef">parents</span>: interface {{ item.ios_if }}
  <span style="color:#66d9ef">when</span>: item.ospf is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>

- <span style="color:#66d9ef">name</span>: OSPF Interfaces - Passive
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - passive-interface {{ item.ios_if }}
    <span style="color:#66d9ef">parents</span>: router ospf <span style="color:#ae81ff">1</span>
  <span style="color:#66d9ef">when</span>: 
    - item.ospf is defined
    - item.ospf.passive is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
</code></pre></div><p>Unfortunately, no Ansible modules currently exist for OSPF, so all of the configuration is applied using the <code>ios_config</code> module.</p>
<h5 id="ospf-process">OSPF Process</h5>
<p><strong>Ansible module</strong>: <code>ios_config</code></p>
<p>This task just enables OSPF, using process ID <code>1</code>.</p>
<p>This generates the configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">router ospf 1
</code></pre></div><h5 id="router-id">Router ID</h5>
<p><strong>Ansible module</strong>: <code>ios_config</code></p>
<p>The Router ID task picks up the <code>router_id</code> variable from <code>host_vars</code>, and applies it as part of the OSPF configuration. So for example, on <code>cisco-02</code> (the <em>internal</em> router), this would generate: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">router ospf 1
 router-id 192.0.2.201
</code></pre></div><h6 id="parents">Parents?</h6>
<p>An option of the <code>ios_config</code> module we haven&rsquo;t seen yet is the <code>parents</code> option. With Cisco, configuration is done hierarchically. To enable some options, you have to enter another hierarchy level before the configuration will apply. In this case, you can&rsquo;t supply the OSPF router ID without entering the <code>router ospf 1</code> hierarchy level first.</p>
<p>The <code>parent</code> option is evaluated sequentially, so for multiple levels of hierarchy, you need to specify them in order. An example would be: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">router bgp 65101
 address-family ipv4 unicast
  neighbor 10.100.101.254 activate
</code></pre></div><p>The syntax that would achieve this is: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>: 
      - neighbor <span style="color:#ae81ff">10.100.101.254</span> 
    <span style="color:#66d9ef">parents</span>:
      - router bgp <span style="color:#ae81ff">65101</span>
      - address-family ipv4 unicast
</code></pre></div><h5 id="ospf-interfaces">OSPF Interfaces</h5>
<p><strong>Ansible module</strong>: <code>ios_config</code></p>
<p>Similar to the Interfaces role, this section will scan through our list of interfaces (sourced from our <code>host_vars</code> per router), and if it finds the <code>ospf</code> field, it will enable OSPF on the interface. The OSPF area is also defined in our <code>host_vars</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;GigabitEthernet0/1.201&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To cisco-01&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.100.201.253/24&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:201::f/64&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;loopback0&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;192.0.2.201/32&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:901:beef::2/128&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
</code></pre></div><p>This would generate the following configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">interface Gi0/1.201
 ip ospf 1 area 0.0.0.0

interface Loopback0
 ip ospf 1 area 0.0.0.0
</code></pre></div><h5 id="ospf-interfaces---passive">OSPF Interfaces - Passive</h5>
<p><strong>Ansible module</strong>: <code>ios_config</code></p>
<p>This is similar to the OSPF interfaces role, except it also adds the Passive option. A passive interface in OSPF is one that has its IPv4 addressing (and subsequent IPv4 route/prefix) included in OSPF, but doesn&rsquo;t attempt to form any OSPF neighbourships over it.</p>
<p>I won&rsquo;t get too far down the rabbit hole of how OSPF works, but this is preferred over redistributing connected networks, because of how OSPF deals with routes received from other protocols.</p>
<p>It is possible for routers in a network to inform others to flush their &ldquo;external&rdquo; routes (i.e. those received from another protocol), even on the router that originated said routes. A weird issue with Quagga refreshing routes when NTP synchronized (i.e. when a server reboots&hellip;) taking down core MPLS in a previous role taught me that!</p>
<p>In this task, we have two <code>when</code> clauses. If they are specified like below (i.e. as a list in YAML syntax), <strong>AND</strong> logic is used (all criteria must be specified, rather than one or more of the criteria): -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  <span style="color:#66d9ef">when</span>: 
    - item.ospf is defined
    - item.ospf.passive is defined
</code></pre></div><p>Again, we source the statements from our <code>host_vars</code>, looping through the interface list: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;GigabitEthernet0/1.201&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To cisco-01&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.100.201.253/24&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:201::f/64&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;loopback0&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;192.0.2.201/32&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:901:beef::2/128&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
</code></pre></div><p>The above <code>host_vars</code> would generate: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">interface Gi0/1.201
 ip ospf 1 area 0.0.0.0

interface Loopback0
 ip ospf 1 area 0.0.0.0

router ospf 1
 passive-interface Loopback0
</code></pre></div><p>As noted before, we need to use the <code>parent</code> option to place the <code>passive-interface $INTERFACE</code> under <code>router ospf 1</code> in the configuration.</p>
<h5 id="verification-2">Verification</h5>
<p>After this, we should be able to see OSPF routes, and we should be able to ping between the loopbacks on the <em>edge</em> router and the <em>internal</em> router</p>
<p><strong>cisco-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show OSPF interfaces
cisco-01#show ip ospf interface brief 
Interface    PID   Area            IP Address/Mask    Cost  State Nbrs F/C
Lo0          1     0.0.0.0         192.0.2.101/32     1     LOOP  0/0
Gi0/1.201    1     0.0.0.0         10.100.201.254/24  1     BDR   1/1
Gi0/1.101    1     0.0.0.0         10.100.101.253/24  1     DR    0/0

! Show OSPF neighbours
cisco-01#show ip ospf neighbor 

Neighbor ID     Pri   State           Dead Time   Address         Interface
192.0.2.201       1   FULL/DR         00:00:38    10.100.201.253  GigabitEthernet0/1.201

! Show routing table
cisco-01#show ip route ospf
Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP
       D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area 
       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2
       E1 - OSPF external type 1, E2 - OSPF external type 2
       i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2
       ia - IS-IS inter area, * - candidate default, U - per-user static route
       o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP
       a - application route
       + - replicated route, % - next hop override, p - overrides from PfR

Gateway of last resort is 192.168.122.1 to network 0.0.0.0

      192.0.2.0/32 is subnetted, 3 subnets
O        192.0.2.201 
           [110/2] via 10.100.201.253, 03:51:43, GigabitEthernet0/1.201

! Ping!
cisco-01#ping 192.0.2.201                   
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 192.0.2.201, timeout is 2 seconds:
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 1/1/1 ms

cisco-01#ping 192.0.2.201 source 192.0.2.101
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 192.0.2.201, timeout is 2 seconds:
Packet sent with a source address of 192.0.2.101 
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 1/1/2 ms
</code></pre></div><p><strong>cisco-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show OSPF interfaces
cisco-02#show ip ospf interface brief 
Interface    PID   Area            IP Address/Mask    Cost  State Nbrs F/C
Lo0          1     0.0.0.0         192.0.2.201/32     1     LOOP  0/0
Gi0/1.201    1     0.0.0.0         10.100.201.253/24  1     DR    1/1

! Show OSPF neighbours
cisco-02#show ip ospf neighbor 

Neighbor ID     Pri   State           Dead Time   Address         Interface
192.0.2.101       1   FULL/BDR        00:00:35    10.100.201.254  GigabitEthernet0/1.201

! Show routing table
cisco-02#show ip route ospf    
Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP
       D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area 
       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2
       E1 - OSPF external type 1, E2 - OSPF external type 2
       i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2
       ia - IS-IS inter area, * - candidate default, U - per-user static route
       o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP
       a - application route
       + - replicated route, % - next hop override, p - overrides from PfR

Gateway of last resort is 192.0.2.101 to network 0.0.0.0

      10.0.0.0/8 is variably subnetted, 5 subnets, 2 masks
O        10.100.101.0/24 
           [110/2] via 10.100.201.254, 04:43:48, GigabitEthernet0/1.201
      192.0.2.0/32 is subnetted, 3 subnets
O        192.0.2.101 
           [110/2] via 10.100.201.254, 04:43:48, GigabitEthernet0/1.201

! Ping!
cisco-02#ping 192.0.2.101
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 192.0.2.101, timeout is 2 seconds:
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 1/1/2 ms

cisco-02#ping 192.0.2.101 source lo0 
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 192.0.2.101, timeout is 2 seconds:
Packet sent with a source address of 192.0.2.201 
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 1/1/2 ms

cisco-02#ping 10.100.101.253 source lo0
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 10.100.101.253, timeout is 2 seconds:
Packet sent with a source address of 192.0.2.201 
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 1/2/5 ms
</code></pre></div><p>All looking good to me!</p>
<h4 id="ospfv3-playbook">OSPFv3 Playbook</h4>
<p>OSPFv3 is a newer version of OSPF that supports IPv6. It also technically supports IPv4 as well, but not all implementations of OSPFv3 enable this feature. In the case of IOS, this feature does exist in more recent versions, but did not in older versions.</p>
<p>I will only use it for IPv6, so that those managing routers without the address family feature in OSPFv3 are still able to make use of these playbooks.</p>
<p>The playbook for OSPFv3 is very similar to the OSPF playbook, except for one key difference: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for routing</span>
<span style="color:#75715e">##</span>

- <span style="color:#66d9ef">name</span>: Enable IPv6 routing
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#e6db74">&#34;ipv6 unicast-routing&#34;</span>

- <span style="color:#66d9ef">name</span>: OSPFv3 Process
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#e6db74">&#34;router ospfv3 1&#34;</span>

- <span style="color:#66d9ef">name</span>: OSPFv3 Process - Router ID
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>: 
      - <span style="color:#e6db74">&#34;router-id {{ router_id }}&#34;</span>
    <span style="color:#66d9ef">parents</span>: router ospfv3 <span style="color:#ae81ff">1</span>

- <span style="color:#66d9ef">name</span>: OSPFv3 Interfaces
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - ipv6 ospf <span style="color:#ae81ff">1</span> area {{ item.ospfv3.area }}
    <span style="color:#66d9ef">parents</span>: interface {{ item.ios_if }}
  <span style="color:#66d9ef">when</span>: item.ospfv3 is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>

- <span style="color:#66d9ef">name</span>: OSPFv3 Interfaces - Passive
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - passive-interface {{ item.ios_if }}
    <span style="color:#66d9ef">parents</span>: router ospfv3 <span style="color:#ae81ff">1</span>
  <span style="color:#66d9ef">when</span>: 
    - item.ospfv3 is defined
    - item.ospfv3.passive is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>

- <span style="color:#66d9ef">name</span>: Removed IPv4 address family
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#e6db74">&#34;no address-family ipv4 unicast&#34;</span>
    <span style="color:#66d9ef">parents</span>: router ospfv3 <span style="color:#ae81ff">1</span>
</code></pre></div><p>The first task in this playbook enables <code>ipv6 unicast-routing</code>. IOS by default (or at least the version I am working in this lab) does not have this enabled this out of the box, meaning that any attempts to add IPv6 routing protocols will fail.</p>
<p>The final task also removes the IPv4 unicast address family. If you are wanting to run both IPv4 and IPv6 with OSPFv3, you can remove this task.</p>
<p>Other than that, the playbook is almost identical to the OSPF playbook, except using the word <code>ipv6</code> where <code>ip</code> is used before, and <code>ospfv3</code> where <code>router ospfv3</code> where <code>router ospf</code> was used before.</p>
<p>The <code>host_vars</code> used to generate this are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;GigabitEthernet0/0&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Management&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.15.30.23/24&#34;</span>
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;GigabitEthernet0/1&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;VLAN Bridge&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">subint</span>:
      <span style="color:#66d9ef">vlans</span>:
      - <span style="color:#ae81ff">101</span>
      - <span style="color:#ae81ff">201</span>
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;GigabitEthernet0/1.101&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To netsvr&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.100.101.253/24&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:101::f/64&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;GigabitEthernet0/1.201&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To cisco-02&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.100.201.254/24&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:201::a/64&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;GigabitEthernet0/2&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To the Internet&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;dhcp&#34;</span>
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;loopback0&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;192.0.2.101/32&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:901:beef::1/128&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
</code></pre></div><p>As can be seen here, for every interface we want OSPFv3 to run on, we add the <code>ospfv3</code> sections. We specify what area they are in, and also add the <code>passive</code> variable if the interfaces are not used for forming OSPF neighbours.</p>
<p>The generated configuration for the above would be: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ipv6 unicast-routing

interface Loopback0
 ipv6 address 2001:DB8:901:BEEF::1/128
 ipv6 ospf 1 area 0.0.0.0

interface GigabitEthernet0/1.101
 ipv6 ospf 1 area 0.0.0.0

interface GigabitEthernet0/1.201
 ipv6 ospf 1 area 0.0.0.0

router ospfv3 1
 address-family ipv6 unicast
  passive-interface GigabitEthernet0/1.101
  passive-interface Loopback0
 exit-address-family
</code></pre></div><h5 id="verification-3">Verification</h5>
<p>After this, we should be able to see OSPFv3 routes, and we should be able to ping between the loopbacks on the <em>edge</em> router and the <em>internal</em> router</p>
<p><strong>cisco-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show OSPF interfaces
cisco-01#show ipv6 ospf interface brief 
Interface    PID   Area            Intf ID    Cost  State Nbrs F/C
Lo0          1     0.0.0.0         8          1     LOOP  0/0
Gi0/1.201    1     0.0.0.0         10         1     BDR   1/1
Gi0/1.101    1     0.0.0.0         9          1     DR    0/0

! Show OSPF neighbours
cisco-01#show ipv6 ospf neighbor 

            OSPFv3 Router with ID (192.0.2.101) (Process ID 1)

Neighbor ID     Pri   State           Dead Time   Interface ID    Interface
192.0.2.201       1   FULL/DR         00:00:35    8               GigabitEthernet0/1.201

! Show routing table
cisco-01#show ipv6 route ospf 
IPv6 Routing Table - default - 8 entries
Codes: C - Connected, L - Local, S - Static, U - Per-user Static route
       B - BGP, HA - Home Agent, MR - Mobile Router, R - RIP
       H - NHRP, I1 - ISIS L1, I2 - ISIS L2, IA - ISIS interarea
       IS - ISIS summary, D - EIGRP, EX - EIGRP external, NM - NEMO
       ND - ND Default, NDp - ND Prefix, DCE - Destination, NDr - Redirect
       RL - RPL, O - OSPF Intra, OI - OSPF Inter, OE1 - OSPF ext 1
       OE2 - OSPF ext 2, ON1 - OSPF NSSA ext 1, ON2 - OSPF NSSA ext 2
       la - LISP alt, lr - LISP site-registrations, ld - LISP dyn-eid
       lA - LISP away, a - Application
O   2001:DB8:901:BEEF::2/128 [110/1]
     via FE80::5054:FF:FE3A:2C40, GigabitEthernet0/1.201

! Ping!
cisco-01#ping 2001:db8:901:beef::2
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 2001:DB8:901:BEEF::2, timeout is 2 seconds:
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 1/1/1 ms

cisco-01#ping 2001:db8:901:beef::2 source lo0
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 2001:DB8:901:BEEF::2, timeout is 2 seconds:
Packet sent with a source address of 2001:DB8:901:BEEF::1
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 1/1/2 ms
</code></pre></div><p><strong>cisco-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show OSPF interfaces
cisco-02#show ipv6 ospf interface brief 
Interface    PID   Area            Intf ID    Cost  State Nbrs F/C
Lo0          1     0.0.0.0         7          1     LOOP  0/0
Gi0/1.201    1     0.0.0.0         8          1     DR    1/1

! Show OSPF neighbours
cisco-02#show ipv6 ospf neighbor 

            OSPFv3 Router with ID (192.0.2.201) (Process ID 1)

Neighbor ID     Pri   State           Dead Time   Interface ID    Interface
192.0.2.101       1   FULL/BDR        00:00:32    10              GigabitEthernet0/1.201

! Show routing table
cisco-02#show ipv6 route ospf
IPv6 Routing Table - default - 7 entries
Codes: C - Connected, L - Local, S - Static, U - Per-user Static route
       B - BGP, HA - Home Agent, MR - Mobile Router, R - RIP
       H - NHRP, I1 - ISIS L1, I2 - ISIS L2, IA - ISIS interarea
       IS - ISIS summary, D - EIGRP, EX - EIGRP external, NM - NEMO
       ND - ND Default, NDp - ND Prefix, DCE - Destination, NDr - Redirect
       RL - RPL, O - OSPF Intra, OI - OSPF Inter, OE1 - OSPF ext 1
       OE2 - OSPF ext 2, ON1 - OSPF NSSA ext 1, ON2 - OSPF NSSA ext 2
       la - LISP alt, lr - LISP site-registrations, ld - LISP dyn-eid
       lA - LISP away, a - Application
O   2001:DB8:101::/64 [110/2]
     via FE80::5054:FF:FEDD:4D5C, GigabitEthernet0/1.201
O   2001:DB8:901:BEEF::1/128 [110/1]
     via FE80::5054:FF:FEDD:4D5C, GigabitEthernet0/1.201

! Ping!
cisco-02#ping 2001:db8:901:beef::1
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 2001:DB8:901:BEEF::1, timeout is 2 seconds:
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 1/1/4 ms

cisco-02#ping 2001:db8:901:beef::1 source lo0
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 2001:DB8:901:BEEF::1, timeout is 2 seconds:
Packet sent with a source address of 2001:DB8:901:BEEF::2
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 1/2/5 ms

cisco-02#ping 2001:db8:101::f
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 2001:DB8:101::F, timeout is 2 seconds:
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 1/1/4 ms
</code></pre></div><p>This all looks good!</p>
<h4 id="bgp-playbook">BGP Playbook</h4>
<p>The BGP playbook is where the most complex logic is used. However we also get to make use of native Ansible modules, so some of this logic can carry over when configuring other vendors in future.</p>
<p>The full playbook looks like so: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for routing</span>
<span style="color:#75715e">##</span>
- <span style="color:#66d9ef">name</span>: Configure BGP - eBGP v4 peers
  <span style="color:#66d9ef">ios_bgp</span>:
    <span style="color:#66d9ef">config</span>:
      <span style="color:#66d9ef">bgp_as</span>: <span style="color:#e6db74">&#34;{{ bgp[&#39;local_as&#39;] }}&#34;</span>
      <span style="color:#66d9ef">log_neighbor_changes</span>: True
      <span style="color:#66d9ef">router_id</span>: <span style="color:#e6db74">&#34;{{ router_id }}&#34;</span>
      <span style="color:#66d9ef">neighbors</span>:
      - <span style="color:#66d9ef">neighbor</span>: <span style="color:#e6db74">&#34;{{ item.peer }}&#34;</span>
        <span style="color:#66d9ef">remote_as</span>: <span style="color:#e6db74">&#34;{{ item.remote_as }}&#34;</span>
  <span style="color:#66d9ef">when</span>: 
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv4 is defined
    - item.ebgp is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv4 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v4

- <span style="color:#66d9ef">name</span>: Configure BGP - eBGP v6 peers
  <span style="color:#66d9ef">ios_bgp</span>:
    <span style="color:#66d9ef">config</span>:
      <span style="color:#66d9ef">bgp_as</span>: <span style="color:#e6db74">&#34;{{ bgp[&#39;local_as&#39;] }}&#34;</span>
      <span style="color:#66d9ef">log_neighbor_changes</span>: True
      <span style="color:#66d9ef">router_id</span>: <span style="color:#e6db74">&#34;{{ router_id }}&#34;</span>
      <span style="color:#66d9ef">neighbors</span>:
      - <span style="color:#66d9ef">neighbor</span>: <span style="color:#e6db74">&#34;{{ item.peer }}&#34;</span>
        <span style="color:#66d9ef">remote_as</span>: <span style="color:#e6db74">&#34;{{ item.remote_as }}&#34;</span>
      <span style="color:#66d9ef">address_family</span>:
        - <span style="color:#66d9ef">afi</span>: ipv6
          <span style="color:#66d9ef">neighbors</span>:
          - <span style="color:#66d9ef">neighbor</span>: <span style="color:#e6db74">&#34;{{ item.peer }}&#34;</span>
            <span style="color:#66d9ef">activate</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">when</span>: 
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv6 is defined
    - item.ebgp is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv6 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v6

- <span style="color:#66d9ef">name</span>: Configure BGP - iBGP v4 peers
  <span style="color:#66d9ef">ios_bgp</span>:
    <span style="color:#66d9ef">config</span>:
      <span style="color:#66d9ef">bgp_as</span>: <span style="color:#e6db74">&#34;{{ bgp[&#39;local_as&#39;] }}&#34;</span>
      <span style="color:#66d9ef">log_neighbor_changes</span>: True
      <span style="color:#66d9ef">router_id</span>: <span style="color:#e6db74">&#34;{{ router_id }}&#34;</span>
      <span style="color:#66d9ef">neighbors</span>:
      - <span style="color:#66d9ef">neighbor</span>: <span style="color:#e6db74">&#34;{{ item.peer }}&#34;</span>
        <span style="color:#66d9ef">remote_as</span>: <span style="color:#e6db74">&#34;{{ item.remote_as }}&#34;</span>
        <span style="color:#66d9ef">update_source</span>: <span style="color:#e6db74">&#34;{{ item.update_source }}&#34;</span>
  <span style="color:#66d9ef">when</span>: 
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv4 is defined
    - item.ibgp is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv4 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v4

- <span style="color:#66d9ef">name</span>: Configure BGP - iBGP v6 peers
  <span style="color:#66d9ef">ios_bgp</span>:
    <span style="color:#66d9ef">config</span>:
      <span style="color:#66d9ef">bgp_as</span>: <span style="color:#e6db74">&#34;{{ bgp[&#39;local_as&#39;] }}&#34;</span>
      <span style="color:#66d9ef">log_neighbor_changes</span>: True
      <span style="color:#66d9ef">router_id</span>: <span style="color:#e6db74">&#34;{{ router_id }}&#34;</span>
      <span style="color:#66d9ef">neighbors</span>:
      - <span style="color:#66d9ef">neighbor</span>: <span style="color:#e6db74">&#34;{{ item.peer }}&#34;</span>
        <span style="color:#66d9ef">remote_as</span>: <span style="color:#e6db74">&#34;{{ item.remote_as }}&#34;</span>
        <span style="color:#66d9ef">update_source</span>: <span style="color:#e6db74">&#34;{{ item.update_source }}&#34;</span>
      <span style="color:#66d9ef">address_family</span>:
        - <span style="color:#66d9ef">afi</span>: ipv6
          <span style="color:#66d9ef">neighbors</span>:
          - <span style="color:#66d9ef">neighbor</span>: <span style="color:#e6db74">&#34;{{ item.peer }}&#34;</span>
            <span style="color:#66d9ef">activate</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">when</span>: 
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv6 is defined
    - item.ibgp is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv6 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v6

- <span style="color:#66d9ef">name</span>: Configure BGP - Redistribute OSPF
  <span style="color:#66d9ef">ios_bgp</span>:
    <span style="color:#66d9ef">config</span>:
      <span style="color:#66d9ef">bgp_as</span>: <span style="color:#e6db74">&#34;{{ bgp[&#39;local_as&#39;] }}&#34;</span>
      <span style="color:#66d9ef">address_family</span>:
        - <span style="color:#66d9ef">afi</span>: ipv4
          <span style="color:#66d9ef">redistribute</span>:
            - <span style="color:#66d9ef">protocol</span>: ospf
              <span style="color:#66d9ef">id</span>: <span style="color:#ae81ff">1</span>
  <span style="color:#66d9ef">when</span>: 
    - bgp is defined
    - bgp.redist is defined
    - bgp.redist.ospf is defined
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v4

- <span style="color:#66d9ef">name</span>: Configure BGP - Redistribute OSPFv3
  <span style="color:#66d9ef">ios_bgp</span>:
    <span style="color:#66d9ef">config</span>:
      <span style="color:#66d9ef">bgp_as</span>: <span style="color:#e6db74">&#34;{{ bgp[&#39;local_as&#39;] }}&#34;</span>
      <span style="color:#66d9ef">address_family</span>:
        - <span style="color:#66d9ef">afi</span>: ipv6
          <span style="color:#66d9ef">redistribute</span>:
            - <span style="color:#66d9ef">protocol</span>: ospf
              <span style="color:#66d9ef">id</span>: <span style="color:#ae81ff">1</span>
  <span style="color:#66d9ef">when</span>: 
    - bgp is defined
    - bgp.redist is defined
    - bgp.redist.ospfv3 is defined
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v6

- <span style="color:#66d9ef">name</span>: Configure BGP - iBGP v4 Default Originate
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - neighbor {{ item.peer }} default-originate
    <span style="color:#66d9ef">parents</span>:
      - router bgp {{ bgp[<span style="color:#e6db74">&#39;local_as&#39;</span>] }}
      - address-family ipv4 unicast
  <span style="color:#66d9ef">when</span>: 
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv4 is defined
    - item.ibgp is defined
    - item.default_originate is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv4 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v4
</code></pre></div><p>There is a lot going on here, however most of the tasks are very similar, with slight updates based upon whether they are <strong>external BGP</strong> or <strong>internal BGP</strong>, and also whether they are <strong>IPv4</strong> or <strong>IPv6</strong>.</p>
<p>Rather than dissecting every single play, I will go through the first of each kind (i.e. adding peers, redistribution) and then mention the differences for the subsequent tasks.</p>
<h5 id="configuring-ebgp-ipv4-peers">Configuring eBGP IPv4 peers</h5>
<p><strong>Ansible module</strong>: <code>ios_bgp</code></p>
<p>Unlike the OSPF playbooks, Ansible has a BGP module for Cisco IOS, so all of the tasks (bar one) use this module.</p>
<p>As per the previous playbooks, the host specific variables are sourced from <code>host_vars</code>. In this case, they are used to define BGP peering: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">router_id</span>: <span style="color:#ae81ff">192.0.2.101</span>
<span style="color:#66d9ef">bgp</span>:
  <span style="color:#66d9ef">local_as</span>: <span style="color:#ae81ff">65101</span>
  <span style="color:#66d9ef">redist</span>:
    <span style="color:#66d9ef">ospf</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">neighbors</span>:
    <span style="color:#66d9ef">ipv4</span>:
     - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">10.100.101.254</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65430</span>
       <span style="color:#66d9ef">ebgp</span>: <span style="color:#66d9ef">true</span>
     - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">192.0.2.201</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65101</span>
       <span style="color:#66d9ef">ibgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">update_source</span>: Loopback0
       <span style="color:#66d9ef">default_originate</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv6</span>:
     - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:101::ffff&#34;</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65430</span>
       <span style="color:#66d9ef">ebgp</span>: <span style="color:#66d9ef">true</span>
     - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:901:beef::2&#34;</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65101</span>
       <span style="color:#66d9ef">ibgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">update_source</span>: Loopback0
</code></pre></div><p>As there&rsquo;s quite a lot going on in this, I shall go through each part separately.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">name</span>: Configure BGP - eBGP v4 peers
  <span style="color:#66d9ef">ios_bgp</span>:
    <span style="color:#66d9ef">config</span>:
      <span style="color:#66d9ef">bgp_as</span>: <span style="color:#e6db74">&#34;{{ bgp[&#39;local_as&#39;] }}&#34;</span>
      <span style="color:#66d9ef">log_neighbor_changes</span>: True
      <span style="color:#66d9ef">router_id</span>: <span style="color:#e6db74">&#34;{{ router_id }}&#34;</span>
</code></pre></div><p>In this section, we are retrieving the Local Autonomous System number (i.e the AS number of this host) and applying it. We also add the <code>log_neighbor_changes</code> option, so that any BGP neighbour changes are sent to syslog, local logging, and to the console.</p>
<p>Finally, we set the <code>router_id</code> to the Router ID variable in <code>host_vars</code>. This is not strictly required, but the whole idea behind having a set router ID is for ease of troubleshooting, rather than one that can change if interface IPs change (or go down).</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">      <span style="color:#66d9ef">neighbors</span>:
      - <span style="color:#66d9ef">neighbor</span>: <span style="color:#e6db74">&#34;{{ item.peer }}&#34;</span>
        <span style="color:#66d9ef">remote_as</span>: <span style="color:#e6db74">&#34;{{ item.remote_as }}&#34;</span>
</code></pre></div><p>The neighbours themselves are built using a list defined in our <code>host_vars</code>. As the <code>neighbors</code> option allows multiple peers to be defined, this is the perfect use case of Ansible loops.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  <span style="color:#66d9ef">when</span>: 
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv4 is defined
    - item.ebgp is defined
</code></pre></div><p>The conditional logic here states that this task will run if: -</p>
<ul>
<li>The <code>bgp</code> variable is defined (which comes from <code>host_vars</code> in our case AND</li>
<li>The <code>bgp</code> variable has a section called <code>neighbors</code> AND</li>
<li>The <code>bgp.neighbors</code> section has a section called <code>ipv4</code> AND</li>
<li>The <code>item</code> variable has the <code>ebgp</code> field</li>
</ul>
<p>The <code>item</code> variable comes from the following loop: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv4 }}&#34;</span>
</code></pre></div><p>This loop goes through our list of neighbours, and if they have IPv4, it adds BGP peers based upon this.</p>
<p>The configuration generated is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">router bgp 65101
 bgp router-id 192.0.2.101
 bgp log-neighbor-changes
 neighbor 10.100.101.254 remote-as 65430
 !
 address-family ipv4
  neighbor 10.100.101.254 activate
 exit-address-family
 !
</code></pre></div><p>As a bonus, I have also added the <code>tags</code> section, which allows you to target tasks with specific tags when running your playbook.</p>
<p>To run only BGP tasks, you would run <code>ansible-playbook ios.yaml --tags bgp</code>. To run only BGP tasks that are for IPv4, you would run <code>ansible-playbook ios.yaml --tags bgp_v4</code>. This makes it easier to target only the changes you want, as well as testing changes to tasks in Playbooks.</p>
<h5 id="configuring-ebgp-ipv6-peers">Configuring eBGP IPv6 peers</h5>
<p><strong>Ansible module</strong>: <code>ios_bgp</code></p>
<p>This task is very similar to the task for building external BGP peers for IPv4, but with some extra options.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">      <span style="color:#66d9ef">address_family</span>:
        - <span style="color:#66d9ef">afi</span>: ipv6
          <span style="color:#66d9ef">neighbors</span>:
          - <span style="color:#66d9ef">neighbor</span>: <span style="color:#e6db74">&#34;{{ item.peer }}&#34;</span>
            <span style="color:#66d9ef">activate</span>: <span style="color:#66d9ef">true</span>
</code></pre></div><p>This section is required to enable BGP for IPv6 peering. The reason behind this is that by default, Cisco will only enable IPv4 peering by default. If you do not add this, your peers will be configured, but they won&rsquo;t attempt to form peering sessions over IPv6.</p>
<p>Otherwise, the only notable changes are that we are looking for whether <code>bgp.neighbors.ipv6</code> is in our <code>when</code> section, our loop is over the list of peers in <code>bgp.neighbors.ipv6</code>, and we have a tag of <code>bgp_v6</code>.</p>
<p>The configuration this will generate is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">router bgp 65101
 bgp router-id 192.0.2.101
 bgp log-neighbor-changes
 neighbor 2001:DB8:101::FFFF remote-as 65430
 !
 address-family ipv4
  no neighbor 2001:DB8:101::FFFF activate
 exit-address-family
 !
 address-family ipv6
  neighbor 2001:DB8:101::FFFF activate
 exit-address-family
</code></pre></div><p>It is worth noting that we are reapplying the BGP router ID and the logging of neighbour changes. This is required, because if we only have a IPv6 sessions, this would be the only task that runs, thus meaning no other task would create the Router ID and add the neighbour change logging.</p>
<h5 id="configuring-ibgp-ipv4-peers">Configuring iBGP IPv4 peers</h5>
<p><strong>Ansible module</strong>: <code>ios_bgp</code></p>
<p>The major difference between the eBGP and iBGP configuration is that we also add the <code>update_source</code> parameter. Typically, external BGP sessions are formed over a WAN link to a provider, whereas internal BGP sessions form over a core network.</p>
<p>You are more likely to have multiple links to each BGP-speaking device in your core network than to external providers, therefore you do not want your BGP session to only work over a specific interface. Instead, if you use an IP that is routable over multiple interfaces (e.g. a loopback IP) your BGP sessions can still form when interfaces go down.</p>
<p>If you do not specify an <code>update_source</code>, your BGP sessions will usually be sourced from the IP of the outgoing interface. Changing the <code>update_source</code> to be a loopback address addresses this.</p>
<p>If you recall from the OSPF section, we are using OSPF to advertise our loopback IPs between the <em>edge</em> and <em>internal</em> router. This gives us reachability between the loopback addresses of the routers.</p>
<p>Other than the above, the only other changes is that we are expecting to see the <code>ibgp: true</code> statement in our <code>host_vars</code> for a peer.</p>
<p>The generated configuration is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">router bgp 65101
 bgp router-id 192.0.2.101
 bgp log-neighbor-changes
 neighbor 192.0.2.201 remote-as 65101
 neighbor 192.0.2.201 update-source Loopback0
 !
 address-family ipv4
  neighbor 192.0.2.201 activate
 exit-address-family
</code></pre></div><h5 id="configuring-ibgp-ipv6-peers">Configuring IBGP IPv6 peers</h5>
<p><strong>Ansible module</strong>: <code>ios_bgp</code></p>
<p>This changes in this task are effectively an amalgamation of what changes are required for eBGP IPv6 peers, and the <code>update_source</code> that we use for iBGP v4 peers</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">        <span style="color:#66d9ef">update_source</span>: <span style="color:#e6db74">&#34;{{ item.update_source }}&#34;</span>
      <span style="color:#66d9ef">address_family</span>:
        - <span style="color:#66d9ef">afi</span>: ipv6
          <span style="color:#66d9ef">neighbors</span>:
          - <span style="color:#66d9ef">neighbor</span>: <span style="color:#e6db74">&#34;{{ item.peer }}&#34;</span>
            <span style="color:#66d9ef">activate</span>: <span style="color:#66d9ef">true</span>
</code></pre></div><p>IPv6 iBGP peers also need activating in the IPv6 address family, to ensure that we attempt to form IPv6 peering. We also add the <code>update_source</code> to ensure that IPv6 peering stays up even if an interface were to go down.</p>
<p>Reachability of loopbacks uses OSPFv3 rather than OSPFv2, but this is necessary for IPv6.</p>
<p>The generated configuration is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">router bgp 65101
 bgp router-id 192.0.2.101
 bgp log-neighbor-changes
 neighbor 2001:DB8:901:BEEF::2 remote-as 65101
 neighbor 2001:DB8:901:BEEF::2 update-source Loopback0
 !
 address-family ipv4
  no neighbor 2001:DB8:901:BEEF::2 activate
 exit-address-family
 !
 address-family ipv6
  neighbor 2001:DB8:901:BEEF::2 activate
 exit-address-family
</code></pre></div><h5 id="configuring-ospf-redistribution-for-ipv4">Configuring OSPF Redistribution for IPv4</h5>
<p><strong>Ansible module</strong>: <code>ios_bgp</code></p>
<p>Redistribution is where you take routes discovered/generated by one routing protocol and export them to another. The reason you would do this is because typically your core network just needs reachability between other parts of the network, and either a default route or a subset of external routes for access to the internet and networks out of your control.</p>
<p>Also, core networks are usually built on Layer 3 switching. Nowadays switches can run routing protocols (including BGP), MPLS, Segment Routing, VxLAN and much more.</p>
<p>However, these switches usually have a limited amount of TCAM (<strong>T</strong>ernary <strong>C</strong>ontent-<strong>a</strong>ddressable <strong>m</strong>emory), which is where routes and MAC address tables (among other things) are stored.</p>
<p>The Broadcom Trident range of chipsets are very popular in the network switching space, however up until recently they could only store around 16,000 IPv4 routes in TCAM. This poses a problem when using BGP, as the full BGP routing table is currently sitting around the 800,000 mark, which would overload the TCAM on these switches.</p>
<p>Using something like OSPF for reachability inside your network, and then using BGP on your edge routers (which are usually capable of storing many millions of routes) will avoid the need to run expensive routers for every hop in your network.</p>
<p>The following task achieves redistribution from OSPF to BGP: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">name</span>: Configure BGP - Redistribute OSPF
  <span style="color:#66d9ef">ios_bgp</span>:
    <span style="color:#66d9ef">config</span>:
      <span style="color:#66d9ef">bgp_as</span>: <span style="color:#e6db74">&#34;{{ bgp[&#39;local_as&#39;] }}&#34;</span>
      <span style="color:#66d9ef">address_family</span>:
        - <span style="color:#66d9ef">afi</span>: ipv4
          <span style="color:#66d9ef">redistribute</span>:
            - <span style="color:#66d9ef">protocol</span>: ospf
              <span style="color:#66d9ef">id</span>: <span style="color:#ae81ff">1</span>
  <span style="color:#66d9ef">when</span>:
    - bgp is defined
    - bgp.redist is defined
    - bgp.redist.ospf is defined
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v4
</code></pre></div><p>The task has similar options to our other <code>ios_bgp</code> based tasks, but rather than defining multiple peers, we are defining (potentially) multiple protocols to redistribute. If we wanted to import our static routes, or we want to run IS-IS or EIGRP, you would just add them to the list like so: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">          <span style="color:#66d9ef">redistribute</span>:
            - <span style="color:#66d9ef">protocol</span>: eigrp
              <span style="color:#66d9ef">id</span>: <span style="color:#ae81ff">10</span>
            - <span style="color:#66d9ef">protocol</span>: isis
            - <span style="color:#66d9ef">protocol</span>: static
</code></pre></div><p>The rest of the task is just to ensure it is defined under the correct BGP process (defined by our BGP AS number), and also to only apply it when: -</p>
<ul>
<li>If the <code>bgp</code> variable exists AND</li>
<li>If the <code>bgp.redist</code> variable exists AND</li>
<li>If the <code>bgp.redist.ospf</code> variable exists</li>
</ul>
<p>The generated configuration is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">router bgp 65101
 !
 address-family ipv4
  redistribute ospf 1
 exit-address-family
</code></pre></div><h5 id="configuring-ospf-redistribution-for-ipv6">Configuring OSPF Redistribution for IPv6</h5>
<p><strong>Ansible module</strong>: <code>ios_bgp</code></p>
<p>Everything mentioned in regards to OSPF redistribution for IPv4 applies here, with the global IPv6 routing table often being larger than what networking switching can handle.</p>
<p>Otherwise, the only differences in the task are that we are targeting <code>afi: ipv6</code>  (i.e. the IPv6 address family) rather than IPv4, and that we are looking for the existence of <code>bgp.redist.ospfv3</code>.</p>
<p>The generated configuration is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">router bgp <span style="color:#ae81ff">65101</span>
 !
 address-family ipv6
  redistribute ospf <span style="color:#ae81ff">1</span>
 exit-address-family
</code></pre></div><h5 id="advertising-a-default-route-via-bgp">Advertising a default route via BGP</h5>
<p><strong>Ansible module</strong>: <code>ios_config</code></p>
<p>There are multiple ways to advertise a default route from BGP: -</p>
<ul>
<li>Receive one from an upstream BGP peer, and advertise it downstream</li>
<li>Import one from another protocol (e.g. a static default route)</li>
<li>Generate one on a per-peer basis</li>
</ul>
<p>The latter is what we are going to do in this task. Unfortunately the option to do this is not in the <code>ios_bgp</code> module, so we use the <code>ios_config</code> module instead to achieve this: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">name</span>: Configure BGP - iBGP v4 Default Originate
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - neighbor {{ item.peer }} default-originate
    <span style="color:#66d9ef">parents</span>:
      - router bgp {{ bgp[<span style="color:#e6db74">&#39;local_as&#39;</span>] }}
      - address-family ipv4 unicast
  <span style="color:#66d9ef">when</span>:
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv4 is defined
    - item.ibgp is defined
    - item.default_originate is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv4 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v4
</code></pre></div><p>In this, we are going two levels into the configuration hierarchy. First, we enter <code>router bgp $OUR-AS-NUMBER</code>, then we enter <code>address-family ipv4 unicast</code>. From here, we can then apply the <code>default-originate</code> statement to our peer.</p>
<p>We only apply this to peers that have the <code>default_originate</code> option specified, and only if they are <code>ibgp</code> peers. As we are not functioning as a transit provider in this lab, we do not need to advertise default routes to external peers.</p>
<p>The generated configuration is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">router bgp 65101
 !
 address-family ipv4
  neighbor 192.0.2.201 default-originate
 exit-address-family
</code></pre></div><p>To verify that this works: -</p>
<p><strong>cisco-01 Edge Router</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">cisco-01#show ip bgp neighbors 192.0.2.201 advertised-routes    
BGP table version is 8, local router ID is 192.0.2.101
Status codes: s suppressed, d damped, h history, * valid, &gt; best, i - internal, 
              r RIB-failure, S Stale, m multipath, b backup-path, f RT-Filter, 
              x best-external, a additional-path, c RIB-compressed, 
Origin codes: i - IGP, e - EGP, ? - incomplete
RPKI validation codes: V valid, I invalid, N Not found

Originating default network 0.0.0.0  &lt;--------------------------

     Network          Next Hop            Metric LocPrf Weight Path
 *&gt;  10.100.101.0/24  0.0.0.0                  0         32768 ?
 *&gt;  10.100.201.0/24  0.0.0.0                  0         32768 ?
 *&gt;  192.0.2.1/32     10.100.101.254           0             0 65430 i
 *&gt;  192.0.2.101/32   0.0.0.0                  0         32768 ?
 *&gt;  192.0.2.201/32   10.100.201.253           2         32768 ?
</code></pre></div><p><strong>cisco-02 Internal Router</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">cisco-02#show ip route bgp 
Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP
       D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area 
       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2
       E1 - OSPF external type 1, E2 - OSPF external type 2
       i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2
       ia - IS-IS inter area, * - candidate default, U - per-user static route
       o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP
       a - application route
       + - replicated route, % - next hop override, p - overrides from PfR

Gateway of last resort is 192.0.2.101 to network 0.0.0.0 &lt;---------------------

B*    0.0.0.0/0 [200/0] via 192.0.2.101, 01:00:45
      192.0.2.0/32 is subnetted, 3 subnets
B        192.0.2.1 [200/0] via 10.100.101.254, 01:00:45
</code></pre></div><p>We have a default route!</p>
<h5 id="verification-4">Verification</h5>
<p>After all of the above has run, we should have BGP sessions up over IPv4 and IPv6, as well as routes received and sent to <strong>netsvr-01</strong> BGP route server.</p>
<p><strong>cisco-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show BGP neighbours on IPv4 and IPv6
cisco-01#show bgp ipv4 unicast summary 
BGP router identifier 192.0.2.101, local AS number 65101
BGP table version is 8, main routing table version 8
6 network entries using 864 bytes of memory
6 path entries using 480 bytes of memory
4/3 BGP path/bestpath attribute entries using 608 bytes of memory
1 BGP AS-PATH entries using 24 bytes of memory
0 BGP route-map cache entries using 0 bytes of memory
0 BGP filter-list cache entries using 0 bytes of memory
BGP using 1976 total bytes of memory
BGP activity 8/0 prefixes, 8/0 paths, scan interval 60 secs

Neighbor        V           AS MsgRcvd MsgSent   TblVer  InQ OutQ Up/Down  State/PfxRcd
10.100.101.254  4        65430     317     348        8    0    0 05:12:20        1
192.0.2.201     4        65101     345     350        8    0    0 05:11:39        0

cisco-01#show bgp ipv6 unicast summary 
BGP router identifier 192.0.2.101, local AS number 65101
BGP table version is 17, main routing table version 17
2 network entries using 336 bytes of memory
2 path entries using 208 bytes of memory
2/2 BGP path/bestpath attribute entries using 304 bytes of memory
1 BGP AS-PATH entries using 24 bytes of memory
0 BGP route-map cache entries using 0 bytes of memory
0 BGP filter-list cache entries using 0 bytes of memory
BGP using 872 total bytes of memory
BGP activity 8/0 prefixes, 8/0 paths, scan interval 60 secs

Neighbor        V           AS MsgRcvd MsgSent   TblVer  InQ OutQ Up/Down  State/PfxRcd
2001:DB8:101::FFFF
                4        65430     316     349       17    0    0 05:12:18        1
2001:DB8:901:BEEF::2
                4        65101     344     359       17    0    0 05:11:40        0

! Show BGP routes
cisco-01#show bgp ipv4 unicast 
BGP table version is 8, local router ID is 192.0.2.101
Status codes: s suppressed, d damped, h history, * valid, &gt; best, i - internal, 
              r RIB-failure, S Stale, m multipath, b backup-path, f RT-Filter, 
              x best-external, a additional-path, c RIB-compressed, 
Origin codes: i - IGP, e - EGP, ? - incomplete
RPKI validation codes: V valid, I invalid, N Not found

     Network          Next Hop            Metric LocPrf Weight Path
     0.0.0.0          0.0.0.0                                0 i
 *&gt;  10.100.101.0/24  0.0.0.0                  0         32768 ?
 *&gt;  10.100.201.0/24  0.0.0.0                  0         32768 ?
 *&gt;  192.0.2.1/32     10.100.101.254           0             0 65430 i
 *&gt;  192.0.2.101/32   0.0.0.0                  0         32768 ?
 *&gt;  192.0.2.201/32   10.100.201.253           2         32768 ?

cisco-01#show bgp ipv6 unicast 
BGP table version is 17, local router ID is 192.0.2.101
Status codes: s suppressed, d damped, h history, * valid, &gt; best, i - internal, 
              r RIB-failure, S Stale, m multipath, b backup-path, f RT-Filter, 
              x best-external, a additional-path, c RIB-compressed, 
Origin codes: i - IGP, e - EGP, ? - incomplete
RPKI validation codes: V valid, I invalid, N Not found

     Network          Next Hop            Metric LocPrf Weight Path
 *&gt;  2001:DB8:901:BEEF::2/128
                       FE80::5054:FF:FE3A:2C40
                                                1         32768 ?
 *&gt;  2001:DB8:999:BEEF::1/128
                       2001:DB8:101::FFFF
                                                0             0 65430 i

! Ping the netsvr Loopback (192.0.2.1 and 2001:DB8:999:BEEF::1)
cisco-01#ping 192.0.2.1
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 192.0.2.1, timeout is 2 seconds:
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 1/1/1 ms

cisco-01#ping 192.0.2.1 source lo0
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 192.0.2.1, timeout is 2 seconds:
Packet sent with a source address of 192.0.2.101 
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 1/1/1 ms

cisco-01#ping 192.0.2.1 source Gi0/1.201    
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 192.0.2.1, timeout is 2 seconds:
Packet sent with a source address of 10.100.201.254 
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 1/1/2 ms

cisco-01#ping 2001:db8:999:beef::1
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 2001:DB8:999:BEEF::1, timeout is 2 seconds:
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 1/1/1 ms

cisco-01#ping 2001:db8:999:beef::1 source lo0
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 2001:DB8:999:BEEF::1, timeout is 2 seconds:
Packet sent with a source address of 2001:DB8:901:BEEF::1
.
Success rate is 0 percent (0/1)
</code></pre></div><p><strong>cisco-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show BGP neighbours on IPv4 and IPv6
cisco-02#show bgp ipv4 unicast summary 
BGP router identifier 192.0.2.201, local AS number 65101
BGP table version is 6, main routing table version 6
5 network entries using 720 bytes of memory
5 path entries using 400 bytes of memory
3/3 BGP path/bestpath attribute entries using 456 bytes of memory
1 BGP AS-PATH entries using 24 bytes of memory
0 BGP route-map cache entries using 0 bytes of memory
0 BGP filter-list cache entries using 0 bytes of memory
BGP using 1600 total bytes of memory
BGP activity 7/0 prefixes, 7/0 paths, scan interval 60 secs

Neighbor        V           AS MsgRcvd MsgSent   TblVer  InQ OutQ Up/Down  State/PfxRcd
192.0.2.101     4        65101     361     355        6    0    0 05:20:43        5

cisco-02#show bgp ipv6 unicast summary 
BGP router identifier 192.0.2.201, local AS number 65101
BGP table version is 3, main routing table version 3
2 network entries using 336 bytes of memory
2 path entries using 208 bytes of memory
2/2 BGP path/bestpath attribute entries using 304 bytes of memory
1 BGP AS-PATH entries using 24 bytes of memory
0 BGP route-map cache entries using 0 bytes of memory
0 BGP filter-list cache entries using 0 bytes of memory
BGP using 872 total bytes of memory
BGP activity 7/0 prefixes, 7/0 paths, scan interval 60 secs

Neighbor        V           AS MsgRcvd MsgSent   TblVer  InQ OutQ Up/Down  State/PfxRcd
2001:DB8:901:BEEF::1
                4        65101     370     354        3    0    0 05:20:52        2

! Show BGP routes
cisco-02#show bgp ipv4 unicast 
BGP table version is 6, local router ID is 192.0.2.201
Status codes: s suppressed, d damped, h history, * valid, &gt; best, i - internal, 
              r RIB-failure, S Stale, m multipath, b backup-path, f RT-Filter, 
              x best-external, a additional-path, c RIB-compressed, 
Origin codes: i - IGP, e - EGP, ? - incomplete
RPKI validation codes: V valid, I invalid, N Not found

     Network          Next Hop            Metric LocPrf Weight Path
 *&gt;i 0.0.0.0          192.0.2.101              0    100      0 i
 r&gt;i 10.100.101.0/24  192.0.2.101              0    100      0 ?
 r&gt;i 10.100.201.0/24  192.0.2.101              0    100      0 ?
 *&gt;i 192.0.2.1/32     10.100.101.254           0    100      0 65430 i
 r&gt;i 192.0.2.101/32   192.0.2.101              0    100      0 ?

cisco-02#show bgp ipv6 unicast 
BGP table version is 3, local router ID is 192.0.2.201
Status codes: s suppressed, d damped, h history, * valid, &gt; best, i - internal, 
              r RIB-failure, S Stale, m multipath, b backup-path, f RT-Filter, 
              x best-external, a additional-path, c RIB-compressed, 
Origin codes: i - IGP, e - EGP, ? - incomplete
RPKI validation codes: V valid, I invalid, N Not found

     Network          Next Hop            Metric LocPrf Weight Path
 r&gt;i 2001:DB8:901:BEEF::2/128
                       2001:DB8:901:BEEF::1
                                                1    100      0 ?
 *&gt;i 2001:DB8:999:BEEF::1/128
                       2001:DB8:101::FFFF
                                                0    100      0 65430 i

! Ping the netsvr Loopback (192.0.2.1 and 2001:db8:999:beef::1)
cisco-02#ping 192.0.2.1
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 192.0.2.1, timeout is 2 seconds:
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 1/2/4 ms

cisco-02#ping 192.0.2.1 source lo0
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 192.0.2.1, timeout is 2 seconds:
Packet sent with a source address of 192.0.2.201 
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 1/2/3 ms

cisco-02#ping 2001:db8:999:beef::1 source lo0
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 2001:DB8:999:BEEF::1, timeout is 2 seconds:
Packet sent with a source address of 2001:DB8:901:BEEF::2
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 1/2/6 ms
</code></pre></div><p>Unfortunately, we can see that there are some reachability issues from <strong>cisco-01</strong> to the loopback of the <strong>netsvr-01</strong>. The reason for this is because when you redistribute OSPFv3 into BGP, by default it will not include any connected networks (i.e. the routes that are local to the router itself).</p>
<p>This option can be enabled with <code>redistribute ospf 1 include-connected</code>, however it is not supported by the Ansible <code>ios_bgp</code> module as an argument. If you need to do this, you would need to change this section to use <code>ios_config</code>, and build the task so that it will apply exact configuration lines. A task like the following would achieve this: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">name</span>: Configure BGP - Redistribute OSPFv3 and include connected networks
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - redistribute ospf <span style="color:#ae81ff">1</span> include-connected
    <span style="color:#66d9ef">parents</span>:
      - router bgp {{ bgp[<span style="color:#e6db74">&#39;local_as&#39;</span>] }}
      - address-family ipv6 unicast
  <span style="color:#66d9ef">when</span>:
    - bgp is defined
    - bgp.redist is defined
    - bgp.redist.ospfv3 is defined
  <span style="color:#66d9ef">tags</span>:
  - bgp
  - bgp_v6
</code></pre></div><h3 id="snmp">SNMP</h3>
<p>Compare to the Routing section, this section will be fairly straightforward. The role is created the same way, <code>ansible-galaxy init snmp</code> in the roles directory.</p>
<p>There is no native Ansible module for enabling SNMP, so again we are using the <code>ios_config</code> module.</p>
<h4 id="playbook-2">Playbook</h4>
<p>The contents of the playbook are below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for snmp</span>
- <span style="color:#66d9ef">name</span>: Enable SNMPv3
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">src</span>: snmpv3.j2
  <span style="color:#66d9ef">tags</span>:
    - snmp
</code></pre></div><p>As you can see, there&rsquo;s only one task in this. We are picking up the <code>snmpv3.j2</code> template, and applying it to the routers.</p>
<h4 id="template-file">Template file</h4>
<pre><code class="language-jinja2" data-lang="jinja2">snmp-server location {{ snmp['location'] }}
snmp-server contact {{ snmp['contact'] }}
snmp-server group {{ snmp['group'] }} v3 priv
snmp-server user {{ snmp['user'] }} {{ snmp['group'] }} v3 auth sha {{ snmp['auth_key'] }} priv aes 128 {{ snmp['priv_key'] }}
</code></pre><p>All of the variables in this are sourced from <code>group_vars</code>, rather than <code>host_vars</code>, as the SNMP details will be common across both routers</p>
<h4 id="group_vars"><code>group_vars</code></h4>
<p>The contents of the <code>group_vars</code> file for this are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">snmp</span>:
  <span style="color:#66d9ef">location</span>: Yeti Home
  <span style="color:#66d9ef">contact</span>: The Hairy One
  <span style="color:#66d9ef">user</span>: yetiops
  <span style="color:#66d9ef">group</span>: yetiops_group
  <span style="color:#66d9ef">auth_key</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">            $ANSIBLE_VAULT;1.1;AES256</span>
            <span style="color:#ae81ff">383</span><span style="color:#75715e">###REDACTED###############################################################566</span>
            <span style="color:#ae81ff">363</span><span style="color:#75715e">###REDACTED###############################################################366</span>
            <span style="color:#ae81ff">343</span><span style="color:#75715e">###REDACTED###############################################################565</span>
            <span style="color:#ae81ff">326</span><span style="color:#75715e">###REDACTED###############################################################362</span>
            <span style="color:#ae81ff">3431</span>
  <span style="color:#66d9ef">priv_key</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">            $ANSIBLE_VAULT;1.1;AES256</span>
            <span style="color:#ae81ff">386</span><span style="color:#75715e">###REDACTED###############################################################764</span>
            <span style="color:#ae81ff">613</span><span style="color:#75715e">###REDACTED###############################################################630</span>
            <span style="color:#ae81ff">646</span><span style="color:#75715e">###REDACTED###############################################################331</span>
            <span style="color:#ae81ff">376</span><span style="color:#75715e">###REDACTED###############################################################137</span>
            <span style="color:#ae81ff">3563</span>
</code></pre></div><p>We are using Ansible Vault to store the keys, which I go into more detail how to use in <a href="/posts/ansible-for-networking-part-2-lab/">Part 2</a></p>
<h4 id="applying-the-task">Applying the task</h4>
<p>As the task has a tag, we can apply the role using the <code>--tag</code> argument when running our playbook. However as Ansible Vault is used, you&rsquo;ll also need to use <code>--ask-vault-pass</code> (or one of the other methods for supply your Vault passwords) otherwise Ansible will not be able to decrypt the secrets.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ansible-playbook ios.yaml --tags snmp --ask-vault-pass
Vault password: 

PLAY <span style="color:#f92672">[</span>ios<span style="color:#f92672">]</span> **********************************************************************************************************************************************

TASK <span style="color:#f92672">[</span>snmp : Enable SNMPv3<span style="color:#f92672">]</span> ******************************************************************************************************************************
changed: <span style="color:#f92672">[</span>cisco-02<span style="color:#f92672">]</span>
changed: <span style="color:#f92672">[</span>cisco-01<span style="color:#f92672">]</span>

PLAY RECAP ***********************************************************************************************************************************************
cisco-01                   : ok<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>    changed<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>    unreachable<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>    failed<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>    skipped<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>    rescued<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>    ignored<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>   
cisco-02                   : ok<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>    changed<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>    unreachable<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>    failed<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>    skipped<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>    rescued<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>    ignored<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>   
</code></pre></div><p>The generated configuration looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">snmp-server group yetiops_group v3 priv 
snmp-server location Yeti Home
snmp-server contact The Hairy One
</code></pre></div><p>It will also generate the line <code>snmp-server user $USER $GROUP v3 auth sha $AUTH-KEY priv aes 128 $PRIV-KEY</code>, but this will not appear in your configuration, due to the <code>service password-encryption</code> option specified earlier.</p>
<h4 id="verification-5">Verification</h4>
<p>To check whether this is working, you will need either some form of monitoring system, or something like <code>snmpwalk</code> to check it: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">! snmpwalk to cisco-01
$ sudo snmpwalk -v3 -l authPriv -u yetiops -a SHA -A $AUTH-KEY -x AES -X $PRIV-KEY 192.0.2.101
SNMPv2-MIB::sysObjectID.0 <span style="color:#f92672">=</span> OID: SNMPv2-SMI::enterprises.9.1.1041
DISMAN-EVENT-MIB::sysUpTimeInstance <span style="color:#f92672">=</span> Timeticks: <span style="color:#f92672">(</span>2355886<span style="color:#f92672">)</span> 6:32:38.86
SNMPv2-MIB::sysContact.0 <span style="color:#f92672">=</span> STRING: The Hairy One
SNMPv2-MIB::sysName.0 <span style="color:#f92672">=</span> STRING: cisco-01.stubob.lab
SNMPv2-MIB::sysLocation.0 <span style="color:#f92672">=</span> STRING: Yeti Home
SNMPv2-MIB::sysServices.0 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">78</span>
<span style="color:#f92672">[</span>...<span style="color:#f92672">]</span>

! snmpwalk to cisco-02
$ sudo snmpwalk -v3 -l authPriv -u yetiops -a SHA -A yeti_hash123 -x AES -X yeti_key123 192.0.2.201
SNMPv2-MIB::sysObjectID.0 <span style="color:#f92672">=</span> OID: SNMPv2-SMI::enterprises.9.1.1041
DISMAN-EVENT-MIB::sysUpTimeInstance <span style="color:#f92672">=</span> Timeticks: <span style="color:#f92672">(</span>2356123<span style="color:#f92672">)</span> 6:32:41.23
SNMPv2-MIB::sysContact.0 <span style="color:#f92672">=</span> STRING: The Hairy One
SNMPv2-MIB::sysName.0 <span style="color:#f92672">=</span> STRING: cisco-02.stubob.lab
SNMPv2-MIB::sysLocation.0 <span style="color:#f92672">=</span> STRING: Yeti Home
SNMPv2-MIB::sysServices.0 <span style="color:#f92672">=</span> INTEGER: <span style="color:#ae81ff">78</span>
<span style="color:#f92672">[</span>...<span style="color:#f92672">]</span>
</code></pre></div><p>Looks like its working!</p>
<h3 id="access-lists">Access Lists</h3>
<p>To limit what traffic can talk over our interface from the <em>edge</em> router to the <em>netsvr-01</em> machine, we are going to use an Access List. Access Lists are stateless, meaning you have to match traffic in both directions, rather than just applying it one direction and the device matching return traffic.</p>
<p>The role is created with the name <code>acl</code>, <code>ansible-galaxy init acl</code> in the roles directory.</p>
<p>There is no native Ansible module for enabling Access Lists, so again we are using the <code>ios_config</code> module. Access lists are even provided as one of the examples for how to use <code>ios_config</code> module in IOS when you read the Ansible documentation.</p>
<h4 id="playbook-3">Playbook</h4>
<p>The playbook looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e">## tasks file for acl</span>
- <span style="color:#66d9ef">name</span>: Edge Access List - Outbound IPv4
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#ae81ff">10</span> permit icmp any any log
      - <span style="color:#ae81ff">20</span> permit udp any host {{ log_host }}  eq syslog log
      - <span style="color:#ae81ff">30</span> permit tcp any host {{ tacacs[<span style="color:#e6db74">&#39;ipv4&#39;</span>] }}  eq tacacs log
      - <span style="color:#ae81ff">1000</span> deny ip any any log
    <span style="color:#66d9ef">parents</span>:
      - ip access-list extended EDGE-OUT
  <span style="color:#66d9ef">when</span>:
    - rtr_role is search(<span style="color:#e6db74">&#34;edge&#34;</span>)
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv4

- <span style="color:#66d9ef">name</span>: Edge Access List - Inbound IPv4
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#ae81ff">10</span> permit icmp any any log
      - <span style="color:#ae81ff">20</span> permit tcp host {{ tacacs[<span style="color:#e6db74">&#39;ipv4&#39;</span>] }} eq tacacs any log
      - <span style="color:#ae81ff">1000</span> deny ip any any log
    <span style="color:#66d9ef">parents</span>:
      - ip access-list extended EDGE-IN
  <span style="color:#66d9ef">when</span>:
    - rtr_role is search(<span style="color:#e6db74">&#34;edge&#34;</span>)
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv4

- <span style="color:#66d9ef">name</span>: Edge Access List - BGP Outbound IPv4
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#e6db74">&#34;{{ item.acl.acl_index }} permit tcp any host {{ item.peer }} eq bgp log&#34;</span>
    <span style="color:#66d9ef">parents</span>:
      - ip access-list extended EDGE-OUT
  <span style="color:#66d9ef">when</span>:
    - rtr_role is search(<span style="color:#e6db74">&#34;edge&#34;</span>)
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv4 is defined
    - item.acl is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv4 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv4

- <span style="color:#66d9ef">name</span>: Edge Access List - BGP Inbound IPv4
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#e6db74">&#34;{{ item.acl.acl_index }} permit tcp host {{ item.peer }} eq bgp any log&#34;</span>
    <span style="color:#66d9ef">parents</span>:
      - ip access-list extended EDGE-IN
  <span style="color:#66d9ef">when</span>:
    - rtr_role is search(<span style="color:#e6db74">&#34;edge&#34;</span>)
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv4 is defined
    - item.acl is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv4 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv4

- <span style="color:#66d9ef">name</span>: Apply Inbound IPv4 ACL
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - ip access-group {{ item.acl.ipv4.in }} in 
    <span style="color:#66d9ef">parents</span>:
      - interface {{ item.ios_if }}
  <span style="color:#66d9ef">when</span>:
    - item.acl is defined
    - item.acl.ipv4 is defined
    - item.acl.ipv4.in is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv4

- <span style="color:#66d9ef">name</span>: Apply Outbound IPv4 ACL
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - ip access-group {{ item.acl.ipv4.out }} out
    <span style="color:#66d9ef">parents</span>:
      - interface {{ item.ios_if }}
  <span style="color:#66d9ef">when</span>:
    - item.acl is defined
    - item.acl.ipv4 is defined
    - item.acl.ipv4.out is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv4

- <span style="color:#66d9ef">name</span>: Edge Access List - Outbound IPv6
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - sequence <span style="color:#ae81ff">10</span> permit icmp any any log
      - sequence <span style="color:#ae81ff">1000</span> deny ipv6 any any log
    <span style="color:#66d9ef">parents</span>:
      - ipv6 access-list EDGEv6-OUT
  <span style="color:#66d9ef">when</span>:
    - rtr_role is search(<span style="color:#e6db74">&#34;edge&#34;</span>)
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv6

- <span style="color:#66d9ef">name</span>: Edge Access List - Inbound IPv6
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - sequence <span style="color:#ae81ff">10</span> permit icmp any any log
      - sequence <span style="color:#ae81ff">1000</span> deny ipv6 any any log
    <span style="color:#66d9ef">parents</span>:
      - ipv6 access-list EDGEv6-IN
  <span style="color:#66d9ef">when</span>:
    - rtr_role is search(<span style="color:#e6db74">&#34;edge&#34;</span>)
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv6

- <span style="color:#66d9ef">name</span>: Edge Access List - BGP Outbound IPv6
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#e6db74">&#34;sequence {{ item.acl.acl_index }} permit tcp any host {{ item.peer }} eq bgp log&#34;</span>
    <span style="color:#66d9ef">parents</span>:
      - ipv6 access-list EDGEv6-OUT
  <span style="color:#66d9ef">when</span>:
    - rtr_role is search(<span style="color:#e6db74">&#34;edge&#34;</span>)
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv6 is defined
    - item.acl is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv6 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv6

- <span style="color:#66d9ef">name</span>: Edge Access List - BGP Inbound IPv6
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - <span style="color:#e6db74">&#34;sequence {{ item.acl.acl_index }} permit tcp host {{ item.peer }} eq bgp any log&#34;</span>
    <span style="color:#66d9ef">parents</span>:
      - ipv6 access-list EDGEv6-IN
  <span style="color:#66d9ef">when</span>:
    - rtr_role is search(<span style="color:#e6db74">&#34;edge&#34;</span>)
    - bgp is defined
    - bgp.neighbors is defined
    - bgp.neighbors.ipv6 is defined
    - item.acl is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ bgp.neighbors.ipv6 }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv6

- <span style="color:#66d9ef">name</span>: Apply Inbound IPv6 ACL
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - ipv6 traffic-filter {{ item.acl.ipv6.in }} in
    <span style="color:#66d9ef">parents</span>:
      - interface {{ item.ios_if }}
  <span style="color:#66d9ef">when</span>:
    - item.acl is defined
    - item.acl.ipv6 is defined
    - item.acl.ipv6.in is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv6

- <span style="color:#66d9ef">name</span>: Apply Outbound IPv6 ACL
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">lines</span>:
      - ipv6 traffic-filter {{ item.acl.ipv6.out }} out
    <span style="color:#66d9ef">parents</span>:
      - interface {{ item.ios_if }}
  <span style="color:#66d9ef">when</span>:
    - item.acl is defined
    - item.acl.ipv6 is defined
    - item.acl.ipv6.out is defined
  <span style="color:#66d9ef">loop</span>: <span style="color:#e6db74">&#34;{{ interfaces }}&#34;</span>
  <span style="color:#66d9ef">tags</span>:
    - acl
    - acl_ipv6
</code></pre></div><p>To summarize what is being applied here: -</p>
<ul>
<li>Allow any ICMP traffic (including pings) inbound and outbound (IPv4 and IPv6)</li>
<li>Allow syslog outbound (IPv4 only)
<ul>
<li>Syslog is UDP, therefore no return rule is required</li>
</ul>
</li>
<li>Allow any eBGP peers to talk inbound and outbound (IPv4 and IPv6)</li>
<li>Allow TACACS destined to and from the TACACS server (IPv4 only)</li>
<li>Apply the access lists to our interfaces</li>
</ul>
<h5 id="edge-access-list---outbound-ipv4">Edge Access List - Outbound IPv4</h5>
<p><strong>Ansible module</strong>: <code>ios_config</code></p>
<p>The module creates an access list called <code>EDGE-OUT</code>, and creates rules in this list for ICMP, syslog, TACACS+, and denies anything else. Each rule also includes the <code>log</code> keyword, so that we will get a log message for every time each rule is matched.</p>
<p>In a production scenario, you probably wouldn&rsquo;t log every match, but this helps in a lab to ensure that every rule is being used appropriately.</p>
<p>The numbers at the start of each line are the sequence number, saying in what order each rule is evaluated. The reason for placing the final deny rule at the sequence number 1000 is so that we have enough space to add entries before the deny rules (up to 999), rather than having to carve out gaps, or deleting the deny rule and moving it (allowing all traffic through while doing so).</p>
<p>We pull the IP of the syslog server and TACACS+ server from our <code>group_vars</code>, and we also only apply this to the edge router (using the <code>rtr_role</code> variable, defined in the <code>host_vars</code> file).</p>
<p><strong>group_vars</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">log_host</span>: <span style="color:#ae81ff">10.100.101.254</span>
<span style="color:#66d9ef">tacacs</span>:
  <span style="color:#66d9ef">ipv4</span>: <span style="color:#ae81ff">192.0.2.1</span>
  <span style="color:#66d9ef">secret</span>: supersecret
</code></pre></div><p><strong>edge (cisco-01) host_vars</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">router_id</span>: <span style="color:#ae81ff">192.0.2.101</span>
<span style="color:#66d9ef">rtr_role</span>: edge
</code></pre></div><p>The generated configuration from this is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ip access-list extended EDGE-OUT
 permit icmp any any log
 permit udp any host 10.100.101.254 eq syslog log
 permit tcp any host 192.0.2.1 eq tacacs log
 deny   ip any any log
</code></pre></div><p>Unfortunately because we have to specify the sequence numbers, the task registers as changed every time you run it (in the running configuration on the router, the sequence numbers are not shown). If you have a fairly static access list, you could probably do away with the sequence numbers, but we will be adding dynamic elements to this list soon.</p>
<h5 id="edge-access-list---inbound-ipv4">Edge Access List - Inbound IPv4</h5>
<p><strong>Ansible module</strong>: <code>ios_config</code></p>
<p>This task is almost identical to the previous one, except that: -</p>
<ul>
<li>Syslog is UDP, so no return traffic is required</li>
<li>Where we are matching specific IPs, they need to be matched as the source of traffic, not the destination</li>
</ul>
<p>So rather than <code>permit tcp any host {{ tacacs['ipv4'] }} eq tacacs log</code> (which matches any source address, with a destination of the TACACS+ server), we instead have <code>permit tcp host {{ tacacs['ipv4'] }} eq tacacs any log</code> (which matches a source of TACACS+, to any destination IP).</p>
<p>The generated configuration is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ip access-list extended EDGE-IN
 permit icmp any any log
 permit tcp host 192.0.2.1 eq tacacs any log
 deny   ip any any log
</code></pre></div><h5 id="edge-access-list---bgp-outbound-ipv4">Edge Access List - BGP Outbound IPv4</h5>
<p><strong>Ansible module</strong>: <code>ios_config</code></p>
<p>This task is where we start to add some &ldquo;dynamic&rdquo; elements, in that we could have 1 BGP peer, or we could have 50, but all would need to go in this access list. You might wonder when you would have that many peers over a single interface. If you are connected to any of the Internet Exchanges/Peering LANs (e.g. <a href="https://www.linx.net">LINX</a>, <a href="https://de-cix.net">DE-CIX</a>, <a href="https://ams-ix.net">AMS-IX</a>), you might have 10s or 100s of peers over the same interface.</p>
<p>The task here looks for values in the <code>host_vars</code> file, and will apply when: -</p>
<ul>
<li>The <code>rtr_role</code> is <code>edge</code> AND</li>
<li>The <code>bgp</code> variable exists AND</li>
<li>The <code>bgp.neighbors</code> variable exists AND</li>
<li>The <code>bgp.neighbors.ipv4</code> variable exists AND</li>
<li>For each BGP peer, if the variable ACL Is defined</li>
</ul>
<p>We loop through all of the peers we have defined to find whether the above conditions are true. We supply an <code>acl_index</code> number in the <code>host_vars</code>, to say where in our access list to place the configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">rtr_role</span>: edge
<span style="color:#66d9ef">bgp</span>:
  <span style="color:#66d9ef">local_as</span>: <span style="color:#ae81ff">65101</span>
  <span style="color:#66d9ef">redist</span>:
    <span style="color:#66d9ef">ospf</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">neighbors</span>:
    <span style="color:#66d9ef">ipv4</span>:
     - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">10.100.101.254</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65430</span>
       <span style="color:#66d9ef">ebgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">acl</span>:
         <span style="color:#66d9ef">acl_index</span>: <span style="color:#ae81ff">110</span>
     - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">192.0.2.201</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65101</span>
       <span style="color:#66d9ef">ibgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">update_source</span>: Loopback0
       <span style="color:#66d9ef">default_originate</span>: <span style="color:#66d9ef">true</span>
</code></pre></div><p>In the above, only the first peer has the <code>acl</code> variable defined. The configuration this will then generate is: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ip access-list extended EDGE-OUT
 permit tcp any host 10.100.101.254 eq bgp log
</code></pre></div><p>With the rest of the access list, you would see the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ip access-list extended EDGE-OUT
 permit icmp any any log
 permit udp any host 10.100.101.254 eq syslog log
 permit tcp any host 192.0.2.1 eq tacacs log
 permit tcp any host 10.100.101.254 eq bgp log
 deny   ip any any log
</code></pre></div><p>The reason that the BGP rule is before before the final <code>deny</code> rule is because we supplied an index number, to use as the sequence number in the access list. Without this, it would go after the deny rule.</p>
<h5 id="edge-access-list---bgp-inbound-ipv4">Edge Access List - BGP Inbound IPv4</h5>
<p><strong>Ansible module</strong>: <code>ios_config</code></p>
<p>This task is the same as the for the outbound IPv4 BGP task, except that the order of matching is reversed. Rather than having the BGP peer as the destination in the access list, in this task it is the source (because the traffic is being matched FROM the peer, not to it)</p>
<p>The resulting access list, using the same <code>host_vars</code> in the previous task, would be generated as: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ip access-list extended EDGE-IN
 permit tcp host 10.100.101.254 eq bgp any log
</code></pre></div><p>The full access list would then be: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">ip access-list extended EDGE-IN
 permit icmp any any log
 permit tcp host 192.0.2.1 eq tacacs any log
 permit tcp host 10.100.101.254 eq bgp any log
 deny   ip any any log
</code></pre></div><h5 id="apply-inbound-ipv4-acl">Apply Inbound IPv4 ACL</h5>
<p><strong>Ansible module</strong>: <code>ios_config</code></p>
<p>This task applies the access list to an interface. The interface (or interfaces) comes from our <code>host_vars</code> again: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;GigabitEthernet0/1.101&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To netsvr&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.100.101.253/24&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:101::f/64&#34;</span>
    <span style="color:#66d9ef">acl</span>:
      <span style="color:#66d9ef">ipv4</span>:
        <span style="color:#66d9ef">in</span>: EDGE-IN
        <span style="color:#66d9ef">out</span>: EDGE-OUT
      <span style="color:#66d9ef">ipv6</span>:
        <span style="color:#66d9ef">in</span>: EDGEv6-IN
        <span style="color:#66d9ef">out</span>: EDGEv6-OUT
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
</code></pre></div><p>As you can see here, with have our <code>acl</code> section, which specifies the name of our inbound Edge Access list. The task we are using looks for whether: -</p>
<ul>
<li>The <code>acl</code> variable is defined AND</li>
<li>The <code>acl.ipv4</code> variable is defined AND</li>
<li>The <code>acl.ipv4.in</code> variable is defined</li>
</ul>
<p>This means that we can supply only an inbound access list, or only an outbound access list, or only IPv4, or only IPv6. We do not need to have all of them for the tasks to work.</p>
<p>In our <code>host_vars</code>, the only interface that has an <code>acl</code> is our interface to the <code>netsvr-01</code> machine. The configuration that would be applied is: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">interface GigabitEthernet0/1.101
 ip access-group EDGE-IN in
</code></pre></div><p>In context with the other configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">interface GigabitEthernet0/1.101
 description To netsvr
 encapsulation dot1Q 101
 ip address 10.100.101.253 255.255.255.0
 ip access-group EDGE-IN in
 ip ospf 1 area 0.0.0.0
 ipv6 address 2001:DB8:101::F/64
 ipv6 ospf 1 area 0.0.0.0
</code></pre></div><h5 id="apply-outbound-ipv4-acl">Apply Outbound IPv4 ACL</h5>
<p><strong>Ansible module</strong>: <code>ios_config</code></p>
<p>This task is almost identical to the previous task, except it applies in the outbound direction. We are looking for the <code>acl.ipv4.out</code> variable in our interfaces defined in <code>host_vars</code>, and then applying the Access List defined.</p>
<p>Based upon the <code>host_vars</code> noted above, the resulting configuration would be: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">interface GigabitEthernet0/1.101
 ip access-group EDGE-OUT out
</code></pre></div><p>In context with the other configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">interface GigabitEthernet0/1.101
 description To netsvr
 encapsulation dot1Q 101
 ip address 10.100.101.253 255.255.255.0
 ip access-group EDGE-IN in
 ip access-group EDGE-OUT out
 ip ospf 1 area 0.0.0.0
 ipv6 address 2001:DB8:101::F/64
 ipv6 ospf 1 area 0.0.0.0
</code></pre></div><h5 id="ipv6-access-lists">IPv6 Access Lists</h5>
<p>All of the following tasks after this are for applying access lists for IPv6. Everything is almost identical to IPv4, except for the following minor details: -</p>
<ul>
<li>You need to use the keyword <code>sequence</code> before you use a sequence number
<ul>
<li>IPv4 - <code>permit icmp any any log</code></li>
<li>IPv6 - <code>sequence 10 permit icmp any any log</code></li>
</ul>
</li>
<li>IPv6 access lists are applied using the <code>traffic-filter</code> keyword rather than <code>access-group</code>
<ul>
<li>IPv4 - <code>ip access-group EDGE-IN in</code></li>
<li>IPv6 - <code>ipv6 traffic-filter EDGEv6-IN in</code></li>
</ul>
</li>
</ul>
<p>You also need to name your access lists differently from your IPv4 access lists. If you use the same name IOS will raise an error, saying there is already an access list with the same name (despite the fact you are defining it for a different address type).</p>
<p>After all the tasks are applied, the following configuration is generated: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">interface GigabitEthernet0/1.101
 ipv6 traffic-filter EDGEv6-IN in
 ipv6 traffic-filter EDGEv6-OUT out

ipv6 access-list EDGEv6-IN
 sequence 10 permit icmp any any log
 sequence 110 permit tcp host 2001:DB8:101::FFFF eq bgp any log
 sequence 1000 deny ipv6 any any log

ipv6 access-list EDGEv6-OUT
 sequence 10 permit icmp any any log
 sequence 110 permit tcp any host 2001:DB8:101::FFFF eq bgp log
 sequence 1000 deny ipv6 any any log
</code></pre></div><p>When seen in context with our other configuration: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">interface GigabitEthernet0/1.101
 description To netsvr
 encapsulation dot1Q 101
 ip address 10.100.101.253 255.255.255.0
 ip access-group EDGE-IN in
 ip access-group EDGE-OUT out
 ip ospf 1 area 0.0.0.0
 ipv6 address 2001:DB8:101::F/64
 ipv6 traffic-filter EDGEv6-IN in
 ipv6 traffic-filter EDGEv6-OUT out
 ipv6 ospf 1 area 0.0.0.0

ipv6 access-list EDGEv6-IN
 sequence 10 permit icmp any any log
 sequence 110 permit tcp host 2001:DB8:101::FFFF eq bgp any log
 sequence 1000 deny ipv6 any any log

ipv6 access-list EDGEv6-OUT
 sequence 10 permit icmp any any log
 sequence 110 permit tcp any host 2001:DB8:101::FFFF eq bgp log
 sequence 1000 deny ipv6 any any log
</code></pre></div><h4 id="verification-6">Verification</h4>
<p>We shall now verify that the access lists are working, and that they match traffic as intended: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Show access lists
cisco-01#           show ip access-lists 
Extended IP access list EDGE-IN
    10 permit icmp any any log (52 matches)
    20 permit tcp host 192.0.2.1 eq tacacs any log (4089 matches)
    30 permit tcp host 10.100.101.254 eq bgp any log (3093 matches)
    40 deny ip any any log (7 matches)
Extended IP access list EDGE-OUT
    10 permit icmp any any log (25 matches)
    20 permit udp any host 10.100.101.254 eq syslog log (4 matches)
    30 permit tcp any host 192.0.2.1 eq tacacs log (768 matches)
    40 permit tcp any host 10.100.101.254 eq bgp log
    50 deny ip any any log

cisco-01#show ipv6 access-list 
IPv6 access list EDGEv6-IN
    permit icmp any any log (4398 matches) sequence 10
    permit tcp host 2001:DB8:101::FFFF eq bgp any log (3115 matches) sequence 110
    deny ipv6 any any log sequence 1000
IPv6 access list EDGEv6-OUT
    permit icmp any any log (62 matches) sequence 10
    permit tcp any host 2001:DB8:101::FFFF eq bgp log sequence 110
    deny ipv6 any any log sequence 1000

! Are we logging traffic?
*Mar  4 07:52:19.002: %IPV6_ACL-6-ACCESSLOGDP: list EDGEv6-IN/10 permitted icmpv6 FE80::90AB:180D:BBD:92C0 -&gt; FE80::5054:FF:FEDD:4D5C (136/0), 10 packets
*Mar  4 07:52:19.002: %IPV6_ACL-6-ACCESSLOGDP: list EDGEv6-IN/10 permitted icmpv6 FE80::90AB:180D:BBD:92C0 -&gt; FE80::5054:FF:FEDD:4D5C (135/0), 10 packets
*Mar  4 07:52:19.002: %IPV6_ACL-6-ACCESSLOGP: list EDGEv6-IN/110 permitted tcp 2001:DB8:101::FFFF(179) -&gt; 2001:DB8:101::F(32940), 20 packets
*Mar  4 07:52:40.997: %SEC-6-IPACCESSLOGP: list EDGE-IN permitted tcp 192.0.2.1(49) -&gt; 10.100.101.253(40173), 1 packet  
*Mar  4 07:52:53.020: %SEC-6-IPACCESSLOGP: list EDGE-IN permitted tcp 192.0.2.1(49) -&gt; 10.100.101.253(28503), 1 packet 

! Are our BGP sessions still working?
cisco-01#show ip bgp summary 
BGP router identifier 192.0.2.101, local AS number 65101
BGP table version is 10, main routing table version 10
6 network entries using 864 bytes of memory
6 path entries using 480 bytes of memory
4/3 BGP path/bestpath attribute entries using 608 bytes of memory
1 BGP AS-PATH entries using 24 bytes of memory
0 BGP route-map cache entries using 0 bytes of memory
0 BGP filter-list cache entries using 0 bytes of memory
BGP using 1976 total bytes of memory
BGP activity 12/1 prefixes, 13/2 paths, scan interval 60 secs

Neighbor        V           AS MsgRcvd MsgSent   TblVer  InQ OutQ Up/Down  State/PfxRcd
10.100.101.254  4        65430    1126    1241       10    0    0 18:41:29        1
192.0.2.201     4        65101    1622    1626       10    0    0 1d00h           0

cisco-01#show bgp ipv6 unicast summary 
BGP router identifier 192.0.2.101, local AS number 65101
BGP table version is 134, main routing table version 134
5 network entries using 840 bytes of memory
5 path entries using 520 bytes of memory
3/3 BGP path/bestpath attribute entries using 456 bytes of memory
1 BGP AS-PATH entries using 24 bytes of memory
0 BGP route-map cache entries using 0 bytes of memory
0 BGP filter-list cache entries using 0 bytes of memory
BGP using 1840 total bytes of memory
BGP activity 12/1 prefixes, 13/2 paths, scan interval 60 secs

Neighbor        V           AS MsgRcvd MsgSent   TblVer  InQ OutQ Up/Down  State/PfxRcd
2001:DB8:101::FFFF
                4        65430    1126    1270      134    0    0 18:41:55        1
2001:DB8:901:BEEF::2
                4        65101    1625    1696      134    0    0 1d00h           0

! How about syslog and tacacs?
[stuh84@netsvr-01 /var/log] $ tail -n2 tac_plus.acct 
Mar  4 07:53:47	10.100.101.253	ansible	tty578	10.15.30.1	stop	task_id=393	timezone=UTC	service=shell	priv-lvl=1	cmd=show ip bgp summary
Mar  4 07:54:18	10.100.101.253	ansible	tty578	10.15.30.1	stop	task_id=394	timezone=UTC	service=shell	priv-lvl=1	cmd=show bgp ipv6 unicast summary

[stuh84@netsvr-01 /var/log/remote] $ sudo tail -n2 10.100.201.253
2020-03-02T12:02:37-05:00 10.100.201.253: %GRUB-5-CONFIG_WRITTEN: GRUB configuration was written to disk successfully.
2020-03-03T13:44:50-05:00 10.100.201.253: %SYS-5-CONFIG_I: Configured from console by ansible on vty0 (10.15.30.1)

[stuh84@netsvr-01 /var/log/remote] $ sudo tail -n2 10.100.101.253 
2020-03-04T07:56:29-05:00 10.100.101.253: %SEC-6-IPACCESSLOGP: list EDGE-IN permitted tcp 192.0.2.1(49) -&gt; 10.100.101.253(28292), 1 packet  
2020-03-04T07:56:30-05:00 10.100.101.253: %SYS-5-CONFIG_I: Configured from console by ansible on vty0 (10.15.30.1)

! And what about attempting to SSH from the **netsvr-01** machine?
[stuh84@netsvr-01 /var/log/remote] $ ssh ansible@192.0.2.101
ssh: connect to host 192.0.2.101 port 22: No route to host

! Can we ping it?
[stuh84@netsvr-01 /var/log/remote] $ ping 192.0.2.101
PING 192.0.2.101 (192.0.2.101) 56(84) bytes of data.
64 bytes from 192.0.2.101: icmp_seq=1 ttl=255 time=1.81 ms
</code></pre></div><p>Looks like it&rsquo;s all working!</p>
<h3 id="nat">NAT</h3>
<p>NAT (<strong>N</strong>etwork <strong>A</strong>ddress <strong>T</strong>ranslation) is the concept of masquerading an IP as another. The most common use case of this is to have a number of private IPv4 ranges appear behind one or more public IPv4 ranges. This is used in pretty much every home router to allow many devices to appear behind one IPv4 address.</p>
<p>We are going to do the same here, allowing the <em>internal</em> router to get access to the internet.</p>
<h4 id="playbook-4">Playbook</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for nat</span>
- <span style="color:#66d9ef">name</span>: Apply NAT Overload
  <span style="color:#66d9ef">ios_config</span>:
    <span style="color:#66d9ef">src</span>: nat-overload.j2
  <span style="color:#66d9ef">tags</span>:
  - nat
</code></pre></div><p>The playbook is short, as all we are doing is applying a template. Again, Ansible does not provide an IOS module for NAT, so we are using the <code>ios_config</code> module to achieve it instead.</p>
<h4 id="template">Template</h4>
<p>The template looks like the below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">access-list 1 permit any
{% for interface in interfaces %}
{% if interface['nat'] is defined %}
interface {{ interface['ios_if'] }}
 ip nat {{ interface['nat']['role'] }}
!
{% if 'outside' in interface['nat']['role'] %}
ip nat inside source list 1 interface {{ interface['ios_if'] }} overload
{% endif %}
{% endif %}
{% endfor %}
</code></pre><p>The first line (the access list) says what traffic to match for NAT. You could make this more specific, or you could involve a <code>route-map</code> and make this conditional (i.e. only NAT to certain destinations). However as this is a very basic configuration, we are happy to NAT everything.</p>
<p>The rest of the template says for any interface in our <code>host_vars</code> that has the <code>nat</code> variable, apply the value of the <code>nat.role</code> variable.</p>
<p>Finally, if the interface has the role of <code>outside</code> (i.e. the outbound interface for NAT), then apply the NAT rule. This NAT rule says that any traffic that matches the access list (i.e. any traffic) and is exiting via the Outside interface, change its source IP address (i.e. the inside address) to the IP of the outgoing interface (the <strong>overload</strong> keyword).</p>
<h4 id="configuration-1">Configuration</h4>
<p>Our <code>host_vars</code> with NAT variables can be seen below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;GigabitEthernet0/1.201&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To cisco-02&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.100.201.254/24&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:201::a/64&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
    <span style="color:#66d9ef">nat</span>:
      <span style="color:#66d9ef">role</span>: inside
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;GigabitEthernet0/2&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To the Internet&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;dhcp&#34;</span>
    <span style="color:#66d9ef">nat</span>:
      <span style="color:#66d9ef">role</span>: outside
</code></pre></div><p>The generated configuration would therefore be: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">interface GigabitEthernet0/1.201
 ip nat inside

interface GigabitEthernet0/2
 ip nat outside

ip nat inside source list 1 interface GigabitEthernet0/2 overload
</code></pre></div><p>You&rsquo;ll also see when this is applied, a NAT Virtual Interface is created on the router itself: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">cisco-01#show int desc
Interface                      Status         Protocol Description
Gi0/0                          up             up       Management
Gi0/1                          up             up       VLAN Bridge
Gi0/1.101                      up             up       To netsvr
Gi0/1.201                      up             up       To cisco-02
Gi0/2                          up             up       To the Internet
Lo0                            up             up       Loopback
NV0                            up             up 
</code></pre></div><p>You don&rsquo;t need to interact with the interface directly, just don&rsquo;t worry about random interfaces appearing!</p>
<h4 id="verification-7">Verification</h4>
<p>We shall now verify if this is working:  -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Can we reach the internet from the internal router?
cisco-02#ping 8.8.8.8
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 8.8.8.8, timeout is 2 seconds:
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 18/19/21 ms

cisco-02#ping 1.1.1.1 
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 1.1.1.1, timeout is 2 seconds:
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 12/14/16 ms

cisco-02#ping 8.8.8.8 source lo0
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 8.8.8.8, timeout is 2 seconds:
Packet sent with a source address of 192.0.2.201 
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 20/20/21 ms

cisco-02#ping 9.9.9.9 source lo0
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 9.9.9.9, timeout is 2 seconds:
Packet sent with a source address of 192.0.2.201 
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 22/23/24 ms

! What does this look like on the edge router?
cisco-01#show ip nat translations 
Pro Inside global         Inside local          Outside local         Outside global
icmp 192.168.122.86:9     10.100.201.253:9      8.8.8.8:9             8.8.8.8:9
icmp 192.168.122.86:10    10.100.201.253:10     1.1.1.1:10            1.1.1.1:10
icmp 192.168.122.86:11    192.0.2.201:11        8.8.8.8:11            8.8.8.8:11
icmp 192.168.122.86:12    192.0.2.201:12        9.9.9.9:12            9.9.9.9:12
</code></pre></div><p>To explain the last command, the <em>Inside local</em> address is being translated to the <em>Inside global</em> address. We are not using any destination NAT (i.e. translation of the destination IP, rather than the source IP), so we see no changes in the <em>Outside local</em> and <em>Outside global</em></p>
<p>Everything looks good!</p>
<h3 id="aaa">AAA</h3>
<p>The final task is to apply AAA (<strong>A</strong>uthentication, <strong>A</strong>uthorization and <strong>A</strong>ccounting). We use AAA to achieve the following: -</p>
<ul>
<li><strong>Authenticating</strong> the user has used the correct credentials</li>
<li>Checking that the user is <strong>Authorized</strong> to login, and run tasks and/or make changes</li>
<li>Log all commands entered on the routers in an <strong>Accounting</strong> log</li>
</ul>
<p>We are using <code>tac_plus</code> on our <strong>netsvr-01</strong> machine to act as the AAA server (configuration covered in <a href="/posts/ansible-for-networking-part-2-lab/">Part 2</a>.</p>
<p>I did have to update the <code>tac_plus</code> configuration so that each user has an <code>enable</code> password as well as <code>login</code> password. This isn&rsquo;t required for standard SSH access, as the privilege level supplied placed the user automatically into enable mode.</p>
<p>However when using the KVM console (i.e. <code>virsh console cisco-01</code>), this is not the case. The user was not placed into enable mode, and the router&rsquo;s local enable password did not work.</p>
<p>The following changes to the <code>tac_plus</code> configuration fixed this: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">{% for user in netusers %}
user = {{ user }} {
    member = netwrite
    login = des {{ netusers[user][&#39;tacpwd&#39;] }}
    enable = des {{ netusers[user][&#39;tacpwd&#39;] }} &lt;----------
}
{% endfor %}
</code></pre></div><p>Again, no Ansible module exists for AAA, so we use the <code>ios_config</code> module.</p>
<h4 id="playbook-5">Playbook</h4>
<p>The playbook is another with just one task, which applies a template.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#75715e">## tasks file for aaa</span>
- <span style="color:#66d9ef">name</span>: Enable TACACS<span style="color:#e6db74">+
</span><span style="color:#e6db74">  ios_config:</span>
    <span style="color:#66d9ef">src</span>: tacacs.j2
  <span style="color:#66d9ef">tags</span>: 
  - aaa
</code></pre></div><h4 id="template-1">Template</h4>
<p>The template used can be seen below: -</p>
<pre><code class="language-jinja2" data-lang="jinja2">aaa new-model
aaa authentication login default group tacacs+ local
aaa authentication enable default group tacacs+ enable
aaa authorization exec default group tacacs+ none
aaa authorization commands 0 default group tacacs+ none
aaa authorization commands 1 default group tacacs+ none
aaa authorization commands 15 default group tacacs+ none
aaa accounting exec default start-stop group tacacs+
aaa accounting commands 0 default start-stop group tacacs+
aaa accounting commands 1 default start-stop group tacacs+
aaa accounting commands 15 default start-stop group tacacs+
tacacs-server directed-request
tacacs server NETSVR
 address ipv4 {{ tacacs['ipv4'] }}
 key 0 {{ tacacs['secret'] }}
 timeout 1
line vty 0 4
 login authentication default
line console 0
 login authentication default
</code></pre><p>The only part we are pulling from our variables (<code>group_vars</code> in this case) is the IPv4 address of the <code>tac_plus</code> server, and the TACACS+ secret (used to encrypt exchange between the routers and the server).</p>
<p>For the rest of the commands are mostly standard AAA/TACACS+ configuration. They define a default AAA group that uses TACACS+ when available, and falls back to local logins when TACACS+ is unreachable.</p>
<p>We also apply the <code>login authentication default</code> line to our VTYs (Virtual Terminals, or remote sessions) and also our console &ldquo;port&rdquo; (the KVM console in our case).</p>
<p>If you are running multiple TACACS+ servers (which you probably should in a production scenario), you would need to change the template to loop through your servers, rather than supply a single one.</p>
<h4 id="configuration-2">Configuration</h4>
<p>Our <code>group_vars</code> contain our TACACS+ details: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">tacacs</span>:
  <span style="color:#66d9ef">ipv4</span>: <span style="color:#ae81ff">192.0.2.1</span>
  <span style="color:#66d9ef">secret</span>: supersecret
</code></pre></div><p>The configuration generated looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">aaa new-model

aaa authentication login default group tacacs+ local
aaa authentication enable default group tacacs+ enable
aaa authorization exec default group tacacs+ none 
aaa authorization commands 0 default group tacacs+ none 
aaa authorization commands 1 default group tacacs+ none 
aaa authorization commands 15 default group tacacs+ none 
aaa accounting exec default start-stop group tacacs+
aaa accounting commands 0 default start-stop group tacacs+
aaa accounting commands 1 default start-stop group tacacs+
aaa accounting commands 15 default start-stop group tacacs+

tacacs-server directed-request
tacacs server NETSVR
 address ipv4 192.0.2.1
 key 7 15011E1C013838212B213036
 timeout 1
</code></pre></div><h4 id="verification-8">Verification</h4>
<p>Verification is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">! Can we login with our yetiops user, and be placed into enable mode instantly?
$ ssh yetiops@10.15.30.24

----------------------------------------
|
| This banner was generated by Ansible 
|
----------------------------------------
|
| You are logged into cisco-02
| 
----------------------------------------
Password: 

cisco-02#

! What about a user that doesn&#39;t exist?
$ ssh bob@10.15.30.24

----------------------------------------
|
| This banner was generated by Ansible 
|
----------------------------------------
|
| You are logged into cisco-02
| 
----------------------------------------
Password: 
Password: 
Password: 
Connection closed by 10.15.30.24 port 22

! What do we see in our accounting log?
ng-config &lt;cr&gt;
Mar  4 08:40:22	10.100.201.253	yetiops	tty579	10.15.30.1	start	task_id=75	timezone=UTC	service=shell
Mar  4 08:41:17	10.100.201.253	yetiops	tty579	10.15.30.1	stop	task_id=75	timezone=UTC	service=shell	priv-lvl=0	cmd=exit &lt;cr&gt;
Mar  4 08:41:17	10.100.201.253	yetiops	tty579	10.15.30.1	stop	task_id=75	timezone=UTC	service=shell	disc-cause=1	disc-cause-ext=9	pre-session-time=4	elapsed_time=56	stop_time=1583311288
Mar  4 08:41:36	10.100.201.253	ansible	tty578	10.15.30.1	stop	task_id=74	timezone=UTC	service=shell	priv-lvl=15	cmd=show logging &lt;cr&gt;
Mar  4 08:41:42	10.100.201.253	ansible	tty578	10.15.30.1	stop	task_id=78	timezone=UTC	service=shell	priv-lvl=15	cmd=show logging &lt;cr&gt;

! What about if the TACACS+ server goes away?
$ sudo systemctl stop tac_plus
$ sudo systemctl status tac_plus
  tac_plus.service - LSB: TACACS+ server based on Cisco source release
   Loaded: loaded (/etc/rc.d/init.d/tac_plus; generated)
   Active: inactive (dead) since Wed 2020-03-04 03:43:42 EST; 36s ago

$ ssh ansible@10.15.30.24
----------------------------------------
|
| This banner was generated by Ansible 
|
----------------------------------------
|
| You are logged into cisco-02
| 
----------------------------------------
Password: 

cisco-02&gt;en
Password: 
cisco-02#
</code></pre></div><p>This looks good!</p>
<h3 id="parent-playbook">Parent playbook</h3>
<p>The parent playbook (i.e. the playbook that brings all the roles together) is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
- <span style="color:#66d9ef">hosts</span>: ios
  <span style="color:#66d9ef">gather_facts</span>: no
  <span style="color:#66d9ef">tasks</span>:
  - <span style="color:#66d9ef">import_role</span>:
      <span style="color:#66d9ef">name</span>: system
  - <span style="color:#66d9ef">import_role</span>:
      <span style="color:#66d9ef">name</span>: snmp
  - <span style="color:#66d9ef">import_role</span>:
      <span style="color:#66d9ef">name</span>: interfaces
  - <span style="color:#66d9ef">import_role</span>:
      <span style="color:#66d9ef">name</span>: acl
  - <span style="color:#66d9ef">import_role</span>:
      <span style="color:#66d9ef">name</span>: routing
  - <span style="color:#66d9ef">import_role</span>:
      <span style="color:#66d9ef">name</span>: nat
  - <span style="color:#66d9ef">import_role</span>:
      <span style="color:#66d9ef">name</span>: aaa
  - <span style="color:#66d9ef">name</span>: save running to startup when modified
    <span style="color:#66d9ef">ios_config</span>:
      <span style="color:#66d9ef">save_when</span>: modified
</code></pre></div><p>The last task saves the configuration (equivalent to <code>copy running-config startup-config</code>) when changes are made. Without this, changes would be lost on reboot.</p>
<p>There are times when this behaviour is desirable, as if you lose access to the device due to changes made, the ability to have the device rebooted and return to its previous configuration is incredibly useful (and will save a site visit). However most of the time saving the configuration is preferable.</p>
<p>The order of the roles is quite important in this too, which I&rsquo;ll explain below.</p>
<h4 id="role-order">Role Order</h4>
<h5 id="system"><code>system</code></h5>
<p>This goes first mainly because of it setting up logging. Having good logging while configuring the router means that you can see the effects of changes without needing to login to the device directly.</p>
<h5 id="snmp-1"><code>snmp</code></h5>
<p>SNMP has no dependency on interfaces being created, or anything else. This can be done at any time, and configuring it after the system tasks makes sense logically.</p>
<h5 id="interfaces-1"><code>interfaces</code></h5>
<p>Everything else depends on the interfaces being created, so this must happen next.</p>
<h5 id="acl"><code>acl</code></h5>
<p>Before you start routing traffic and allowing access to the outside world, you want to restrict what can access your network. Applying ACLs next therefore makes most sense.</p>
<h5 id="routing-1"><code>routing</code></h5>
<p>This needs to exist before NAT is implemented, and before AAA. NAT relies on the <em>internal</em> router knowing how to egress the network, and AAA relies on reachability to the <code>netsvr-01</code> machine.</p>
<h5 id="nat-1"><code>nat</code></h5>
<p>If this goes before routing is enabled, it will not work anyway. You could configure it prior to routing, but until routing works, the <em>internal</em> router has no way of egressing the network anyway.</p>
<h5 id="aaa-1"><code>aaa</code></h5>
<p>This must go last. It depends upon routing, and if configured incorrectly you can lose all access to your device. It can also break existing local login sessions (including the one Ansible is using to configure the device). If you enable this before this point, you could end up with a router that is half configured and unusable.</p>
<h2 id="artifacts">Artifacts</h2>
<p>The final directory structure looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">$ tree -L 2
.
├── ansible.cfg
├── ansible.log
├── group_vars
│   └── ios
├── host_vars
│   ├── cisco-01.yml
│   └── cisco-02.yml
├── inventory
├── ios.yaml
└── roles
    ├── aaa
    ├── acl
    ├── interfaces
    ├── lldp
    ├── nat
    ├── routing
    ├── snmp
    └── system
</code></pre></div><p>The final contents of our <code>group_vars</code> are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">ansible_user</span>: ansible
<span style="color:#66d9ef">ansible_connection</span>: network_cli
<span style="color:#66d9ef">ansible_network_os</span>: ios
<span style="color:#66d9ef">ansible_ssh_pass</span>: <span style="color:#75715e">###REDACTED### </span>
<span style="color:#66d9ef">ansible_become</span>: yes
<span style="color:#66d9ef">ansible_become_method</span>: enable
<span style="color:#66d9ef">ansible_become_password</span>: <span style="color:#75715e">###REDACTED### </span>
<span style="color:#66d9ef">log_host</span>: <span style="color:#ae81ff">10.100.101.254</span>
<span style="color:#66d9ef">tacacs</span>:
  <span style="color:#66d9ef">ipv4</span>: <span style="color:#ae81ff">192.0.2.1</span>
  <span style="color:#66d9ef">secret</span>: supersecret
<span style="color:#66d9ef">snmp</span>:
  <span style="color:#66d9ef">location</span>: Yeti Home
  <span style="color:#66d9ef">contact</span>: The Hairy One
  <span style="color:#66d9ef">user</span>: yetiops
  <span style="color:#66d9ef">group</span>: yetiops_group
  <span style="color:#66d9ef">auth_key</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">            $ANSIBLE_VAULT;1.1;AES256</span>
            <span style="color:#ae81ff">383</span><span style="color:#75715e">###REDACTED###############################################################566</span>
            <span style="color:#ae81ff">363</span><span style="color:#75715e">###REDACTED###############################################################366</span>
            <span style="color:#ae81ff">343</span><span style="color:#75715e">###REDACTED###############################################################565</span>
            <span style="color:#ae81ff">326</span><span style="color:#75715e">###REDACTED###############################################################362</span>
            <span style="color:#ae81ff">3431</span>
  <span style="color:#66d9ef">priv_key</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">            $ANSIBLE_VAULT;1.1;AES256</span>
            <span style="color:#ae81ff">386</span><span style="color:#75715e">###REDACTED###############################################################764</span>
            <span style="color:#ae81ff">613</span><span style="color:#75715e">###REDACTED###############################################################630</span>
            <span style="color:#ae81ff">646</span><span style="color:#75715e">###REDACTED###############################################################331</span>
            <span style="color:#ae81ff">376</span><span style="color:#75715e">###REDACTED###############################################################137</span>
            <span style="color:#ae81ff">3563</span>
</code></pre></div><p>The final contents of our <code>host_vars</code> are: -</p>
<p><strong>cisco-01.yaml</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">router_id</span>: <span style="color:#ae81ff">192.0.2.101</span>
<span style="color:#66d9ef">rtr_role</span>: edge
<span style="color:#66d9ef">bgp</span>:
  <span style="color:#66d9ef">local_as</span>: <span style="color:#ae81ff">65101</span>
  <span style="color:#66d9ef">redist</span>:
    <span style="color:#66d9ef">ospf</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">neighbors</span>:
    <span style="color:#66d9ef">ipv4</span>:
     - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">10.100.101.254</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65430</span>
       <span style="color:#66d9ef">ebgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">acl</span>:
         <span style="color:#66d9ef">acl_index</span>: <span style="color:#ae81ff">110</span>
     - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">192.0.2.201</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65101</span>
       <span style="color:#66d9ef">ibgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">update_source</span>: Loopback0
       <span style="color:#66d9ef">default_originate</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv6</span>:
     - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:101::ffff&#34;</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65430</span>
       <span style="color:#66d9ef">ebgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">acl</span>:
         <span style="color:#66d9ef">acl_index</span>: <span style="color:#ae81ff">110</span>
     - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:901:beef::2&#34;</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65101</span>
       <span style="color:#66d9ef">ibgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">update_source</span>: Loopback0
<span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;GigabitEthernet0/0&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Management&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.15.30.23/24&#34;</span>
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;GigabitEthernet0/1&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;VLAN Bridge&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">subint</span>:
      <span style="color:#66d9ef">vlans</span>:
      - <span style="color:#ae81ff">101</span>
      - <span style="color:#ae81ff">201</span>
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;GigabitEthernet0/1.101&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To netsvr&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.100.101.253/24&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:101::f/64&#34;</span>
    <span style="color:#66d9ef">acl</span>:
      <span style="color:#66d9ef">ipv4</span>:
        <span style="color:#66d9ef">in</span>: EDGE-IN
        <span style="color:#66d9ef">out</span>: EDGE-OUT
      <span style="color:#66d9ef">ipv6</span>:
        <span style="color:#66d9ef">in</span>: EDGEv6-IN
        <span style="color:#66d9ef">out</span>: EDGEv6-OUT
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;GigabitEthernet0/1.201&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To cisco-02&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.100.201.254/24&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:201::a/64&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
    <span style="color:#66d9ef">nat</span>:
      <span style="color:#66d9ef">role</span>: inside
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;GigabitEthernet0/2&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To the Internet&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;dhcp&#34;</span>
    <span style="color:#66d9ef">nat</span>:
      <span style="color:#66d9ef">role</span>: outside
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;loopback0&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;192.0.2.101/32&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:901:beef::1/128&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
</code></pre></div><p><strong>cisco-02.yaml</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">router_id</span>: <span style="color:#ae81ff">192.0.2.201</span>
<span style="color:#66d9ef">rtr_role</span>: internal
<span style="color:#66d9ef">bgp</span>:
  <span style="color:#66d9ef">local_as</span>: <span style="color:#ae81ff">65101</span>
  <span style="color:#66d9ef">neighbors</span>:
    <span style="color:#66d9ef">ipv4</span>:
     - <span style="color:#66d9ef">peer</span>: <span style="color:#ae81ff">192.0.2.101</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65101</span>
       <span style="color:#66d9ef">ibgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">update_source</span>: Loopback0
    <span style="color:#66d9ef">ipv6</span>:
     - <span style="color:#66d9ef">peer</span>: <span style="color:#e6db74">&#34;2001:db8:901:beef::1&#34;</span>
       <span style="color:#66d9ef">remote_as</span>: <span style="color:#ae81ff">65101</span>
       <span style="color:#66d9ef">ibgp</span>: <span style="color:#66d9ef">true</span>
       <span style="color:#66d9ef">update_source</span>: Loopback0
<span style="color:#66d9ef">interfaces</span>:
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;GigabitEthernet0/0&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Management&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;GigabitEthernet0/1&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;VLAN Bridge&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">subint</span>:
      <span style="color:#66d9ef">vlans</span>:
      - <span style="color:#ae81ff">201</span>
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;GigabitEthernet0/1.201&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;To cisco-01&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;10.100.201.253/24&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:201::f/64&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
  - <span style="color:#66d9ef">ios_if</span>: <span style="color:#e6db74">&#34;loopback0&#34;</span>
    <span style="color:#66d9ef">desc</span>: <span style="color:#e6db74">&#34;Loopback&#34;</span>
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ipv4</span>: <span style="color:#e6db74">&#34;192.0.2.201/32&#34;</span>
    <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:901:beef::2/128&#34;</span>
    <span style="color:#66d9ef">ospf</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
    <span style="color:#66d9ef">ospfv3</span>:
      <span style="color:#66d9ef">area</span>: <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
      <span style="color:#66d9ef">passive</span>: <span style="color:#66d9ef">true</span>
</code></pre></div><p>With the roles that have been created, and the variables above, we have achieved all of the configuration objectives set out at the beginning.</p>
<p>While it does look like a lot of configuration up front, once you have your roles created, you can configure new routers by adding some host-specific variables (<code>host_vars</code>) and an entry in your inventory.</p>
<p>If someone had told me a few years ago that I could configure a brand new router, with full OSPF and BGP peering, VLANs, IPv4 and IPv6, SNMPv3, TACACS+ authentication and remote syslog with only a 45-line YAML file, I would have been ecstatic!</p>
<h2 id="running-the-playbooks">Running the playbooks</h2>
<p>Below is an Asciinema output of my terminal when running the playbooks, so you can see what is happening as it goes past: -</p>
<script src="https://asciinema.org/a/307297.js" id="asciicast-307297" async></script>
<h3 id="why-does-it-show-so-many-changes">Why does it show so many changes?</h3>
<p>This playbook is being run over routers that have already been configured. There are quite a few places where it notes that the task has registered a change, and yet if you login to the routers they look identical to before.</p>
<p>Even though I have tried to match configuration as close as I could to the running configuration, it still registers changes.</p>
<p>There are ways to tidy this up, with the <code>changed_when</code> option (for more details, see the <a href="https://docs.ansible.com/ansible/latest/user_guide/playbooks_error_handling.html">Ansible Documentation</a>). I may look into doing this in a future post, once I have been through a few of the other vendors, and see if there are common themes of what can be changed, and what would realistically require their own Ansible modules.</p>
<h2 id="native-modules-versus-ios_config">Native modules versus <code>ios_config</code></h2>
<p>Below is a summary of how many different modules are used, and also how many in total were native modules (compared to using <code>ios_config</code>).</p>
<table>
<thead>
<tr>
<th>Module</th>
<th>Used</th>
</tr>
</thead>
<tbody>
<tr>
<td>ios_config</td>
<td>29</td>
</tr>
<tr>
<td>ios_bgp</td>
<td>6</td>
</tr>
<tr>
<td>ios_logging</td>
<td>2</td>
</tr>
<tr>
<td>ios_l3_interfaces</td>
<td>2</td>
</tr>
<tr>
<td>ios_banner</td>
<td>2</td>
</tr>
<tr>
<td>ios_interfaces</td>
<td>1</td>
</tr>
</tbody>
</table>
<p>The <code>ios_config</code> module is used far more than any other. However do remember that in the case of <code>ios_bgp</code>, <code>ios_interfaces</code> and <code>ios_l3_interfaces</code>, we are using loops to run the tasks multiple times, for lists of BGP peers and interfaces respectively.</p>
<p>Still, if you look at this compared to the modules available for Cisco <a href="https://docs.ansible.com/ansible/latest/modules/list_of_network_modules.html#nxos">NX-OS</a> or Fortinet&rsquo;s <a href="https://docs.ansible.com/ansible/latest/modules/list_of_network_modules.html#fortios">FortiOS</a>, there is definitely scope for improving this.</p>
<p>In total, we use <code>ios_config</code> 29 times, whereas we use the native modules 13 times.</p>
<p>As the Ansible for Networking ecosystem continues to grow and mature, I am sure I will be able to replace a lot of the tasks in this playbook with native Ansible modules, rather than falling back to the <code>ios_config</code> module). I will definitely revisit this post when that is the case.</p>
<h2 id="summary">Summary</h2>
<p>It has been an interesting processing using Ansible for network devices. If I was primarily a network engineer now, I would immediately be pushing for Ansible or Saltstack to manage the infrastructure.</p>
<p>You could pull your variables from external sources too. Your IP addressing, VLANs, BGP peers and basic system parameters could exist in your Infrastructure Management system (e.g. <a href="https://github.com/netbox-community/netbox">Netbox</a>. Using a system like this is recommended anyway (overlapping IP ranges are not fun!). If you&rsquo;re able to leverage a system like this to for your network estate, you may never have to configure a router manually again!</p>
<p>The next part of this series will be configuring Juniper devices, running JunOS. I hope this part has been helpful to you!</p>
]]></content>
        </item>
        
        <item>
            <title>Ansible for Networking - Part 2: The Lab environment</title>
            <link>https://yetiops.net/posts/ansible-for-networking-part-2-lab/</link>
            <pubDate>Sun, 23 Feb 2020 15:10:52 +0000</pubDate>
            
            <guid>https://yetiops.net/posts/ansible-for-networking-part-2-lab/</guid>
            <description>This is the second part in my ongoing series on using Ansible for Networking, showing how to use Ansible to configure and manage equipment from multiple networking vendors.
You can view the other posts in the series below: -
 Part 1 - Start of the series Part 3 - Cisco IOS Part 4 - Juniper JunOS Part 5 - Arista EOS Part 6 - MikroTik RouterOS Part 7 - VyOS  In the &amp;ldquo;Start of the series&amp;rdquo; post, I mentioned that the lab would consist of: -</description>
            <content type="html"><![CDATA[<p>This is the second part in my ongoing series on using Ansible for Networking, showing how to use Ansible to configure and manage equipment from multiple networking vendors.</p>
<p>You can view the other posts in the series below: -</p>
<ul>
<li><a href="/posts/ansible-for-networking-series">Part 1 - Start of the series</a></li>
<li><a href="/posts/ansible-for-networking-part-3-cisco-ios">Part 3 - Cisco IOS</a></li>
<li><a href="/posts/ansible-for-networking-part-4-juniper-junos">Part 4 - Juniper JunOS</a></li>
<li><a href="/posts/ansible-for-networking-part-5-arista-eos">Part 5 - Arista EOS</a></li>
<li><a href="/posts/ansible-for-networking-part-6-mikrotik-routeros">Part 6 - MikroTik RouterOS</a></li>
<li><a href="/posts/ansible-for-networking-part-7-vyos">Part 7 - VyOS</a></li>
</ul>
<p>In the <a href="/posts/ansible-for-networking-series">&ldquo;Start of the series&rdquo;</a> post, I mentioned that the lab would consist of: -</p>
<blockquote>
<ul>
<li>The KVM hypervisor running on Linux</li>
<li>A virtual machine, running CentOS 8, that will run: -
<ul>
<li>FRR - Acting as a route server</li>
<li>Syslog</li>
<li>Tacplus (for TACACS+ integration)</li>
</ul>
</li>
<li>Two routers/virtual machines of each vendor, one running as an “edge” router, one running as an “internal” router</li>
<li>A control machine that Ansible will run from, over a management network to all machines</li>
</ul>
</blockquote>
<p>This post goes through the Hypervisor, setting up the CentOS 8 virtual machine, and the control machine.</p>
<h2 id="the-hypervisor">The Hypervisor</h2>
<p>The Hypervisor in this scenario is KVM, running on my <a href="https://manjaro.org/">Manjaro</a>-based <a href="https://support.hp.com/gb-en/document/c03559797">laptop</a>. Rather than trying to run this on the KVM machines in my home network, using my laptop allowed me to make changes to the environment without impacting the services on the network in my house. The reason for Manjaro is simply that I like their i3wm implementation.</p>
<p>As KVM is baked into the Linux kernel, just about every distribution of Linux can support it.</p>
<h3 id="networking">Networking</h3>
<p>For networking, I run three bridge interfaces: -</p>
<ul>
<li><code>virbr0</code> - The default NAT bridge that is installed as part of KVM (which allows access out to the internet)</li>
<li><code>virbr1</code> - An isolated network (i.e. one which allows traffic between VMs) that serves as a management network (named <strong>network</strong>)</li>
<li><code>virbr2</code> - Another isolated network, that VLANs will be passed over between VMs (named <strong>vlan-bridge</strong>)</li>
</ul>
<p>Rather than creating separate bridges for each separate network/subnet in use, I decided that a common bridge with VLANs tagged across it would be far easier to manage. Also, some of the virtual machine images are limited in how many &ldquo;physical&rdquo; (i.e. virtual NICs) interfaces they can support.</p>
<h3 id="anything-else">Anything else?</h3>
<p>Other than the two extra network bridges, the KVM setup is largely default. I tend towards using <strong>virtio</strong> drivers where the virtual machine will support them (some networking OSs recommend the E1000 Intel NIC emulation instead), and all hard disk images are stored in <code>/var/lib/libvirt/images</code> (as per the default KVM setup).</p>
<h2 id="virtual-machine-configuration">Virtual Machine Configuration</h2>
<p>All the virtual machines have at least two network interfaces. Each machine has an interface connected to the Management network, and also an interface connected to the VLAN bridge. VLANs are carried across the <strong>vlan-bridge</strong> using 802.1q-based VLAN tagging. To check that your kernel supports this, run <code>modprobe 8021q</code>. If no errors are returned, you can pass VLANs without an issue.</p>
<h3 id="using-ids-for-the-lab">Using IDs for the lab</h3>
<p>To make the lab easy to work with and troubleshoot, I am using an &ldquo;ID&rdquo; for each vendor. This ID will be used to form the VLANs, IP addressing and Autonomous System. This is different from the virtual machine IDs, which are generated by the host operating system.</p>
<p>This means that when I need to look at any issues in the lab (say, not seeing certain routes), I know which virtual machine to look at.</p>
<p>The ID system looks something like the below: -</p>
<table>
<thead>
<tr>
<th>Vendor</th>
<th>ID</th>
<th>Edge VLAN</th>
<th>Internal VLAN</th>
</tr>
</thead>
<tbody>
<tr>
<td>Cisco IOS</td>
<td>01</td>
<td>101</td>
<td>201</td>
</tr>
<tr>
<td>Juniper JunOS</td>
<td>02</td>
<td>102</td>
<td>202</td>
</tr>
<tr>
<td>Arista EOS</td>
<td>03</td>
<td>103</td>
<td>203</td>
</tr>
<tr>
<td>etc</td>
<td>etc</td>
<td>etc</td>
<td>etc</td>
</tr>
</tbody>
</table>
<p>Further to this, the IP addressing and Autonomous System numbers would be: -</p>
<ul>
<li>Vendor: <strong>Cisco</strong></li>
<li>ID: <code>01</code></li>
<li>IPv4 Subnet on VLAN101: <code>10.100.101.0/24</code></li>
<li>IPv4 Subnet on VLAN201: <code>10.100.201.0/24</code></li>
<li>IPv6 Subnet on VLAN101: <code>2001:db8:101::/64</code></li>
<li>IPv6 Subnet on VLAN201: <code>2001:db8:201::/64</code></li>
<li>IPv4 Loopback Addresses: <code>192.0.2.101/32</code> and <code>192.0.2.201/32</code></li>
<li>IPv6 Loopback Address: <code>2001:db8:901:beef::1/128</code> and <code>2001:db8:901:beef::2/128</code></li>
<li>BGP Autonomous System Number: AS65101</li>
</ul>
<p>Each is explained a bit further below, but using this system does make verification and troubleshooting much easier.</p>
<h3 id="vlan-scheme">VLAN scheme</h3>
<p>The VLAN scheme is defined as follows: -</p>
<ul>
<li>1<em><strong>ID</strong></em> (e.g. 101, 102, 103) - Connectivity from the <strong>edge</strong> router to the CentOS Virtual Machine</li>
<li>2<em><strong>ID</strong></em> (e.g. 201, 202, 203) - Connectivity between the <strong>edge</strong> router and the <strong>internal</strong> router</li>
</ul>
<h3 id="ip-addressing-scheme">IP addressing scheme</h3>
<p>IPv4 addressing is defined as follows: -</p>
<ul>
<li><code>10.100.1$ID.0/24</code> (e.g. <code>10.100.101.0/24</code>, <code>10.100.102.0/24</code>) - Connectivity from the <strong>edge</strong> router to the CentOS Virtual Machine</li>
<li><code>10.100.2$ID.0/24</code> (e.g. <code>10.100.201.0/24</code>, <code>10.100.202.0/24</code>) - Connectivity between the <strong>edge</strong> router and the <strong>internal</strong> router</li>
<li><code>10.15.30.0/24</code> - The management network, each machine gets an IP in this network</li>
<li><code>192.0.2.0/24</code> - The loopback range, which will be used for Router IDs and iBGP connectivity</li>
</ul>
<p>IPv6 addressing is defined as follows: -</p>
<ul>
<li><code>2001:db8:1$ID::0/64</code> (e.g. <code>2001:db8:101::1/64</code>, <code>2001:db8:101::10/64</code>) - Connectivity from the <strong>edge</strong> router to the CentOS Virtual Machine</li>
<li><code>2001:db8:2$ID::0/64</code> (e.g. <code>2001:db8:201::1/64</code>, <code>2001:db8:201::10/64</code>) - Connectivity between the <strong>edge</strong> router and the <strong>internal</strong> router</li>
<li><code>2001:db8:9NN:beef::/64</code> - The loopback range, which will be used for Router IDs and iBGP connectivity</li>
</ul>
<p>No management range has been assigned for IPv6 in this lab.</p>
<h2 id="the-centos-8-virtual-machine">The CentOS 8 Virtual Machine</h2>
<p>In my current role, nearly all of our Linux estate runs on Debian (apart from some Amazon EC2s that run Amazon Linux). Previously, most of my professional Linux experience has been with RHEL and/or CentOS.</p>
<p>Since starting my current role, CentOS 8 has been released. I decided to use this series to familiarise myself with the changes from CentOS 7 to CentOS 8.</p>
<p>The CentOS 8 Virtual Machine, which from now on will be referred to as <strong>netsvr-01</strong> or <strong>netsvr</strong>, is (<em>almost</em>) entirely managed by Ansible. This includes the configuration for <a href="https://frrouting.org">FRR</a> (for routing), <a href="https://www.shrubbery.net/tac_plus/">tac_plus</a> (for TACACS+ integration), <a href="https://www.syslog-ng.com/products/open-source-log-management/">syslog-ng</a> (for logging purposes), as well as managing <strong>firewalld</strong> and any package dependencies.</p>
<h3 id="install-and-user-configuration">Install and user configuration</h3>
<p>Installing the operating system was done manually, rather than using something like PXE, Vagrant, or Packer. I currently do not run a PXE server at home, and I have not used Vagrant or Packer with KVM previously. This is something I&rsquo;ll look at in a future post, but for the purposes of this series, it doesn&rsquo;t add any benefits.</p>
<p>The initial user setup (i.e. adding the Ansible user) was also not automated. This is because I wanted to avoid the cyclical dependency of needing a user with sufficient privileges that Ansible would use, to allow Ansible to create users. There are ways around this (potentially using something like <code>cloud-init</code> or other methods), but for now adding the user myself was sufficient.</p>
<h3 id="tooling">Tooling</h3>
<p>Rather than templating configuration files (and making liberal usage of the Ansible <code>copy</code> task), I decided to try and make use of the native CentOS 8 tooling where possible. This includes <strong>dnf</strong> for package management, <strong>firewalld</strong> for firewall management and <strong>NetworkManager</strong> for network interface management.</p>
<p>Many of these tools also have associated Ansible modules, with excellent <a href="https://docs.ansible.com/ansible/latest/index.html">documentation</a>.</p>
<p>However in using this approach, I came across some interesting issues and caveats.</p>
<h3 id="what-caveats">What caveats?</h3>
<h4 id="ansible-and-networkmanager">Ansible and NetworkManager</h4>
<p>Ansible previously used the <strong>networkmanager-glib</strong> library for interacting with NetworkManager. However this library has been deprecated, and is not included in CentOS 8. Instead, the recommended library is <strong>networkmanager-libnm</strong>.</p>
<p>As of writing this post, Ansible (<code>v2.9.5</code>) will not interact with NetworkManager unless <strong>networkmanager-glib</strong> is installed. This dependency issue (and compatibility for <strong>networkmanager-libnm</strong>) is due to be fixed, and has been merged into the Ansible master branch, but it is currently scheduled for version <code>2.10</code>.</p>
<p>For now, all network additions and changes on the <strong>netsvr</strong> machine will be done manually using <code>nmcli</code>. This avoids spending time creating network configuration templates (in Jinja2) that will not be required soon anyway.</p>
<p>In the meantime, I have commented out the <strong>NetworkManager</strong>-specific sections of my playbooks, and will re-enable them when the support is available.</p>
<h4 id="frr-packages-and-dependencies">FRR packages and dependencies</h4>
<p>The <a href="https://github.com/FRRouting/frr/releases">latest</a> RPM packages for FRR (at the time of writing) have dependencies on libraries that are not present in CentOS 8. This is not entirely surprising, as the latest release was packaged for CentOS 7, rather than CentOS 8. As CentOS 7 is still the most common version of CentOS, and still supported, I expect this is a problem across many other applications too.</p>
<p>Where libraries and dependencies still exist in CentOS 8, I have been able to install CentOS 7 packages without issue (for example, with <strong>tac_plus</strong>). However with FRR I am relying on the version that is in the CentOS 8 repositories, which is a couple of versions behind the current one.</p>
<p>This is not a major issue, as all the features I require are in this version. If I really do need them, FRR do provide a <a href="http://docs.frrouting.org/projects/dev-guide/en/latest/building-frr-for-centos8.html">guide</a> for compiling FRR on CentOS 8.</p>
<p>I believe as time goes on, CentOS 8 (or at least RHEL8-based systems) will become the &ldquo;standard&rdquo; version to target (for all CentOS/RHEL-based RPMs/releases), and problems like this will go away.</p>
<h4 id="anything-else-1">Anything else?</h4>
<p>For the most part, I have not found any other issues in utilising CentOS 8, rather than say Debian Buster (my usual choice) or CentOS 7 (the version with &ldquo;better&rdquo; application support currently). For example, <strong>dnf</strong> is very similar to <strong>yum</strong> in everyday usage, so managing packages with Ansible doesn&rsquo;t require big changes conceptually.</p>
<h3 id="frr-configuration">FRR Configuration</h3>
<p><a href="https://frrouting.org/">FRR</a>, or Free Range Routing, is a notable fork of Quagga that provides a number of routing protocols (and other useful network protocols, like VRRP and LDP) on Linux. It also has the <code>vtysh</code> shell package, which allows you to configure, verify and monitor using very Cisco-like syntax. It can be used to turn just about any Linux device into a router, or used to allow a server to use dynamic routing.</p>
<p>In this lab, FRR is configured as a route server, and will be set up to allow peering with all the &ldquo;edge&rdquo; routers from each vendor.</p>
<p>Now please refer to the above where I said: -</p>
<blockquote>
<p>Rather than templating configuration files (and making liberal usage of the Ansible <code>copy</code> task), I decided to try and make use of the native [&hellip;] tooling where possible</p>
</blockquote>
<p>So how am I managing the FRR configuration? With templated configuration files and making use of the <code>template</code> (i.e. the <code>copy</code> task, but with templated variables) task&hellip;</p>
<h4 id="why">Why?!</h4>
<p>The Ansible module for configuring BGP in FRR (<a href="https://docs.ansible.com/ansible/latest/modules/frr_bgp_module.html">documentation here</a>) covers most common use cases. If you&rsquo;re setting up standard BGP (IPv4 or IPv6) peering, route reflectors, and all the usual configuration options (e.g. <code>route-map</code>, <code>prefix-list</code> etc), then all of these use cases are covered. Currently though, it does not support <strong>dynamic</strong> BGP neighbours.</p>
<p>Traditionally, BGP requires that you configure your peers statically, with the IP address of the specific neighbour (e.g. <code>neighbor 192.168.1.1 remote-as 65001</code>). You can use techniques like <strong>route reflection</strong> or <strong>confederation</strong> to reduce the amount of configuration required, but it still requires a known (and therefore static) set of peers to configure.</p>
<p>Recently, a number of vendors have added a feature called dynamic BGP peering. This means that one side can listen for peers, and those that meet certain requirements can form a BGP peering session with it.</p>
<p>Dynamic BGP peering originated because of the recent trend for using BGP in the data centre. Allowing peers to dynamically form means less static configuration. It also allows common configuration across multiple devices, as opposed to different peering configuration based upon where it is installed in the network. Devices can be pre-provisioned with the same configuration, and added to the network with ease, regardless of where they are physically connected.</p>
<p>To configure dynamic peers, you configure either a &ldquo;prefix&rdquo; (i.e. a subnet/range of IP addresses that peers could be coming from), an interface (e.g. <code>eth0</code>), or both, as part of a BGP peer group (essentially a set of configuration parameters common across peers).</p>
<p>If a device attempts to peer with the &ldquo;listening&rdquo; BGP process and comes from either the &ldquo;prefix&rdquo; or &ldquo;interface&rdquo; specified, then as long as they meet the other configuration parameters, a BGP peering session will be formed.</p>
<p>Admittedly in this lab, only the &ldquo;edge&rdquo; router from each vendor will peer with the <strong>netsvr</strong> machine. FRR will only ever see one device from the prefix range attempt to peer with it. However it does make it easier to add a secondary device (say, to test failover), as the FRR configuration would not require any changes.</p>
<h4 id="ansible-role">Ansible Role</h4>
<p>I am using Ansible Roles to configure FRR, with a directory structure as follows: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ tree frr
frr
├── defaults
│   └── main.yml
├── files
├── handlers
│   └── main.yml
├── meta
│   └── main.yml
├── README.md
├── tasks
│   └── main.yml
├── templates
│   └── bgpd.conf.j2
├── tests
│   ├── inventory
│   └── test.yml
└── vars
    └── main.yml
</code></pre></div><p>To create this Ansible role, I used <code>ansible-galaxy init frr</code>. This automatically creates the directory structure, as well as all the YAML files and test <code>inventory</code> file.</p>
<h5 id="tasks">Tasks</h5>
<p>The <code>tasks/main.yml</code> looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e">## tasks file for frr</span>
<span style="color:#75715e">##</span>
- <span style="color:#66d9ef">name</span>: NetworkManager libnm
  <span style="color:#66d9ef">dnf</span>:
    <span style="color:#66d9ef">name</span>: NetworkManager-libnm
    <span style="color:#66d9ef">state</span>: present

<span style="color:#75715e">#########################################</span>
<span style="color:#75715e">## Commenting out until NMCLI is fixed  #</span>
<span style="color:#75715e">#########################################</span>
<span style="color:#75715e">##- name: Create loopback interface</span>
<span style="color:#75715e">##  nmcli:</span>
<span style="color:#75715e">##    type: bridge</span>
<span style="color:#75715e">##    autoconnect: yes</span>
<span style="color:#75715e">##    conn_name: bridge-loopback</span>
<span style="color:#75715e">##    ifname: bridge-lo0</span>
<span style="color:#75715e">##    ip4: &#34;{{ loopback.ip4 }}&#34;</span>
<span style="color:#75715e">##    state: present</span>

- <span style="color:#66d9ef">name</span>: Install FRR 
  <span style="color:#66d9ef">dnf</span>:
    <span style="color:#66d9ef">name</span>: 
    - frr
    <span style="color:#66d9ef">state</span>: present

- <span style="color:#66d9ef">name</span>: Enable BGP
  <span style="color:#66d9ef">lineinfile</span>:
    <span style="color:#66d9ef">path</span>: /etc/frr/daemons
    <span style="color:#66d9ef">regexp</span>: <span style="color:#e6db74">&#39;bgpd=no&#39;</span>
    <span style="color:#66d9ef">line</span>: <span style="color:#e6db74">&#39;bgpd=yes&#39;</span>
  <span style="color:#66d9ef">register</span>: frr_bgp_daemon

- <span style="color:#66d9ef">name</span>: Enable zebra
  <span style="color:#66d9ef">lineinfile</span>:
    <span style="color:#66d9ef">path</span>: /etc/frr/daemons
    <span style="color:#66d9ef">regexp</span>: <span style="color:#e6db74">&#39;zebra=no&#39;</span>
    <span style="color:#66d9ef">line</span>: <span style="color:#e6db74">&#39;zebra=yes&#39;</span>
  <span style="color:#66d9ef">register</span>: frr_zebra_daemon

- <span style="color:#66d9ef">name</span>: BGP Config
  <span style="color:#66d9ef">template</span>:
    <span style="color:#66d9ef">src</span>: bgpd.conf.j2
    <span style="color:#66d9ef">dest</span>: /etc/frr/bgpd.conf
    <span style="color:#66d9ef">owner</span>: frr
    <span style="color:#66d9ef">group</span>: frr
  <span style="color:#66d9ef">register</span>: frr_bgp_config

- <span style="color:#66d9ef">name</span>: Allow BGP through FirewallD
  <span style="color:#66d9ef">firewalld</span>:
    <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">179</span>/tcp
    <span style="color:#66d9ef">permanent</span>: yes
    <span style="color:#66d9ef">state</span>: enabled
    <span style="color:#66d9ef">zone</span>: public

- <span style="color:#66d9ef">name</span>: Run FRR
  <span style="color:#66d9ef">service</span>:
    <span style="color:#66d9ef">name</span>: frr
    <span style="color:#66d9ef">state</span>: restarted
    <span style="color:#66d9ef">enabled</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">when</span>: frr_bgp_daemon.changed or frr_zebra_daemon.changed or frr_bgp_config.changed
</code></pre></div><p>As noted previously, until the Ansible NetworkManager module works correctly, it is commented out. To summarize what is done here: -</p>
<ul>
<li>Install <code>networkmanager-libnm</code> and <code>frr</code> (using <strong>dnf</strong>)</li>
<li>Update <code>/etc/frr/daemons</code> to enable BGP and the Zebra daemons</li>
<li>Generate the <code>/etc/frr/bgpd.conf</code> file for BGP configuration</li>
<li>Allow BGP through the firewall (using <strong>firewalld</strong>)</li>
<li>Restart FRR, <strong>if</strong> the configuration of either <code>/etc/frr/daemons</code> or <code>/etc/frr/bgpd.conf</code> has changed
<ul>
<li>In a production scenario, reloading would be preferable, but restarting on changes is fine in a lab</li>
</ul>
</li>
</ul>
<p>The <code>register</code> option creates a variable, that is updated with the status of the task. If the task has a status of <code>changed</code> (i.e. the configuration files have been updated), then <code>$variable.changed</code> (e.g. <code>frr_bgp_daemon.changed</code>) evaluates to <code>True</code>.</p>
<h5 id="template">Template</h5>
<p>The template that is used to generate the <code>bgpd.conf</code> configuration file looks like the below: -</p>
<pre><code>frr version 7.0
frr defaults traditional
!
hostname netsvr-01
!
!
!
router bgp {{ frr['asn'] }}
  bgp log-neighbor-changes
  no bgp default ipv4-unicast
{% for group in frr['bgp'] %}
 neighbor {{ group }} peer-group
 neighbor {{ group }} remote-as {{ frr['bgp'][group]['asn'] }}
{% if 'ipv4' in frr['bgp'][group]['listen_range'] %}
 bgp listen range {{ frr['bgp'][group]['listen_range']['ipv4'] }} peer-group {{ group }}
{% endif %}
{% if 'ipv6' in frr['bgp'][group]['listen_range'] %}
 bgp listen range {{ frr['bgp'][group]['listen_range']['ipv6'] }} peer-group {{ group }}
{% endif %}
{% endfor %}
 address-family ipv4 unicast
{% for group in frr['bgp'] %}
{% if 'ipv4' in frr['bgp'][group]['address_family'] %}
{% if 'unicast' in frr['bgp'][group]['address_family']['ipv4']['safi'] %}
  neighbor {{ group }} activate
{% if 'networks' in frr['bgp'][group]['address_family']['ipv4'] %}
{% for network in frr['bgp'][group]['address_family']['ipv4']['networks'] %}
  network {{ network }}
{% endfor %}
{% endif %}
{% endif %}
{% endif %}
 address-family ipv6 unicast
{% if 'ipv6' in frr['bgp'][group]['address_family'] %}
{% if 'unicast' in frr['bgp'][group]['address_family']['ipv6']['safi'] %}
  neighbor {{ group }} activate
{% if 'networks' in frr['bgp'][group]['address_family']['ipv6'] %}
{% for network in frr['bgp'][group]['address_family']['ipv6']['networks'] %}
  network {{ network }}
{% endfor %}
{% endif %}
{% endif %}
{% endif %}
{% endfor %}
!
!
line vty
!
</code></pre><p>For those who haven&rsquo;t used Jinja2 before (or Python, which Jinja2 shares some common syntax with) can look a bit opaque, so to summarise each section: -</p>
<pre><code>router bgp {{ frr['asn'] }}
  bgp log-neighbor-changes
  no bgp default ipv4-unicast
</code></pre><ul>
<li>Start BGP, using the Autonomous System number provided by the <code>frr['asn']</code> variable</li>
<li>Log any changes in neighbour states (e.g. neighbour up, neighbour down)</li>
<li>For any neighbours configured, do not automatically enable IPv4 BGP peering
<ul>
<li>You can activate it on a per peer or group basis instead</li>
</ul>
</li>
</ul>
<p>The reason for using <code>bgp default ipv4-unicast</code> is useful when you run different address families (e.g. <code>l2vpn</code> or <code>evpn</code>), and stops FRR automatically configuring a standard IPv4 BGP session to every peer (or peer-group) defined.</p>
<pre><code>{% for group in frr['bgp'] %}
 neighbor {{ group }} peer-group
 neighbor {{ group }} remote-as {{ frr['bgp'][group]['asn'] }}
</code></pre><p>For all groups specified in the <code>frr['bgp']</code> variable, create: -</p>
<ul>
<li>The <code>peer-group</code>, named <code>group</code> (which in this lab would be <code>cisco</code> or <code>juniper</code> or <code>mikrotik</code> for example)</li>
<li>Define the <code>remote-as</code> (i.e. the peer&rsquo;s autonomous system) for the group
<ul>
<li>This is derived from the <code>frr['bgp'][$THIS-SPECIFIC-GROUP]['asn']</code> variable (each group will have a different ASN)</li>
</ul>
</li>
</ul>
<pre><code>{% if 'ipv4' in frr['bgp'][group]['listen_range'] %}
 bgp listen range {{ frr['bgp'][group]['listen_range']['ipv4'] }} peer-group {{ group }}
{% endif %}
</code></pre><ul>
<li>If there is an IPv4 section in the group, create a dynamic listening range
<ul>
<li>The listening range will be an IPv4 prefix/subnet</li>
</ul>
</li>
</ul>
<pre><code>{% if 'ipv6' in frr['bgp'][group]['listen_range'] %}
 bgp listen range {{ frr['bgp'][group]['listen_range']['ipv6'] }} peer-group {{ group }}
{% endif %}
</code></pre><ul>
<li>As per the above, but for IPv6 (a listening range, but using an IPv6 prefix)</li>
</ul>
<pre><code> address-family ipv4 unicast
</code></pre><ul>
<li>Enable the IPv4 unicast address family (i.e. standard IPv4 BGP peering)</li>
</ul>
<pre><code>{% for group in frr['bgp'] %}
{% if 'ipv4' in frr['bgp'][group]['address_family'] %}
{% if 'unicast' in frr['bgp'][group]['address_family']['ipv4']['safi'] %}
  neighbor {{ group }} activate
</code></pre><p>There are three nested levels here (i.e. an <code>if</code> statement, inside an <code>if</code> statement, inside a <code>for</code> loop)</p>
<ul>
<li>For loop - for all groups in the <code>frr['bgp']</code> variable, then&hellip;</li>
<li>First if statement - If IPv4 is fined as part of the the group&rsquo;s <code>address_family</code> variable, then&hellip;</li>
<li>Second if statement - If <code>unicast</code> exists in the <code>safi</code> variable then&hellip;</li>
<li>Activate the peer group</li>
</ul>
<pre><code>{% if 'networks' in frr['bgp'][group]['address_family']['ipv4'] %}
{% for network in frr['bgp'][group]['address_family']['ipv4']['networks'] %}
  network {{ network }}
{% endfor %}
{% endif %}
{% endif %}
{% endif %}
</code></pre><p>More nested <code>if</code> statements! The above will only evaluate if the IPv4 unicast peer group is set to be activated, as otherwise any associated networks would never be advertised out.</p>
<ul>
<li>If the above is evaluated as true, then&hellip;</li>
<li>For all networks listed in the <code>networks</code> variable, create a <strong>network</strong> statement (i.e. advertising a subnet)</li>
</ul>
<pre><code> address-family ipv6 unicast
{% if 'ipv6' in frr['bgp'][group]['address_family'] %}
{% if 'unicast' in frr['bgp'][group]['address_family']['ipv6']['safi'] %}
  neighbor {{ group }} activate
{% if 'networks' in frr['bgp'][group]['address_family']['ipv6'] %}
{% for network in frr['bgp'][group]['address_family']['ipv6']['networks'] %}
  network {{ network }}
{% endfor %}
{% endif %}
{% endif %}
{% endif %}
{% endfor %}
</code></pre><p>This does the same for IPv6 as the previous statements did for IPv4.</p>
<p>If you are not familiar with Jinja2 syntax, this may look daunting. I would recommend looking at resources like <a href="https://www.redhat.com/sysadmin/ansible-jinja">this</a> and <a href="https://docs.ansible.com/ansible/latest/user_guide/playbooks_templating.html">here</a> to start with, and then soon all of the above will start to make sense.</p>
<h5 id="variables">Variables</h5>
<p>I referenced the use of multiple variables in the template above, but where do these variables come from? In this case, I am using Ansible <code>host_vars</code>, which are host specific variables. They can be fined in an INI-style format, or YAML. I prefer YAML for this, as while you have to be careful with spaces and indentation, they are grouped together in a way which makes sense to me.</p>
<p>The variables I have used for the FRR configuration are as follows: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">frr</span>:
  <span style="color:#66d9ef">asn</span>: <span style="color:#ae81ff">65430</span>
  <span style="color:#66d9ef">bgp</span>:
    <span style="color:#66d9ef">cisco</span>:
      <span style="color:#66d9ef">asn</span>: <span style="color:#ae81ff">64101</span>
      <span style="color:#66d9ef">listen_range</span>:
        <span style="color:#66d9ef">ipv4</span>: <span style="color:#ae81ff">10.100.101.0</span>/<span style="color:#ae81ff">24</span>
        <span style="color:#66d9ef">ipv6</span>: <span style="color:#e6db74">&#34;2001:db8:101::0/64&#34;</span>
      <span style="color:#66d9ef">address_family</span>:
        <span style="color:#66d9ef">ipv4</span>:
          <span style="color:#66d9ef">safi</span>: unicast
          <span style="color:#66d9ef">networks</span>:
          - <span style="color:#ae81ff">192.0.2.1</span>/<span style="color:#ae81ff">32</span>
        <span style="color:#66d9ef">ipv6</span>:
          <span style="color:#66d9ef">safi</span>: unicast
          <span style="color:#66d9ef">networks</span>:
          - <span style="color:#e6db74">&#34;2001:db8:999:beef::1/128&#34;</span> 
</code></pre></div><p>In the template above, each section of the variable (i.e. the squared-brackets) refers to the next &ldquo;level&rdquo; down in the YAML variables defined above.</p>
<p>For example, <code>frr['bgp'][group]['address_family']['ipv4']['networks']</code>, would refer to: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">frr</span>:
  <span style="color:#66d9ef">bgp</span>:
    <span style="color:#66d9ef">cisco</span>:
      <span style="color:#66d9ef">address_family</span>:
        <span style="color:#66d9ef">ipv4</span>:
          <span style="color:#66d9ef">networks</span>:
          - <span style="color:#ae81ff">192.0.2.1</span>/<span style="color:#ae81ff">32</span>
</code></pre></div><p>The reason for <code>group</code> not having single quotation marks is because it derived a <strong>for</strong> loop, rather than being a hardcoded string. This allows you to loop through each group, rather than having to add sections of the template that are specific to each vendor/group.</p>
<p>When the BGP configuration template is generated, using the variables provided above, the output looks like so: -</p>
<pre><code>router bgp 65430
  bgp log-neighbor-changes
  no bgp default ipv4-unicast
 neighbor cisco peer-group
 neighbor cisco remote-as 64101
 bgp listen range 10.100.101.0/24 peer-group cisco
 bgp listen range 2001:db8:101::0/64 peer-group cisco
 address-family ipv4 unicast
  neighbor cisco activate
  network 192.0.2.1/32
 address-family ipv6 unicast
  neighbor cisco activate
  network 2001:db8:999:beef::1/128
</code></pre><p>The indentation could be tidied up, but the above is a fully functioning FRR BGP configuration, as seen below: -</p>
<pre><code>netsvr-01# show running-config 
Building configuration...

Current configuration:
!
frr version 7.0
frr defaults traditional
hostname netsvr-01
no ip forwarding
no ipv6 forwarding
!
router bgp 65430
 bgp log-neighbor-changes
 no bgp default ipv4-unicast
 neighbor cisco peer-group
 neighbor cisco remote-as 64101
 bgp listen range 10.100.101.0/24 peer-group cisco
 bgp listen range 2001:db8:101::/64 peer-group cisco
 !
 address-family ipv4 unicast
  network 192.0.2.1/32
  neighbor cisco activate
 exit-address-family
 !
 address-family ipv6 unicast
  network 2001:db8:999:beef::1/128
  neighbor cisco activate
 exit-address-family
!
line vty

netsvr-01# show bgp ipv4 summary
IPv4 Unicast Summary:
BGP router identifier 192.168.122.81, local AS number 65430 vrf-id 0
BGP table version 1
RIB entries 1, using 160 bytes of memory
Peers 1, using 21 KiB of memory
Peer groups 1, using 64 bytes of memory

Neighbor        V         AS MsgRcvd MsgSent   TblVer  InQ OutQ  Up/Down State/PfxRcd
*10.100.101.253 4      65101       4       4        0    0    0 00:01:03            0

Total number of neighbors 1
* - dynamic neighbor
1 dynamic neighbor(s), limit 100
</code></pre><h3 id="syslog-ng-configuration">syslog-ng Configuration</h3>
<p><code>syslog-ng</code> is a syslog daemon, that in this scenario will be used for storing logs from each network device. This means you can look at logs from across the network in one place, rather than retrieving them from each device manually.</p>
<h4 id="ansible-role-1">Ansible role</h4>
<p>The role is created with <code>ansible-galaxy init syslog</code>. The directory structure is as follows: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ tree syslog
syslog
├── defaults
│   └── main.yml
├── files
│   └── syslog-remote.conf
├── handlers
│   └── main.yml
├── meta
│   └── main.yml
├── README.md
├── tasks
│   └── main.yml
├── templates
├── tests
│   ├── inventory
│   └── test.yml
└── vars
    └── main.yml
</code></pre></div><h4 id="tasks-1">Tasks</h4>
<p>The <code>tasks/main.yml</code> file looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e">## tasks file for syslog</span>
- <span style="color:#66d9ef">name</span>: Remove rsyslog
  <span style="color:#66d9ef">dnf</span>:
    <span style="color:#66d9ef">name</span>:
      - rsyslog
    <span style="color:#66d9ef">state</span>: absent

- <span style="color:#66d9ef">name</span>: Install syslog-ng
  <span style="color:#66d9ef">dnf</span>:
    <span style="color:#66d9ef">name</span>:
      - syslog-ng
    <span style="color:#66d9ef">state</span>: present

- <span style="color:#66d9ef">name</span>: Remote Syslog
  <span style="color:#66d9ef">copy</span>:
    <span style="color:#66d9ef">src</span>: syslog-remote.conf
    <span style="color:#66d9ef">dest</span>: /etc/syslog-ng/conf.d/syslog-remote.conf
  <span style="color:#66d9ef">register</span>: syslog_conf

- <span style="color:#66d9ef">name</span>: Remote Syslog directory
  <span style="color:#66d9ef">file</span>:
    <span style="color:#66d9ef">state</span>: directory
    <span style="color:#66d9ef">path</span>: /var/log/remote
    <span style="color:#66d9ef">owner</span>: root
    <span style="color:#66d9ef">group</span>: root
    <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0755</span>

- <span style="color:#66d9ef">name</span>: Reload syslog-ng
  <span style="color:#66d9ef">service</span>:
    <span style="color:#66d9ef">name</span>: syslog-ng
    <span style="color:#66d9ef">state</span>: restarted
    <span style="color:#66d9ef">enabled</span>: yes
  <span style="color:#66d9ef">when</span>: syslog_conf.changed

- <span style="color:#66d9ef">name</span>: Allow syslog through FirewallD
  <span style="color:#66d9ef">firewalld</span>:
    <span style="color:#66d9ef">service</span>: syslog
    <span style="color:#66d9ef">permanent</span>: yes
    <span style="color:#66d9ef">state</span>: enabled
    <span style="color:#66d9ef">zone</span>: public
</code></pre></div><p>Steps: -</p>
<ul>
<li>Remove <code>rsyslog</code> with <strong>dnf</strong> (as it conflicts with <code>syslog-ng</code>)</li>
<li>Install <code>syslog-ng</code> with <strong>dnf</strong></li>
<li>Add the <code>syslog-remote.conf</code> file</li>
<li>Add the <code>/var/log/remote</code> directory to store logs from the network devices</li>
<li>Reload <code>syslog-ng</code>, only if the configuration has changed</li>
<li>Allow <code>syslog-ng</code> through the firewall with <strong>firewalld</strong></li>
</ul>
<p>We&rsquo;re not using any templating or providing any extra variables, because the configuration required is static.</p>
<h4 id="syslog-ng-remote-configuration">Syslog-ng remote configuration</h4>
<p>The configuration required to enable remote logging within <code>syslog-ng</code> looks like the below: -</p>
<pre><code>source net { udp(); };
destination remote { file(&quot;/var/log/remote/${FULLHOST}&quot; template(&quot;${ISODATE} ${HOST}: ${MSGHDR}${MESSAGE}\n&quot;) ); };
log { source(net); destination(remote); };
</code></pre><p>The files will get created as <code>$HOSTNAME</code> or <code>$IP</code> in <code>/var/log/remote</code>, in the format of <code>ISODATE HOSTNAME: %SYSLOG-PROGRAM Syslog message</code>.</p>
<p>An example of the output can be seen below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ pwd
/var/log/remote

$ ls
10.100.101.253

$ cat 10.100.101.253
2020-02-23T14:23:21-05:00 10.100.101.253: %CRYPTO-6-ISAKMP_ON_OFF: ISAKMP is OFF
2020-02-23T14:23:21-05:00 10.100.101.253: %CRYPTO-6-GDOI_ON_OFF: GDOI is OFF
2020-02-23T14:23:21-05:00 10.100.101.253: %SYS-6-LOGGINGHOST_STARTSTOP: Logging to host 10.100.101.254 port <span style="color:#ae81ff">514</span> started - CLI initiated
2020-02-23T14:23:37-05:00 10.100.101.253: %BGP-5-ADJCHANGE: neighbor 10.100.101.254 Up 
</code></pre></div><p>You could then find out if for example, multiple BGP peers had dropped by doing <code>grep -i bgp /var/log/remote/* | grep -i down</code>. This would return all the files (which are named based upon the devices) that contain BGP drops.</p>
<p>With tools like the <a href="https://www.elastic.co/">Elastic</a> stack, <a href="https://www.graylog.org/">Graylog</a> or <a href="https://www.splunk.com/">Splunk</a>, its now possible to index logs (making them quicker to search based upon the type of queries used), create dashboards and alerts based upon them, and much more. Still, running <code>syslog-ng</code> (or other syslog daemons) can still help you gather huge insights into where you may be having issues in your network.</p>
<h3 id="tac_plus-configuration">tac_plus Configuration</h3>
<p><code>tac_plus</code> is a daemon that can be used for <a href="https://en.wikipedia.org/wiki/TACACS">TACACS+</a>-based authentication and authorization (as an alternative to RADIUS). This allows you to manage your users centrally on a server (such as this one) so that you can login to any device in the network with your username and password. It can also assign privileges to the user, based upon &ldquo;privilege&rdquo; levels.</p>
<p>The &ldquo;privilege&rdquo; levels are configured in groups, which users can added to.</p>
<h4 id="ansible-role-2">Ansible role</h4>
<p>The role is created with <code>ansible-galaxy init syslog</code>. The directory structure is as follows: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ tree tacplus
tacplus
├── defaults
│   └── main.yml
├── files
├── handlers
│   └── main.yml
├── meta
│   └── main.yml
├── README.md
├── tasks
│   └── main.yml
├── templates
│   └── tac_plus.conf.j2
├── tests
│   ├── inventory
│   └── test.yml
└── vars
    └── main.yml
</code></pre></div><h4 id="tasks-2">Tasks</h4>
<p>The <code>tasks/main.yml</code> file looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e">## tasks file for tacplus</span>
- <span style="color:#66d9ef">name</span>: Nux Repo
  <span style="color:#66d9ef">yum_repository</span>:
    <span style="color:#66d9ef">name</span>: nux-misc
    <span style="color:#66d9ef">description</span>: nux-misc
    <span style="color:#66d9ef">baseurl</span>: http://li.nux.ro/download/nux/misc/el7/x86_64/
    <span style="color:#66d9ef">enabled</span>: <span style="color:#ae81ff">0</span>
    <span style="color:#66d9ef">gpgcheck</span>: <span style="color:#ae81ff">1</span>
    <span style="color:#66d9ef">gpgkey</span>: http://li.nux.ro/download/nux/RPM-GPG-KEY-nux.ro

- <span style="color:#66d9ef">name</span>: Install tcp-wrappers (not in CentOS <span style="color:#ae81ff">8</span>)
  <span style="color:#66d9ef">dnf</span>:
    <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#39;http://mirror.centos.org/centos/7/os/x86_64/Packages/tcp_wrappers-libs-7.6-77.el7.x86_64.rpm&#39;</span>
    <span style="color:#66d9ef">state</span>: present

- <span style="color:#66d9ef">name</span>: Install tac_plus
  <span style="color:#66d9ef">dnf</span>:
    <span style="color:#66d9ef">name</span>: tac_plus
    <span style="color:#66d9ef">enablerepo</span>: nux-misc
    <span style="color:#66d9ef">state</span>: present

- <span style="color:#66d9ef">name</span>: Generate configuration
  <span style="color:#66d9ef">template</span>:
    <span style="color:#66d9ef">src</span>: tac_plus.conf.j2
    <span style="color:#66d9ef">dest</span>: /etc/tac_plus.conf
  <span style="color:#66d9ef">register</span>: tac_conf

- <span style="color:#66d9ef">name</span>: Restart tac_plus
  <span style="color:#66d9ef">service</span>:
    <span style="color:#66d9ef">name</span>: tac_plus
    <span style="color:#66d9ef">state</span>: restart
    <span style="color:#66d9ef">enabled</span>: yes
  <span style="color:#66d9ef">when</span>: tac_conf.changed

- <span style="color:#66d9ef">name</span>: Allow tacacs through FirewallD
  <span style="color:#66d9ef">firewalld</span>:
    <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">49</span>/tcp
    <span style="color:#66d9ef">permanent</span>: yes
    <span style="color:#66d9ef">state</span>: enabled
    <span style="color:#66d9ef">zone</span>: public
</code></pre></div><p>Steps: -</p>
<ul>
<li>Add the <code>nux-misc</code> repository to <strong>yum</strong> (which <strong>dnf</strong> makes use of)
<ul>
<li>Disabled by default, only used when it is specifically called for</li>
</ul>
</li>
<li>Install <strong>tcp-wrappers</strong> (deprecated in CentOS 8), a <strong>tac_plus</strong> dependency, directly from an RPM file</li>
<li>Install <strong>tac_plus</strong> using <strong>dnf</strong>, enabling the <strong>nux-misc</strong> repository to do so</li>
<li>Generate the <strong>tac_plus</strong> configuration</li>
<li>Restart <strong>tac_plus</strong>, if the configuration is changed</li>
<li>Allow the <strong>tacacs</strong> port through the firewall, using <strong>firewalld</strong></li>
</ul>
<h4 id="configuration-template">Configuration template</h4>
<p>The <code>tac_plus.conf.j2</code> configuration template looks like the below: -</p>
<pre><code>## Created by Henry-Nicolas Tourneur(henry.nicolas@tourneur.be)
## See man(5) tac_plus.conf for more details

## Define where to log accounting data, this is the default.

accounting file = /var/log/tac_plus.acct

## This is the key that clients have to use to access Tacacs+

key = {{ tacacs_key }} 

group = netwrite {
    default service = permit
    service = exec {
        priv-lvl = 15
    }
}

{% for user in netusers %}
user = {{ user }} {
    member = netwrite
    login = des {{ netusers[user]['tacpwd'] }}
}
{% endfor %}
</code></pre><p>Compared to the FRR <code>bgpd.conf.j2</code> file, there are far fewer parts to generate. We supply the <code>tacacs_key</code>, which is used to encrypt the messages between the network device and the <code>tac_plus</code> server. We also supply a list of users, with passwords, to generate this file.</p>
<p>The <strong>netwrite</strong> group has <strong>priv-lvl 15</strong>, which is analogous to full admin access on each network device. It is possible to create groups with read-only permissions, or with additional permissions. You could create a group so that certain <strong>write</strong> commands are allowed (for example, reloading a BGP neighbour or clearing statistics on an interface), but all other commands are restricted.</p>
<p>The actual list of users is defined in <code>host_vars</code>.</p>
<h4 id="host-variables">Host Variables</h4>
<p>The <code>host_vars</code> specific to <code>tac_plus</code> are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">tacacs_key</span>: supersecret
<span style="color:#66d9ef">netusers</span>:
  <span style="color:#66d9ef">yetiops</span>:
    <span style="color:#66d9ef">tacpwd</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">          $ANSIBLE_VAULT;1.1;AES256</span>
          <span style="color:#ae81ff">66336131323637326166316232623161663630373739613137366266633937306662323363333039</span>
          xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxREDACTEDxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
          xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxREDACTEDxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
          xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxREDACTEDxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
          <span style="color:#ae81ff">6165</span>
  <span style="color:#66d9ef">davethechicken</span>:
    <span style="color:#66d9ef">tacpwd</span>: !vault <span style="color:#e6db74">|
</span><span style="color:#e6db74">          $ANSIBLE_VAULT;1.1;AES256</span>
          <span style="color:#ae81ff">19784782343345848123148123094812389452340958230495809234846666642381109434123412</span>
          xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxREDACTEDxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
          xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxREDACTEDxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
          xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxREDACTEDxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
          <span style="color:#ae81ff">6162</span>
</code></pre></div><p>The passwords that <code>tac_plus</code> expects are DES-hashed (yes, not even 3DES!). The easiest way to generate them is by using <code>tac_pwd</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ tac_pwd 
Password to be encrypted: test
CCVwN31H4K74A
</code></pre></div><p>The user passwords are then encrypted in this file using <a href="https://docs.ansible.com/ansible/latest/user_guide/vault.html">Ansible Vault</a>, which allows you to store sensitive data in version control, as they require an encryption key to unlock.</p>
<p>To encrypt a string, you would use the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ansible-vault encrypt_string <span style="color:#e6db74">&#39;s3cr3tp8ss&#39;</span> --name <span style="color:#e6db74">&#39;pass&#39;</span>
New Vault password: 
Confirm New Vault password: 
pass: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          <span style="color:#ae81ff">64373235663534646635306363626365376537343137393136623863626332303235386264393237</span>
          3435313266336633633430646462393138353331633734340a356265336366663030313338393965
          <span style="color:#ae81ff">31643738383461616465626435376265333739663031366636353865373938663236653262396366</span>
          3833346263653436380a333936633363303038646333613832313564316566313534373537396433
          <span style="color:#ae81ff">3366</span>
Encryption successful
</code></pre></div><p>You can then copy and paste this secret into your <code>host_vars</code>.</p>
<p>You can store your encryption keys in local files (and reference them with <code>ansible-playbook --vault-password-file /path/to/vault-keys</code>), so that Ansible does not need to ask for them when you run your playbooks.</p>
<p>Alternatively, you can make Ansible ask you for the encryption key, meaning you can then store the encryption key in whatever password management system you choose. To do this, see below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ansible-playbook centos.yaml --ask-vault-pass
Vault password: 

PLAY <span style="color:#f92672">[</span>centos<span style="color:#f92672">]</span> *************************************************

TASK <span style="color:#f92672">[</span>Gathering Facts<span style="color:#f92672">]</span> ****************************************
ok: <span style="color:#f92672">[</span>10.15.30.252<span style="color:#f92672">]</span>

<span style="color:#f92672">[</span>...<span style="color:#f92672">]</span>
</code></pre></div><p>Without this, your Playbook run will fail, as it will not be able to decrypt your keys.</p>
<h4 id="generated-configuration-file">Generated configuration file</h4>
<p>The configuration file for <code>tac_plus</code> when generated from the template looks like the below: -</p>
<pre><code>## Created by Henry-Nicolas Tourneur(henry.nicolas@tourneur.be)
## See man(5) tac_plus.conf for more details

## Define where to log accounting data, this is the default.

accounting file = /var/log/tac_plus.acct

## This is the key that clients have to use to access Tacacs+

key = supersecret 

group = netwrite {
    default service = permit
    service = exec {
        priv-lvl = 15
    }
}

user = yetiops {
    member = netwrite
    login = des ###REDACTED### 
}

user = davethechicken {
    member = netwrite
    login = des ###REDACTED###
}
</code></pre><p>With this, you can then configure TACACS+-based authentication on your network, and then login to your network devices with the users defined in this file.</p>
<h3 id="top-level-playbook">Top-level playbook</h3>
<p>The playbook that includes all of the roles, as well as defining what hosts it will run on, is in the directory level below the roles: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ tree -L <span style="color:#ae81ff">1</span>
.
├── ansible.cfg
├── ansible.log
├── centos.yaml
├── epel           &lt;- Role directory
├── frr            &lt;- Role directory
├── host_vars
├── inventory
├── README.md
├── syslog         &lt;- Role directory
└── tacplus        &lt;- Role directory
<span style="color:#ae81ff">5</span> directories, <span style="color:#ae81ff">5</span> files
</code></pre></div><p>The contents of the <code>centos.yaml</code> playbook are: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">hosts</span>: centos
  <span style="color:#66d9ef">become</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#66d9ef">become_method</span>: sudo
  <span style="color:#66d9ef">tasks</span>:
  - <span style="color:#66d9ef">import_role</span>:
      <span style="color:#66d9ef">name</span>: epel
  - <span style="color:#66d9ef">import_role</span>:
      <span style="color:#66d9ef">name</span>: frr
  - <span style="color:#66d9ef">import_role</span>:
      <span style="color:#66d9ef">name</span>: syslog
  - <span style="color:#66d9ef">import_role</span>:
      <span style="color:#66d9ef">name</span>: tacplus
</code></pre></div><p>There is an additional role here (<code>epel</code>), but all this does is install the <code>epel</code> release package (<strong>E</strong>xtra <strong>P</strong>ackages for <strong>E</strong>nterprise <strong>L</strong>inux).</p>
<p>When you run this playbook, each role will be imported and ran in order (so <code>epel</code>, <code>frr</code>, <code>syslog</code>, then <code>tacplus</code>). It will also, by default, pick up the <code>host_vars/$IP_ADDRESS.yaml</code> file for host-specific variables (ensure that <code>$IP_ADDRESS</code> is replaced with the IP or hostname you have defined in your Ansible inventory).</p>
<h3 id="other-files">Other files</h3>
<p>I also have a few settings in the <code>ansible.cfg</code> file: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#66d9ef">[defaults]</span>
<span style="color:#a6e22e">inventory</span>      <span style="color:#f92672">=</span> <span style="color:#e6db74">./inventory</span>
<span style="color:#a6e22e">timeout</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">5</span>
<span style="color:#a6e22e">log_path</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">./ansible.log</span>
</code></pre></div><p>The above specifies my inventory file as <code>./inventory</code>, adds a timeout (more useful for network devices, but I&rsquo;m keeping it for consistency) and also creates a log file of every Playbook run. This makes it easier to debug, or go back and look at where changes were made that potentially broke the playbook runs.</p>
<h2 id="ansible-control-machine">Ansible control machine</h2>
<p>As Ansible can run from just about anywhere, the choice of how you invoke your playbooks is down to personal preference.</p>
<p>In a production scenario, you would usually have either a machine (or machines) that have access to your devices, and run your playbooks from there. This means that a team of people can make changes and run them from the same place (rather than playbooks going out of sync on people&rsquo;s workstations). Alternatively, you can run something like <a href="https://www.ansible.com/products/tower">Ansible Tower</a> or <a href="https://www.ansible.com/products/awx-project">AWX</a> (the upstream project that Ansible Tower builds upon) to manage your infrastructure.</p>
<p>In this scenario, as it is a lab environment, I am running all of the playbooks from the same laptop that is running the lab. I use passwordless SSH where it is supported (not every network vendor does support this), and I maintain all my playbooks in a Git repository (that I will make public during the series).</p>
<h2 id="summary">Summary</h2>
<p>My approach to setting up the lab environment has been to make use of the native tools available (either on my laptop, or the virtual machines themselves), while also trying to keep things as simple as possible. Thanks to taking this approach, and because it is all managed using Ansible Playbooks, I can easily recreate this setup on other machines.</p>
<p>The next few posts in the series will get into managing actual network devices themselves. I also have a few bonus posts to make during this series, thanks to the generosity and help from the readers of this site.</p>
<p>Hopefully this will help get you up and running with your own lab, so you can test these kind of scenarios yourself!</p>
]]></content>
        </item>
        
        <item>
            <title>Ansible for Networking - Part 1: The start of the series</title>
            <link>https://yetiops.net/posts/ansible-for-networking-series/</link>
            <pubDate>Sun, 16 Feb 2020 14:36:04 +0000</pubDate>
            
            <guid>https://yetiops.net/posts/ansible-for-networking-series/</guid>
            <description>For those who have been reading my posts for a while, they’ll know that while currently I’m a DevOps Engineer, I spent the previous decade managing and configuring service provider networks. For the majority of that time, the network was configured by hand. The closest most people in the industry had to an automation toolset was either using a spreadsheet with variables, their own scripts they had created, or delegating the task to multiple junior engineers.</description>
            <content type="html"><![CDATA[<p>For those who have been reading my posts for a while, they’ll know that while currently I’m a DevOps Engineer, I spent the previous decade managing and configuring service provider networks. For the majority of that time, the network was configured by hand. The closest most people in the industry had to an automation toolset was either using a spreadsheet with variables, their own scripts they had created, or delegating the task to multiple junior engineers.</p>
<p>However in recent years, tools like Ansible and Salt have made it so that you no longer need to learn Python, Golang, Perl or other languages to reap the benefits of automation.</p>
<p>With that in mind, I am putting together a series of posts on using Ansible to manage a number of different vendor’s networking appliances. This is the first time I have put together a true series of posts, but I am hoping to do more series on other topics in the future.</p>
<h2 id="what-vendors-will-be-covered">What vendors will be covered?</h2>
<p>I have chosen a number of different vendors to cover in this series, all for different reasons: -</p>
<h3 id="cisco-ios">Cisco IOS</h3>
<p>Cisco is still a heavyweight in the network industry, and I have spent the majority of my career managing Cisco-based networks. Therefore IOS seems an obvious choice. I’ll also try and cover Cisco IOS-XE (which is mostly a continuation of the same/similar syntax, but with better process isolation and a more modern architecture)</p>
<h3 id="juniper-junos">Juniper JunOS</h3>
<p>I currently work for a company who run an entirely Juniper-based network (save for a couple of Cisco-based Out-of-Band management switches). Juniper are also a very popular option in the service provider sector.</p>
<h3 id="arista-eos">Arista EOS</h3>
<p>Arista are very popular in the data centre space, and also they run on Linux. Automation in the data centre can allow rapid turn-up of services, bringing the time to deploy a service down from weeks and months, to hours and minutes.</p>
<h3 id="cumulus">Cumulus</h3>
<p>Cumulus provide a network “distribution” that runs primarily on white box switching (i.e. hardware you can run your own operating system on, rather than being tied to a vendor). It runs on a variant of Debian.</p>
<p>Cumulus have contributed heavily to open source networking already, with significant changes to Quagga/FRR (including the ability to use unnumbered BGP neighbours, a topic I will cover later), and also provide the Cumulus VX virtual machine to train and test with.</p>
<h3 id="extreme-exos">Extreme EXOS</h3>
<p>Extreme, who now own most of the Brocade (and therefore ex-Foundry) service provider portfolio. They also have their own operating system called EXOS. A lot of Extreme, Brocade and Foundry kit has formed the basis of many of the major peering LANs. LINX (London InterNet Exchange) ran one of their main peering LANs in London on Extreme switching until very recently, and their current Juniper LAN used to be based upon Foundry gear.</p>
<p>Beyond this though, I have little familiarity with Extreme gear, so this will be as much about learning EXOS for me, as managing it with Ansible too.</p>
<h3 id="mikrotik-routeros">MikroTik RouterOS</h3>
<p>MikroTik provide a lot of features at a very attractive price, and when you find the right use case for them, they can keep up with equipment that costs significantly more (sometimes at a tenth of the price).</p>
<p>A significant part of my career involved managing MikroTik devices, so they are an obvious choice to include in this series from my perspective.</p>
<h3 id="vyos">VyOS</h3>
<p>The fork of Vyatta, VyOS is open source, and easy to get hold of. I have used VyOS in previous posts as generic routers, and they provide a good option for virtualised routers too.</p>
<h3 id="pfsense-and-opnsense">pfSense and OPNsense</h3>
<p>I am not as familiar with pfSense as I am with Cisco ASAs, Juniper SRXs or Check Point firewalls, but they are a very popular option in the industry, and it will give me an opportunity to learn the inner workings of pfSense.</p>
<p>Ignoring the pfSense/OPNsense controversy, for sake of completeness I also want to include OPNsense, to see if those migrating from one to the other will be able to use the same tooling to manage them.</p>
<h3 id="other-vendors">Other vendors</h3>
<p>The above is non-exhaustive, and I am more than happy to look into other vendors if I get enough feedback on them. The following are ones I have considered, but with caveats as to whether I will include them: -</p>
<h4 id="hparuba-procurve">HP/Aruba Procurve</h4>
<p>I do not currently have access to any of the hardware, so would need to source them from somewhere for this. If anyone has one I could get access to, either physically or remotely, I am more than happy to include them. If this series proves popular enough, I may look to source some anyway.</p>
<h4 id="hpeh3c-comware">HPE/H3C Comware</h4>
<p>Unfortunately, Comware does not seem to be a popular option any longer. I still have access to HP’s VSR1000 image if people would like me to include them, but (at least from the HPE perspective) they appear to be getting phased out.</p>
<p>This would also probably cover Huawei’s data centre and service provider line, as there are a number of similarities to Comware.</p>
<p>Again, if there is enough call for this, I am more than happy to include them.</p>
<h4 id="nokia-ex-alcatel">Nokia (ex-Alcatel)</h4>
<p>I am considering including Nokia, but this is more down to time constraints. While I have previously briefly played with some Nokia (then Alcatel) kit, their approach to services and customers within the configuration would take quite some time to refamiliarise myself with.</p>
<h4 id="cisco-ios-xr-and-nx-os">Cisco IOS-XR and NX-OS</h4>
<p>For those who use IOS and IOS-XE, the transition to either IOS-XR or NX-OS is not the largest. Therefore many of the techniques used to manage IOS would be applicable to either. If there is enough demand, I will try to include them.</p>
<h4 id="can-i-include-these">Can I include these?</h4>
<p>If time permits, I will try and include the above (and maybe others, like Check Point, Fortinet, and any other vendor that provides a virtualised/training version of their operating system to use).</p>
<h2 id="labbing-environment">Labbing environment</h2>
<p>The labbing environment I am going to use for this will be: -</p>
<ul>
<li>The KVM hypervisor running on Linux</li>
<li>A virtual machine, running CentOS 8, that will run: -
<ul>
<li>FRR - Acting as a route server</li>
<li>Syslog</li>
<li>Tacplus (for TACACS+ integration)</li>
</ul>
</li>
<li>Two routers/virtual machines of each vendor, one running as an “edge” router, one running as an “internal” router</li>
<li>A control machine that Ansible will run from, over a management network to all machines</li>
</ul>
<h3 id="prerequisites">Prerequisites</h3>
<p>For each vendor, there will be some prerequisites required to be able to manage the devices via Ansible. This will likely be creating an Ansible user, with the correct access level, and allowing SSH.</p>
<p>Where possible, I will also use public SSH keys, so that passwords do not need to be stored within Ansible configuration. If it is not possible, I will use Ansible Vault to store the passwords in an encrypted manner.</p>
<h3 id="configuration-objectives">Configuration objectives</h3>
<p>For each vendor, I am setting myself the challenge of configuring the following, all using Ansible.</p>
<h4 id="bgp">BGP</h4>
<p>Each vendor “network” (i.e. the two routers) will run in their own Autonomous System. The “edge” router will run a BGP peering session to FRR running on the CentOS 8 virtual machine. The “internal” router will receive networks from the “edge” router via iBGP.</p>
<h4 id="ospf">OSPF</h4>
<p>OSPF will run between the two routers, to exchange loopback addresses (for BGP to run over). Authentication will be ran between the peers.</p>
<h4 id="ipv6">IPv6</h4>
<p>IPv6 will be used as well as IPv4. This includes running BGP v6 peering sessions, OSPFv3 for IPv6 routing, and IPv6 addressing will exist throughout.</p>
<h4 id="firewall-filtering">Firewall Filtering</h4>
<p>Filtering and/or access-lists will be used to limit the “edge” router to only allow BGP between the “edge” router and the CentOS 8 VM, and will allow ICMP/pings and syslog from the loopback IPs of each router.</p>
<h4 id="nat-to-the-internet">NAT to the internet</h4>
<p>NAT will be used on the “edge” router to provide basic ICMP/ping access to the internet.</p>
<h4 id="management-logging-and-authentication">Management, logging and authentication</h4>
<p>SNMPv3 will be configured, to allow monitoring from a Network Monitoring System. Syslog will be configured to forward all logs to the CentOS 8 virtual machine. TACACS+ will be configured, to allow authentication and authorization of remote user login sessions to the routers.</p>
<h2 id="anything-else">Anything else?</h2>
<p>While I won’t be configuring all the features everyone will use on the different vendor equipment, it will hopefully provide enough information on how to use Ansible to manage your network. Once you know how to use Ansible to manage your network, you can then customize the approach to what features you need to use.</p>
<p>I also believe that showing it across multiple vendors (and how similar some aspects are) will make it easier for people to move from one vendor to another, without worrying about needing to relearn syntax or tools for managing their networking estate.</p>
<p>I intend to have the first post out very soon, so keep an eye out!</p>
]]></content>
        </item>
        
        <item>
            <title>Building Windows AWS AMIs using Packer and Ansible</title>
            <link>https://yetiops.net/posts/packer-ansible-windows-aws/</link>
            <pubDate>Tue, 04 Feb 2020 12:24:43 +0000</pubDate>
            
            <guid>https://yetiops.net/posts/packer-ansible-windows-aws/</guid>
            <description>Like many other companies that are deploying their applications to the cloud, the majority of our estate uses Linux. However we do need to use Windows for a couple of purposes. This could be for application testing, or for specific Windows features.
We also recently adopted Packer to build our machine images, to allow them to be defined in code (and therefore within version control). In Amazon, these machine images are called AMIs.</description>
            <content type="html"><![CDATA[<p>Like many other companies that are deploying their applications to the cloud, the majority of our estate uses Linux. However we do need to use Windows for a couple of purposes. This could be for application testing, or for specific Windows features.</p>
<p>We also recently adopted Packer to build our machine images, to allow them to be defined in code (and therefore within version control). In Amazon, these machine images are called AMIs. Think of them like &ldquo;golden images&rdquo;, a known base to run your applications on top of.</p>
<p>Packer itself can bootstrap machines using shell scripts, single commands, or you can make use of a configuration management tool to install the base utilities and necessary files to prepare the image. We currently use Ansible do do this.</p>
<h2 id="basics-of-packer">Basics of Packer</h2>
<p>Packer can build images for a number of different providers, and this doesn&rsquo;t just include the likes of AWS, Azure or GCP. You could use it for creating Docker images, to help creating Vagrant images, VMware, Proxmox, as well as smaller providers like Digital Ocean, Hetzner and a number of others. For the full list, take a look at the <a href="https://packer.io/docs/builders/">builders</a> page in the Packer documentation.</p>
<p>Packer defines the characteristics of a machine image using JSON. The most basic of images could look something like: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;builders&#34;</span>: [
   {
    <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;amazon-ebs&#34;</span>,
    <span style="color:#f92672">&#34;region&#34;</span>: <span style="color:#e6db74">&#34;{{ user `aws_region`}}&#34;</span>,
    <span style="color:#f92672">&#34;profile&#34;</span>: <span style="color:#e6db74">&#34;{{ user `aws_profile`}}&#34;</span>,
    <span style="color:#f92672">&#34;source_ami_filter&#34;</span>: {
      <span style="color:#f92672">&#34;filters&#34;</span>: {
        <span style="color:#f92672">&#34;virtualization-type&#34;</span>: <span style="color:#e6db74">&#34;hvm&#34;</span>,
        <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;debian-stretch-*&#34;</span>,
        <span style="color:#f92672">&#34;architecture&#34;</span>: <span style="color:#e6db74">&#34;x86_64&#34;</span>,
        <span style="color:#f92672">&#34;root-device-type&#34;</span>: <span style="color:#e6db74">&#34;ebs&#34;</span>
      },
      <span style="color:#f92672">&#34;owners&#34;</span>: [<span style="color:#e6db74">&#34;379101102735&#34;</span>],
      <span style="color:#f92672">&#34;most_recent&#34;</span>: <span style="color:#66d9ef">true</span>
    },
    <span style="color:#f92672">&#34;instance_type&#34;</span>: <span style="color:#e6db74">&#34;t3.micro&#34;</span>,
    <span style="color:#f92672">&#34;ssh_username&#34;</span>: <span style="color:#e6db74">&#34;admin&#34;</span>,
    <span style="color:#f92672">&#34;ssh_keypair_name&#34;</span>: <span style="color:#e6db74">&#34;{{ user `ssh_key_name`}}&#34;</span>,
    <span style="color:#f92672">&#34;ssh_private_key_file&#34;</span>: <span style="color:#e6db74">&#34;{{ user `local_ssh_key_location`}}&#34;</span>,
    <span style="color:#f92672">&#34;ssh_pty&#34;</span>: <span style="color:#66d9ef">true</span>,
    <span style="color:#f92672">&#34;ami_name&#34;</span>: <span style="color:#e6db74">&#34;debian9-base-{{ timestamp }}&#34;</span>,
    <span style="color:#f92672">&#34;vpc_id&#34;</span>: <span style="color:#e6db74">&#34;{{ user `default_vpc_id`}}&#34;</span>,
    <span style="color:#f92672">&#34;subnet_id&#34;</span>: <span style="color:#e6db74">&#34;{{ user `subnet_id_1a`}}&#34;</span>,
    <span style="color:#f92672">&#34;security_group_id&#34;</span>: <span style="color:#e6db74">&#34;{{ user `default_security_group`}}&#34;</span>,
    <span style="color:#f92672">&#34;tags&#34;</span>: {
       <span style="color:#f92672">&#34;ami_type&#34;</span>: <span style="color:#e6db74">&#34;debian9-base&#34;</span>,
       <span style="color:#f92672">&#34;created_by&#34;</span>: <span style="color:#e6db74">&#34;Packer&#34;</span>
    }
  }
  ],
  <span style="color:#f92672">&#34;provisioners&#34;</span>: [
   {
    <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;shell&#34;</span>,
    <span style="color:#f92672">&#34;inline_shebang&#34;</span>: <span style="color:#e6db74">&#34;/bin/sh -x&#34;</span>,
    <span style="color:#f92672">&#34;inline&#34;</span>: [
      <span style="color:#e6db74">&#34;sleep 30&#34;</span>,
      <span style="color:#e6db74">&#34;sudo apt-get update -qy &amp;&amp; sudo apt-get dist-upgrade -qy&#34;</span>,
      <span style="color:#e6db74">&#34;sudo apt-get install -qy tcpdump telnet iotop htop dnsutils net-tools sysstat vim git wget zsh&#34;</span>,
      <span style="color:#e6db74">&#34;wget -O - https://repo.saltstack.com/apt/debian/9/amd64/latest/SALTSTACK-GPG-KEY.pub | sudo apt-key add -&#34;</span>,
      <span style="color:#e6db74">&#34;echo &#39;deb http://repo.saltstack.com/apt/debian/9/amd64/latest stretch main&#39; | sudo tee /etc/apt/sources.list.d/saltstack.list&#34;</span>,
      <span style="color:#e6db74">&#34;sudo apt update &amp;&amp; sudo apt-get install -y salt-minion nagios-nrpe-server nagios-plugins&#34;</span>
    ]
   }
 ]
}
</code></pre></div><p>The above is a mix of JSON, but with Jinja-style templated variables. Anything which is prefaced with <code>user</code>, e.g. <code>{{ user `ssh_key_name` }}</code> is a user-defined variable. In the above, we also have <code>{{ timestamp }}</code>, which is just a Unix timestamp (so that each generated image has a unique name).</p>
<p>There are two sections, the <strong>Builder</strong> and the <strong>Provisioner</strong>. The builder is where you are spinning up the official/existing image with the specified parameters, the provisioner is how you will customize the image after it starts.</p>
<p>You can also make use of multiple provisioners. For example, you could start bootstrapping with a couple of shell commands, and then apply the rest of the configuration with Ansible. The various provisioners are detailed <a href="https://packer.io/docs/provisioners/index.html">here</a></p>
<p>Typically in the main cloud providers, you would make use of an Official machine image, e.g. the Debian-created Stretch 64-bit x86 AWS AMI, and then apply your customization from there.</p>
<p>To make use of user variables, you can either provide them at the command line, or you can make use of a &ldquo;vars&rdquo; file. An example of the file would be the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
 <span style="color:#f92672">&#34;aws_region&#34;</span>: <span style="color:#e6db74">&#34;eu-central-1&#34;</span>,
 <span style="color:#f92672">&#34;aws_profile&#34;</span>: <span style="color:#e6db74">&#34;testing&#34;</span>,
 <span style="color:#f92672">&#34;default_vpc_id&#34;</span>: <span style="color:#e6db74">&#34;vpc-xxxxxx&#34;</span>,
 <span style="color:#f92672">&#34;subnet_id_1a&#34;</span>: <span style="color:#e6db74">&#34;subnet-xxxxxx&#34;</span>,
 <span style="color:#f92672">&#34;default_security_group&#34;</span>: <span style="color:#e6db74">&#34;sg-xxxxxxx&#34;</span>,
 <span style="color:#f92672">&#34;windows_security_group&#34;</span>: <span style="color:#e6db74">&#34;sg-xxxxxxx&#34;</span>,
 <span style="color:#f92672">&#34;ssh_key_name&#34;</span>: <span style="color:#e6db74">&#34;$SSH_KEY_NAME&#34;</span>,
 <span style="color:#f92672">&#34;local_ssh_key_location&#34;</span>: <span style="color:#e6db74">&#34;~/.ssh/$SSH_KEY_NAME&#34;</span>,
 <span style="color:#f92672">&#34;environment&#34;</span>: <span style="color:#e6db74">&#34;staging&#34;</span>,
 <span style="color:#f92672">&#34;private_domain&#34;</span>: <span style="color:#e6db74">&#34;packer.yetiops.net&#34;</span>,
 <span style="color:#f92672">&#34;aws_account&#34;</span>: <span style="color:#e6db74">&#34;xxxxxxxxx&#34;</span>
}
</code></pre></div><p>Unlike <a href="https://terraform.io">Terraform</a>, Packer does not automatically make use of &ldquo;vars&rdquo; files. Instead, you need to specify it like such: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ packer build -var-file<span style="color:#f92672">=</span>vars.json packer-ami-build.json
</code></pre></div><p>After this, you will see Packer build and generate the image: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ packer build -var-file<span style="color:#f92672">=</span>vars.json packer-ami-build.json
<span style="color:#f92672">==</span>&gt; amazon-ebs: Prevalidating AMI Name: debian9-base-1580820854
    amazon-ebs: Found Image ID: ami-06d77f4fcb1f698eb
<span style="color:#f92672">==</span>&gt; amazon-ebs: Using existing SSH private key
<span style="color:#f92672">==</span>&gt; amazon-ebs: Launching a source AWS instance...
<span style="color:#f92672">==</span>&gt; amazon-ebs: Adding tags to source instance
    amazon-ebs: Adding tag: <span style="color:#e6db74">&#34;Name&#34;</span>: <span style="color:#e6db74">&#34;Packer Builder&#34;</span>
    amazon-ebs: Instance ID: i-0bc40cf110e2e886e
<span style="color:#f92672">==</span>&gt; amazon-ebs: Waiting <span style="color:#66d9ef">for</span> instance <span style="color:#f92672">(</span>i-0bc40cf110e2e886e<span style="color:#f92672">)</span> to become ready...
<span style="color:#f92672">==</span>&gt; amazon-ebs: Using ssh communicator to connect: 10.100.1.1
<span style="color:#f92672">==</span>&gt; amazon-ebs: Waiting <span style="color:#66d9ef">for</span> SSH to become available...
<span style="color:#f92672">==</span>&gt; amazon-ebs: Connected to SSH!
<span style="color:#f92672">[</span>...<span style="color:#f92672">]</span>
</code></pre></div><h2 id="beyond-the-shell-provisioner">Beyond the shell provisioner</h2>
<p>As mentioned, you can also use a number of different configuration management tools to bootstrap an image ready for usage. This could be Puppet Masterless, Salt Masterless, or (as in our scenario, Ansible). More information on them is available <a href="https://packer.io/docs/provisioners/index.html">here</a></p>
<p>We do make use of Salt generally for our configuration management, but Ansible was chosen for this due to the ease of setup and configuration for basic tasks. Salt has, in my experience, a speed advantage over Ansible (due to the lack of agents in Ansible). However this is negated when using Salt Masterless, at which point Ansible seemed like a good choice.</p>
<p>To make use of Ansible within a Packer build, you would define a provisioner like such: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">  <span style="color:#e6db74">&#34;provisioners&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> [
   {
    <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;ansible&#34;</span>,
    <span style="color:#f92672">&#34;extra_arguments&#34;</span>: [
      <span style="color:#e6db74">&#34;--extra-vars&#34;</span>,
      <span style="color:#e6db74">&#34;ansible_python_interpreter=/usr/bin/python&#34;</span>
    ],
    <span style="color:#f92672">&#34;playbook_file&#34;</span>: <span style="color:#e6db74">&#34;../ansible/base/debian/base.yaml&#34;</span>,
    <span style="color:#f92672">&#34;user&#34;</span>: <span style="color:#e6db74">&#34;admin&#34;</span>
   }
  ]
</code></pre></div><p>As you can see, you do not require a lot of configuration to add a provisioner. You could then reference an Ansible Playbook that does something like adding a few packages, e.g.: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">hosts</span>: all
  <span style="color:#66d9ef">become</span>: yes
  <span style="color:#66d9ef">become_method</span>: sudo
  <span style="color:#66d9ef">tasks</span>:
  - <span style="color:#66d9ef">name</span>: Ensure gnupg2 is installed to add keys
    <span style="color:#66d9ef">package</span>:
      <span style="color:#66d9ef">name</span>: gnupg2
      <span style="color:#66d9ef">state</span>: present
      <span style="color:#66d9ef">update_cache</span>: yes

  - <span style="color:#66d9ef">name</span>: Add Salt GPG
    <span style="color:#66d9ef">apt_key</span>:
      <span style="color:#66d9ef">url</span>: https://repo.saltstack.com/apt/debian/<span style="color:#ae81ff">9</span>/amd64/latest/SALTSTACK-GPG-KEY.pub
      <span style="color:#66d9ef">state</span>: present

  - <span style="color:#66d9ef">name</span>: Add Salt Repo
    <span style="color:#66d9ef">apt_repository</span>:
      <span style="color:#66d9ef">repo</span>: deb http://repo.saltstack.com/apt/debian/<span style="color:#ae81ff">9</span>/amd64/latest stretch main
      <span style="color:#66d9ef">state</span>: present
  
  - <span style="color:#66d9ef">name</span>: Update cache and upgrade packages
    <span style="color:#66d9ef">apt</span>:
      <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;*&#34;</span>
      <span style="color:#66d9ef">state</span>: latest
      <span style="color:#66d9ef">update_cache</span>: yes

  - <span style="color:#66d9ef">name</span>: Dist Upgrade
    <span style="color:#66d9ef">apt</span>:
      <span style="color:#66d9ef">upgrade</span>: dist

  - <span style="color:#66d9ef">name</span>: Get all required base packages
    <span style="color:#66d9ef">apt</span>:
      <span style="color:#66d9ef">name</span>: <span style="color:#e6db74">&#34;{{ packages }}&#34;</span>
    <span style="color:#66d9ef">vars</span>:
      <span style="color:#66d9ef">packages</span>:
       - tcpdump
       - telnet
       - iotop
       - htop
       - dnsutils
       - net-tools
       - sysstat
       - vim
       - git
       - wget
       - zsh
       - salt-minion
       - nagios-nrpe-server
       - nagios-plugins
       - exim4
</code></pre></div><p>Given the breadth of modules available within Ansible, there is a lot more you could do to an image than the above. It does at least show the basics of what can be done using Packer and Ansible together.</p>
<h2 id="so-what-about-windows">So what about Windows?</h2>
<p>Previously, we had Packer building Windows images using Powershell commands. For example, the below installs <a href="https://chocolatey.org">Chocolatey</a> (a Windows package manager), restarts Windows to allow Chocolatey to install correctly, installs a few packages, and then transfers some files over, ready to be used by <code>NSClient++</code> (the Windows equivalent to NRPE, for Nagios remote checks): -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json"> <span style="color:#e6db74">&#34;provisioners&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> [
    {
      <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;powershell&#34;</span>,
      <span style="color:#f92672">&#34;scripts&#34;</span>: [
        <span style="color:#e6db74">&#34;../build-files/windows/disable-uac.ps1&#34;</span>,
        <span style="color:#e6db74">&#34;../build-files/windows/ChocolateyInstall.ps1&#34;</span>
       ]
    },
    {
      <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;windows-restart&#34;</span>,
      <span style="color:#f92672">&#34;restart_check_command&#34;</span>: <span style="color:#e6db74">&#34;powershell -command \&#34;&amp; {Write-Output &#39;restarted.&#39;}\&#34;&#34;</span>
    },
    {
      <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;powershell&#34;</span>,
      <span style="color:#f92672">&#34;inline&#34;</span>: [
        <span style="color:#e6db74">&#34;choco install -y nscp&#34;</span>,
        <span style="color:#e6db74">&#34;choco install -y prometheus-wmi-exporter.install&#34;</span>,
        <span style="color:#e6db74">&#34;choco install -y openssh&#34;</span>
      ]
    },
    {
      <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;file&#34;</span>,
      <span style="color:#f92672">&#34;source&#34;</span>: <span style="color:#e6db74">&#34;../build-files/windows/nsclient.ini&#34;</span>,
      <span style="color:#f92672">&#34;destination&#34;</span>: <span style="color:#e6db74">&#34;C:/Program Files/NSClient++/nsclient.ini&#34;</span>
    }
  ]
</code></pre></div><p>After a while, this kind of configuration can start to become unmanageable. So what can we do?</p>
<p>Since Ansible version 1.7, Ansible has supported managing Windows machines. This gives us a good option for applying configuration, adding packages, and updating files on the Windows images before they are used.</p>
<h3 id="stumbling-blocks">Stumbling blocks</h3>
<p>When I first attempted to use Packer with Ansible to manage Windows machine, I could never get Ansible to talk to the Windows machines correctly. Take the below as an example: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">amazon-ebs output will be in this color.

<span style="color:#f92672">==</span>&gt; amazon-ebs: Prevalidating AMI Name: win2016-base-1580822124
    amazon-ebs: Found Image ID: ami-0e484c84e6d59f3a3
<span style="color:#f92672">==</span>&gt; amazon-ebs: Creating temporary keypair: packer_5e396e6c-0cbc-fc22-f978-015e782280b4
<span style="color:#f92672">==</span>&gt; amazon-ebs: Launching a source AWS instance...
<span style="color:#f92672">==</span>&gt; amazon-ebs: Adding tags to source instance
    amazon-ebs: Adding tag: <span style="color:#e6db74">&#34;Name&#34;</span>: <span style="color:#e6db74">&#34;Packer Builder&#34;</span>
    amazon-ebs: Instance ID: i-021e6d35e7e6e5ae9
<span style="color:#f92672">==</span>&gt; amazon-ebs: Waiting <span style="color:#66d9ef">for</span> instance <span style="color:#f92672">(</span>i-021e6d35e7e6e5ae9<span style="color:#f92672">)</span> to become ready...
<span style="color:#f92672">==</span>&gt; amazon-ebs: Waiting <span style="color:#66d9ef">for</span> auto-generated password <span style="color:#66d9ef">for</span> instance...
    amazon-ebs: It is normal <span style="color:#66d9ef">for</span> this process to take up to <span style="color:#ae81ff">15</span> minutes,
    amazon-ebs: but it usually takes around 5. Please wait.
    amazon-ebs:  
    amazon-ebs: Password retrieved!
<span style="color:#f92672">==</span>&gt; amazon-ebs: Using winrm communicator to connect: 10.100.1.1
<span style="color:#f92672">==</span>&gt; amazon-ebs: Waiting <span style="color:#66d9ef">for</span> WinRM to become available...
    amazon-ebs: WinRM connected.
<span style="color:#f92672">==</span>&gt; amazon-ebs: <span style="color:#75715e">#&lt; CLIXML</span>
<span style="color:#f92672">==</span>&gt; amazon-ebs: &lt;Objs Version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;1.1.0.1&#34;</span> xmlns<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;http://schemas.microsoft.com/powershell/2004/04&#34;</span>&gt;&lt;Obj S<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;progress&#34;</span> RefId<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;0&#34;</span>&gt;&lt;TN RefId<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;0&#34;</span>&gt;&lt;T&gt;System.Management.Automation.PSCustomObject&lt;/T&gt;&lt;T&gt;System.Object&lt;/T&gt;&lt;/TN&gt;&lt;MS&gt;&lt;I64 N<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;SourceId&#34;</span>&gt;1&lt;/I64&gt;&lt;PR N<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Record&#34;</span>&gt;&lt;AV&gt;Preparing modules <span style="color:#66d9ef">for</span> first use.&lt;/AV&gt;&lt;AI&gt;0&lt;/AI&gt;&lt;Nil /&gt;&lt;PI&gt;-1&lt;/PI&gt;&lt;PC&gt;-1&lt;/PC&gt;&lt;T&gt;Completed&lt;/T&gt;&lt;SR&gt;-1&lt;/SR&gt;&lt;SD&gt; &lt;/SD&gt;&lt;/PR&gt;&lt;/MS&gt;&lt;/Obj&gt;&lt;Obj S<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;progress&#34;</span> RefId<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;1&#34;</span>&gt;&lt;TNRef RefId<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;0&#34;</span> /&gt;&lt;MS&gt;&lt;I64 N<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;SourceId&#34;</span>&gt;1&lt;/I64&gt;&lt;PR N<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Record&#34;</span>&gt;&lt;AV&gt;Preparing modules <span style="color:#66d9ef">for</span> first use.&lt;/AV&gt;&lt;AI&gt;0&lt;/AI&gt;&lt;Nil /&gt;&lt;PI&gt;-1&lt;/PI&gt;&lt;PC&gt;-1&lt;/PC&gt;&lt;T&gt;Completed&lt;/T&gt;&lt;SR&gt;-1&lt;/SR&gt;&lt;SD&gt; &lt;/SD&gt;&lt;/PR&gt;&lt;/MS&gt;&lt;/Obj&gt;&lt;/Objs&gt;
<span style="color:#f92672">==</span>&gt; amazon-ebs: Connected to WinRM!
<span style="color:#f92672">==</span>&gt; amazon-ebs: Provisioning with Ansible...
<span style="color:#f92672">==</span>&gt; amazon-ebs: Executing Ansible: ansible-playbook --extra-vars packer_build_name<span style="color:#f92672">=</span>amazon-ebs packer_builder_type<span style="color:#f92672">=</span>amazon-ebs -o IdentitiesOnly<span style="color:#f92672">=</span>yes -i /tmp/packer-provisioner-ansible207671579 /home/stuh84/git/Infrastructure/packer/ansible/base/windows/base.yaml -e ansible_ssh_private_key_file<span style="color:#f92672">=</span>/tmp/ansible-key559438764--extra-vars ansible_shell_type<span style="color:#f92672">=</span>powershell --extra-vars ansible_shell_executable<span style="color:#f92672">=</span>None
    amazon-ebs:
    amazon-ebs: PLAY <span style="color:#f92672">[</span>all<span style="color:#f92672">]</span> *********************************************************************
    amazon-ebs:
    amazon-ebs: TASK <span style="color:#f92672">[</span>Install required packages<span style="color:#f92672">]</span> ***********************************************
    amazon-ebs: fatal: <span style="color:#f92672">[</span>default<span style="color:#f92672">]</span>: FAILED! <span style="color:#f92672">=</span>&gt; <span style="color:#f92672">{</span><span style="color:#e6db74">&#34;changed&#34;</span>: false, <span style="color:#e6db74">&#34;module_stderr&#34;</span>: <span style="color:#e6db74">&#34;Warning: Permanently added &#39;[127.0.0.1]:39793&#39; (RSA) to the list of known hosts.\r\nParameter format not correct - ;\r\n&#34;</span>, <span style="color:#e6db74">&#34;module_stdout&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#e6db74">&#34;msg&#34;</span>: <span style="color:#e6db74">&#34;MODULE FAILURE\nSee stdout/stderr for the exact error&#34;</span>, <span style="color:#e6db74">&#34;rc&#34;</span>: 1<span style="color:#f92672">}</span>
    amazon-ebs:
    amazon-ebs: PLAY RECAP *********************************************************************
    amazon-ebs: default                    : ok<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>    changed<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>    unreachable<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>    failed<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>    skipped<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>    rescued<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>    ignored<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>
    amazon-ebs:
<span style="color:#f92672">==</span>&gt; amazon-ebs: Provisioning step had errors: Running the cleanup provisioner, <span style="color:#66d9ef">if</span> present...
<span style="color:#f92672">==</span>&gt; amazon-ebs: Terminating the source AWS instance...
</code></pre></div><p>The provisioner at this point looks almost exactly like the one we use for Linux deployments</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">  <span style="color:#e6db74">&#34;provisioners&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> [
   {
    <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;ansible&#34;</span>,
    <span style="color:#f92672">&#34;extra_arguments&#34;</span>: [
      <span style="color:#e6db74">&#34;--extra-vars&#34;</span>,
      <span style="color:#e6db74">&#34;ansible_shell_type=powershell&#34;</span>,
      <span style="color:#e6db74">&#34;--extra-vars&#34;</span>,
      <span style="color:#e6db74">&#34;ansible_shell_executable=None&#34;</span>
    ],
    <span style="color:#f92672">&#34;playbook_file&#34;</span>: <span style="color:#e6db74">&#34;../ansible/base/windows/base.yaml&#34;</span>,
    <span style="color:#f92672">&#34;user&#34;</span>: <span style="color:#e6db74">&#34;Administrator&#34;</span>
   },
</code></pre></div><p>It&rsquo;s worth mentioning at this point that Ansible does not talk to the AWS Packer Builder image (i.e. the image that you are customizing) directly, instead it talks to the machine through a connection that Packer sets up as a kind of proxy to the machine. This is the same with Linux or Windows.</p>
<p>However there seems to be an issue with using this directly when Ansible is trying to use WinRM/Powershell (i.e. its native method of talking to Windows machines).</p>
<h3 id="packer-connection-plugin-for-ansible">Packer connection plugin for Ansible</h3>
<p>After some searching around and looking through a number of bug reports, I found reference to an Ansible connection plugin for Packer. This allows Ansible to use the existing Packer connection to the image.</p>
<p>To make use of it, do the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo mkdir -p /usr/share/ansible/plugins/connection/
$ cd /usr/share/ansible/plugins/connection/
$ wget https://raw.githubusercontent.com/hashicorp/packer/master/test/fixtures/provisioner-ansible/connection_plugins/packer.py
</code></pre></div><p>You&rsquo;ll also need to edit your <code>ansible.cfg</code> file (by default, in the <code>/etc/ansible</code> directory) and make sure that there is a line that says</p>
<ul>
<li><code>connection_plugins = /usr/share/ansible/plugins/connection</code></li>
</ul>
<p>To make use of this connection plugin in your Packer file, all you need to do is add the following line: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">    <span style="color:#e6db74">&#34;type&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#e6db74">&#34;ansible&#34;</span><span style="color:#960050;background-color:#1e0010">,</span>
    <span style="color:#e6db74">&#34;extra_arguments&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> [
      <span style="color:#e6db74">&#34;--connection&#34;</span>, <span style="color:#e6db74">&#34;packer&#34;</span>,          <span style="color:#960050;background-color:#1e0010">&lt;----------</span> <span style="color:#960050;background-color:#1e0010">This</span> <span style="color:#960050;background-color:#1e0010">one</span>
      <span style="color:#e6db74">&#34;--extra-vars&#34;</span>,
      <span style="color:#e6db74">&#34;ansible_shell_type=powershell&#34;</span>,
      <span style="color:#e6db74">&#34;--extra-vars&#34;</span>,
      <span style="color:#e6db74">&#34;ansible_shell_executable=None&#34;</span>
</code></pre></div><p>When you next attempt to run a Packer build, Ansible should start working through the Packer connection, and then deploy as normal. For example: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">amazon-ebs output will be in this color.

<span style="color:#f92672">==</span>&gt; amazon-ebs: Prevalidating AMI Name: win2016-base-1580823483
    amazon-ebs: Found Image ID: ami-0e484c84e6d59f3a3
<span style="color:#f92672">==</span>&gt; amazon-ebs: Creating temporary keypair: packer_5e3973bb-4e9f-1749-d007-8addeee4cf0d
<span style="color:#f92672">==</span>&gt; amazon-ebs: Launching a source AWS instance...
<span style="color:#f92672">==</span>&gt; amazon-ebs: Adding tags to source instance
    amazon-ebs: Adding tag: <span style="color:#e6db74">&#34;Name&#34;</span>: <span style="color:#e6db74">&#34;Packer Builder&#34;</span>
    amazon-ebs: Instance ID: i-XXXXXXXXXXXXXXX
<span style="color:#f92672">==</span>&gt; amazon-ebs: Waiting <span style="color:#66d9ef">for</span> instance <span style="color:#f92672">(</span>i-XXXXXXXXXXX<span style="color:#f92672">)</span> to become ready...
<span style="color:#f92672">==</span>&gt; amazon-ebs: Waiting <span style="color:#66d9ef">for</span> auto-generated password <span style="color:#66d9ef">for</span> instance...
    amazon-ebs: It is normal <span style="color:#66d9ef">for</span> this process to take up to <span style="color:#ae81ff">15</span> minutes,
    amazon-ebs: but it usually takes around 5. Please wait.
    amazon-ebs:  
    amazon-ebs: Password retrieved!
<span style="color:#f92672">==</span>&gt; amazon-ebs: Using winrm communicator to connect: 10.100.1.1
<span style="color:#f92672">==</span>&gt; amazon-ebs: Waiting <span style="color:#66d9ef">for</span> WinRM to become available...
<span style="color:#f92672">==</span>&gt; amazon-ebs: <span style="color:#75715e">#&lt; CLIXML</span>
    amazon-ebs: WinRM connected.
<span style="color:#f92672">==</span>&gt; amazon-ebs: &lt;Objs Version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;1.1.0.1&#34;</span> xmlns<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;http://schemas.microsoft.com/powershell/2004/04&#34;</span>&gt;&lt;Obj S<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;progress&#34;</span> RefId<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;0&#34;</span>&gt;&lt;TN RefId<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;0&#34;</span>&gt;&lt;T&gt;System.Management.Automation.PSCustomObject&lt;/T&gt;&lt;T&gt;System.Object&lt;/T&gt;&lt;/TN&gt;&lt;MS&gt;&lt;I64 N<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;SourceId&#34;</span>&gt;1&lt;/I64&gt;&lt;PR N<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Record&#34;</span>&gt;&lt;AV&gt;Preparing modules <span style="color:#66d9ef">for</span> first use.&lt;/AV&gt;&lt;AI&gt;0&lt;/AI&gt;&lt;Nil /&gt;&lt;PI&gt;-1&lt;/PI&gt;&lt;PC&gt;-1&lt;/PC&gt;&lt;T&gt;Completed&lt;/T&gt;&lt;SR&gt;-1&lt;/SR&gt;&lt;SD&gt; &lt;/SD&gt;&lt;/PR&gt;&lt;/MS&gt;&lt;/Obj&gt;&lt;Obj S<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;progress&#34;</span> RefId<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;1&#34;</span>&gt;&lt;TNRef RefId<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;0&#34;</span> /&gt;&lt;MS&gt;&lt;I64 N<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;SourceId&#34;</span>&gt;1&lt;/I64&gt;&lt;PR N<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Record&#34;</span>&gt;&lt;AV&gt;Preparing modules <span style="color:#66d9ef">for</span> first use.&lt;/AV&gt;&lt;AI&gt;0&lt;/AI&gt;&lt;Nil /&gt;&lt;PI&gt;-1&lt;/PI&gt;&lt;PC&gt;-1&lt;/PC&gt;&lt;T&gt;Completed&lt;/T&gt;&lt;SR&gt;-1&lt;/SR&gt;&lt;SD&gt; &lt;/SD&gt;&lt;/PR&gt;&lt;/MS&gt;&lt;/Obj&gt;&lt;/Objs&gt;
<span style="color:#f92672">==</span>&gt; amazon-ebs: Connected to WinRM!
<span style="color:#f92672">==</span>&gt; amazon-ebs: Provisioning with Ansible...
<span style="color:#f92672">==</span>&gt; amazon-ebs: Executing Ansible: ansible-playbook --extra-vars packer_build_name<span style="color:#f92672">=</span>amazon-ebs packer_builder_type<span style="color:#f92672">=</span>amazon-ebs -o IdentitiesOnly<span style="color:#f92672">=</span>yes -i /tmp/packer-provisioner-ansible213632365 /home/stuh84/git/Infrastructure/packer/ansible/base/windows/base.yaml -e ansible_ssh_private_key_file<span style="color:#f92672">=</span>/tmp/ansible-key670542534 --connection packer --extra-vars ansible_shell_type<span style="color:#f92672">=</span>powershell --extra-vars ansible_shell_executable<span style="color:#f92672">=</span>None
    amazon-ebs:
    amazon-ebs: PLAY <span style="color:#f92672">[</span>all<span style="color:#f92672">]</span> *********************************************************************
    amazon-ebs:
    amazon-ebs: TASK <span style="color:#f92672">[</span>Install required packages<span style="color:#f92672">]</span> ***********************************************
    amazon-ebs: <span style="color:#f92672">[</span>WARNING<span style="color:#f92672">]</span>: Chocolatey was missing from this system, so it was installed during
    amazon-ebs: changed: <span style="color:#f92672">[</span>default<span style="color:#f92672">]</span>
    amazon-ebs: this task run.
    amazon-ebs:
    amazon-ebs: TASK <span style="color:#f92672">[</span>nsclient INI<span style="color:#f92672">]</span> ************************************************************
    amazon-ebs: changed: <span style="color:#f92672">[</span>default<span style="color:#f92672">]</span>
    amazon-ebs:
    amazon-ebs: TASK <span style="color:#f92672">[</span>Invoke OpenSSD Install script<span style="color:#f92672">]</span> *******************************************
    amazon-ebs: changed: <span style="color:#f92672">[</span>default<span style="color:#f92672">]</span>
    amazon-ebs:
    amazon-ebs: TASK <span style="color:#f92672">[</span>Enable SSHD<span style="color:#f92672">]</span> *************************************************************
    amazon-ebs: changed: <span style="color:#f92672">[</span>default<span style="color:#f92672">]</span>
    amazon-ebs:
    amazon-ebs: TASK <span style="color:#f92672">[</span>SSHD Firewall Rule<span style="color:#f92672">]</span> ******************************************************
    amazon-ebs: changed: <span style="color:#f92672">[</span>default<span style="color:#f92672">]</span>
    amazon-ebs:
    amazon-ebs: PLAY RECAP *********************************************************************
    amazon-ebs: default                    : ok<span style="color:#f92672">=</span><span style="color:#ae81ff">10</span>   changed<span style="color:#f92672">=</span><span style="color:#ae81ff">9</span>    unreachable<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>    failed<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>    skipped<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>    rescued<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>    ignored<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>
    amazon-ebs:
<span style="color:#f92672">==</span>&gt; amazon-ebs: Provisioning with Powershell...
<span style="color:#f92672">==</span>&gt; amazon-ebs: Provisioning with powershell script: /tmp/powershell-provisioner627331863
    amazon-ebs:
    amazon-ebs: TaskPath                                       TaskName                          State
    amazon-ebs: --------                                       --------                          -----
    amazon-ebs: <span style="color:#ae81ff">\ </span>                                             Amazon Ec2 Launch - Instance I... Ready
<span style="color:#f92672">==</span>&gt; amazon-ebs: Stopping the source instance...
    amazon-ebs: Stopping instance
<span style="color:#f92672">==</span>&gt; amazon-ebs: Waiting <span style="color:#66d9ef">for</span> the instance to stop...
<span style="color:#f92672">==</span>&gt; amazon-ebs: Creating AMI win2016-base-1580823483 from instance i-0e60a616f082a01bd
    amazon-ebs: AMI: ami-XXXXXXXXXXXXXX
<span style="color:#f92672">==</span>&gt; amazon-ebs: Waiting <span style="color:#66d9ef">for</span> AMI to become ready...
<span style="color:#f92672">==</span>&gt; amazon-ebs: Adding tags to AMI <span style="color:#f92672">(</span>ami-XXXXXXXXXXX<span style="color:#f92672">)</span>...
<span style="color:#f92672">==</span>&gt; amazon-ebs: Tagging snapshot: snap-XXXXXXXXXXX
<span style="color:#f92672">==</span>&gt; amazon-ebs: Creating AMI tags
    amazon-ebs: Adding tag: <span style="color:#e6db74">&#34;ami_type&#34;</span>: <span style="color:#e6db74">&#34;win2016-base&#34;</span>
    amazon-ebs: Adding tag: <span style="color:#e6db74">&#34;created_by&#34;</span>: <span style="color:#e6db74">&#34;Packer&#34;</span>
<span style="color:#f92672">==</span>&gt; amazon-ebs: Creating snapshot tags
<span style="color:#f92672">==</span>&gt; amazon-ebs: Terminating the source AWS instance...
<span style="color:#f92672">==</span>&gt; amazon-ebs: Cleaning up any extra volumes...
<span style="color:#f92672">==</span>&gt; amazon-ebs: No volumes to clean up, skipping
<span style="color:#f92672">==</span>&gt; amazon-ebs: Deleting temporary keypair...
Build <span style="color:#e6db74">&#39;amazon-ebs&#39;</span> finished.
</code></pre></div><p>An example playbook to apply would be something like the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">- <span style="color:#66d9ef">hosts</span>: all
  <span style="color:#66d9ef">remote_user</span>: Administrator 
  <span style="color:#66d9ef">gather_facts</span>: <span style="color:#66d9ef">false</span>
  <span style="color:#66d9ef">tasks</span>:
  - <span style="color:#66d9ef">name</span>: Install required packages
    <span style="color:#66d9ef">win_chocolatey</span>:
      <span style="color:#66d9ef">name</span>:
      - nscp
      - prometheus-wmi-exporter.install
      - openssh

  - <span style="color:#66d9ef">name</span>: nsclient INI
    <span style="color:#66d9ef">win_copy</span>: 
      <span style="color:#66d9ef">src</span>: ../../../build-files/windows/nsclient.ini
      <span style="color:#66d9ef">dest</span>: C:/Program Files/NSClient++/nsclient.ini

  - <span style="color:#66d9ef">name</span>: Invoke OpenSSD Install script
    <span style="color:#66d9ef">win_shell</span>: powershell.exe -ExecutionPolicy Bypass -File <span style="color:#e6db74">&#34;c:\Program Files\OpenSSH-Win64\install-sshd.ps1&#34;</span>

  - <span style="color:#66d9ef">name</span>: Enable SSHD
    <span style="color:#66d9ef">win_service</span>:
      <span style="color:#66d9ef">name</span>: sshd
      <span style="color:#66d9ef">state</span>: started
      <span style="color:#66d9ef">start_mode</span>: auto

  - <span style="color:#66d9ef">name</span>: SSHD Firewall Rule
    <span style="color:#66d9ef">win_firewall_rule</span>:
      <span style="color:#66d9ef">name</span>: OpenSSHD
      <span style="color:#66d9ef">localport</span>: <span style="color:#ae81ff">22</span>
      <span style="color:#66d9ef">action</span>: allow
      <span style="color:#66d9ef">direction</span>: in
      <span style="color:#66d9ef">protocol</span>: tcp
      <span style="color:#66d9ef">state</span>: present
      <span style="color:#66d9ef">enabled</span>: yes
</code></pre></div><p>An interesting point to note here is that when you use the <strong>win_chocolatey</strong> module, Ansible will install Chocolatey before attempting to install any packages, meaning you don&rsquo;t need to worry about installing it first.</p>
<p>I also add in OpenSSH, because I still haven&rsquo;t entirely gotten Powershell remoteing/WinRM working from Linux. Having SSH available allows me to manage it like a Linux box (just with a different shell).</p>
<h3 id="sysprep-and-initializeinstance">Sysprep and InitializeInstance</h3>
<p>To ensure that any Windows machines generated do not have the same GUIDs and any other identifying IDs (which can cause issues with joining Active Directory, and Licensing), AWS provide Sysprep and InitializeInstance scripts as part of their base Windows AMI (which all our Windows images use as base for customization).</p>
<p>I did initially try to execute these with Ansible. However it appears that in making the image unique, it breaks the Ansible-over-Packer connectivity. Instead, to finish off any Windows image, I just apply these scripts using the Powershell provisioner: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">    {
      <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;powershell&#34;</span>,
      <span style="color:#f92672">&#34;inline&#34;</span>: [
        <span style="color:#e6db74">&#34;C:\\ProgramData\\Amazon\\EC2-Windows\\Launch\\Scripts\\InitializeInstance.ps1 -Schedule&#34;</span>,
        <span style="color:#e6db74">&#34;C:\\ProgramData\\Amazon\\EC2-Windows\\Launch\\Scripts\\SysprepInstance.ps1 -NoShutdown&#34;</span>
      ]
    }
</code></pre></div><p>While I haven&rsquo;t attempted to build Windows machine images on other platforms/cloud providers, I would assume they provide similar methods to ensure that a unique machine is generated when you fire up your machine image later. If you do find issues with Ansible dropping connectivity when trying to kick off these scripts, I would recommend using the Powershell provisioner instead.</p>
<h2 id="bonus-using-the-images-with-terraform">Bonus: Using the images with Terraform</h2>
<p>If you are using Terraform to deploy your cloud infrastructure, you can easily make use of the images Packer builds, using the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;aws_ami&#34; &#34;packer-windows-image&#34;</span> {
  most_recent <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>

  <span style="color:#66d9ef">filter</span> {
    name   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;name&#34;</span>
    values <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;windows2016-base-*&#34;</span>]
  }

  <span style="color:#66d9ef">filter</span> {
    name   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;virtualization-type&#34;</span>
    values <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;hvm&#34;</span>]
  }

  <span style="color:#66d9ef">filter</span> {
    name   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tag:ami_type&#34;</span>
    values <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;windows2016-base&#34;</span>]
  }

  owners <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;${var.aws_account}&#34;</span>]
}

<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_instance&#34; &#34;windows-ec2&#34;</span> {
  ami                  <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">aws_ami</span>.<span style="color:#66d9ef">packer</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">windows</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">image</span>.<span style="color:#66d9ef">id</span>
[...]
</code></pre></div><p>The above will match the most recent version of your base image, and use it as a data source for your EC2 AWS Instance resource.</p>
<h2 id="references">References</h2>
<p>To put this together, I used a few points from this <a href="https://github.com/mjhermanson/packer-windows-ansible-aws">repository</a>, <a href="https://github.com/hashicorp/packer/issues/5845">this GitHub issue</a> and <a href="https://gist.github.com/ruzickap/3258c16015d35495811a65b17626b94a">this GitHub Gist</a>.</p>
]]></content>
        </item>
        
        <item>
            <title>Prometheus: snmp_exporter and OpenBSD</title>
            <link>https://yetiops.net/posts/openbsd-snmp-exporter/</link>
            <pubDate>Tue, 28 Jan 2020 19:53:20 +0000</pubDate>
            
            <guid>https://yetiops.net/posts/openbsd-snmp-exporter/</guid>
            <description>In a previous post, I showed how to run the Prometheus node_exporter on a number of different operating systems, including OpenBSD.
Many OpenBSD installs are used as, or to replace, network appliances (e.g. peering routers, firewalls, VPN concentrators). Traditionally, you would monitor networking equipment using SNMP.
OpenBSDs snmpd(8) can expose a number of metrics that cover carp(4), pf(4), relayd(8) and more.
Prometheus and SNMP The snmp_exporter is used so that Prometheus can monitor devices via SNMP.</description>
            <content type="html"><![CDATA[<p>In a <a href="/posts/prometheus-monitoring-other-os">previous post</a>, I showed how to run the Prometheus <code>node_exporter</code> on a number of different operating systems, including OpenBSD.</p>
<p>Many OpenBSD installs are used as, or to replace, network appliances (e.g. peering routers, firewalls, VPN concentrators). Traditionally, you would monitor networking equipment using SNMP.</p>
<p>OpenBSDs <code>snmpd(8)</code> can expose a number of metrics that cover <code>carp(4)</code>, <code>pf(4)</code>, <code>relayd(8)</code> and more.</p>
<h2 id="prometheus-and-snmp">Prometheus and SNMP</h2>
<p>The <code>snmp_exporter</code> is used so that Prometheus can monitor devices via SNMP. It functions as a &ldquo;proxy&rdquo;, with Prometheus targeting the <code>snmp_exporter</code>, informing it what devices to retrieve SNMP-based metrics from.</p>
<p>The <code>snmp_exporter</code> will then run an SNMP request on OIDs defined in a <strong>module</strong>. Within each <strong>module</strong>, you can define a small or large number of OIDs to check. It could be a mixture of multiple MIBs, or OIDs from a single MIB.</p>
<h2 id="the-snmp_exporter-generator">The <code>snmp_exporter</code> generator</h2>
<p>The configuration that the <code>snmp_exporter</code> uses looks something like the below example: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">if_mib</span>:
  <span style="color:#66d9ef">auth</span>:
    <span style="color:#66d9ef">community</span>: <span style="color:#75715e">### SNMP COMMUNITY ### </span>
  <span style="color:#66d9ef">walk</span>:
  - <span style="color:#ae81ff">1.3.6.1.2.1.2</span>
  - <span style="color:#ae81ff">1.3.6.1.2.1.31.1.1</span>
  <span style="color:#66d9ef">get</span>:
  - <span style="color:#ae81ff">1.3.6.1.2.1.1.3.0</span>
  <span style="color:#66d9ef">metrics</span>:
  - <span style="color:#66d9ef">name</span>: sysUpTime
    <span style="color:#66d9ef">oid</span>: <span style="color:#ae81ff">1.3.6.1.2.1.1.3</span>
    <span style="color:#66d9ef">type</span>: gauge
    <span style="color:#66d9ef">help</span>: The time (in hundredths of a second) since the network management portion
      of the system was last re-initialized. - <span style="color:#ae81ff">1.3.6.1.2.1.1.3</span>
  - <span style="color:#66d9ef">name</span>: ifNumber
    <span style="color:#66d9ef">oid</span>: <span style="color:#ae81ff">1.3.6.1.2.1.2.1</span>
    <span style="color:#66d9ef">type</span>: gauge
    <span style="color:#66d9ef">help</span>: The number of network interfaces (regardless of their current state) present
      on this system. - <span style="color:#ae81ff">1.3.6.1.2.1.2.1</span>
[...]
<span style="color:#66d9ef">openbsd_pf</span>:
  <span style="color:#66d9ef">walk</span>:
  - <span style="color:#ae81ff">1.3.6.1.4.1.30155.1.8.128</span>
  <span style="color:#66d9ef">get</span>:
  - <span style="color:#ae81ff">1.3.6.1.4.1.30155.1.1.1.0</span>
  - <span style="color:#ae81ff">1.3.6.1.4.1.30155.1.1.2.0</span>
  - <span style="color:#ae81ff">1.3.6.1.4.1.30155.1.1.3.0</span>
  - <span style="color:#ae81ff">1.3.6.1.4.1.30155.1.1.4.0</span>
  - <span style="color:#ae81ff">1.3.6.1.4.1.30155.1.3.1.0</span>
  - <span style="color:#ae81ff">1.3.6.1.4.1.30155.1.3.2.0</span>
  - <span style="color:#ae81ff">1.3.6.1.4.1.30155.1.3.3.0</span>
  - <span style="color:#ae81ff">1.3.6.1.4.1.30155.1.3.4.0</span>
  <span style="color:#66d9ef">metrics</span>:
  - <span style="color:#66d9ef">name</span>: pfRunning
    <span style="color:#66d9ef">oid</span>: <span style="color:#ae81ff">1.3.6.1.4.1.30155.1.1.1</span>
    <span style="color:#66d9ef">type</span>: gauge
    <span style="color:#66d9ef">help</span>: Indicates whether pf is enabled or not. - <span style="color:#ae81ff">1.3.6.1.4.1.30155.1.1.1</span>
    <span style="color:#66d9ef">enum_values</span>:
      <span style="color:#66d9ef">1</span>: <span style="color:#e6db74">&#34;true&#34;</span>
      <span style="color:#66d9ef">2</span>: <span style="color:#e6db74">&#34;false&#34;</span>
[...]
</code></pre></div><p>The problem is, even for the most basic of MIBs and configurations, this could be thousands of lines long. For example, my configuration contains configuration for the basic <strong>IF-MIB</strong> (for general network statistics from a number of different kinds of devices), my MikroTik router, my Unifi APs and for gathering statistics from OpenBSD. How many lines of configuration does this entail?</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ cat snmp.yml | wc -l
<span style="color:#ae81ff">7963</span>
</code></pre></div><p>This is because each potential OID is contained in the configuration file, the type of metric we expect to return, and a useful description of what the OID is for. If lots of OIDs are being referenced, the size of the configuration will grow.</p>
<p>Thankfully, there is a tool called the <strong>generator</strong>, which takes a basic YAML configuration, detailing the OIDs, module name, authentication details, what SNMP version is to be used, and more.</p>
<p>For example, to generate configuration for an <strong>IF-MIB</strong> based module, you would use something like the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">modules</span>:
  <span style="color:#75715e"># Default IF-MIB interfaces table with ifIndex.</span>
  <span style="color:#66d9ef">if_mib</span>:
    <span style="color:#66d9ef">auth</span>:
      <span style="color:#66d9ef">community</span>: <span style="color:#75715e">### SNMP-COMMUNITY ###</span>
    <span style="color:#66d9ef">walk</span>: [sysUpTime, interfaces, ifXTable]
    <span style="color:#66d9ef">lookups</span>:
      - <span style="color:#66d9ef">source_indexes</span>: [ifIndex]
        <span style="color:#66d9ef">lookup</span>: ifAlias
      - <span style="color:#66d9ef">source_indexes</span>: [ifIndex]
        <span style="color:#66d9ef">lookup</span>: ifDescr
      - <span style="color:#66d9ef">source_indexes</span>: [ifIndex]
        <span style="color:#75715e"># Use OID to avoid conflict with Netscaler NS-ROOT-MIB.</span>
        <span style="color:#66d9ef">lookup</span>: <span style="color:#ae81ff">1.3.6.1.2.1.31.1.1.1.1</span> <span style="color:#75715e"># ifName</span>
    <span style="color:#66d9ef">overrides</span>:
      <span style="color:#66d9ef">ifAlias</span>:
        <span style="color:#66d9ef">ignore</span>: <span style="color:#66d9ef">true</span> <span style="color:#75715e"># Lookup metric</span>
      <span style="color:#66d9ef">ifDescr</span>:
        <span style="color:#66d9ef">ignore</span>: <span style="color:#66d9ef">true</span> <span style="color:#75715e"># Lookup metric</span>
      <span style="color:#66d9ef">ifName</span>:
        <span style="color:#66d9ef">ignore</span>: <span style="color:#66d9ef">true</span> <span style="color:#75715e"># Lookup metric</span>
      <span style="color:#66d9ef">ifType</span>:
        <span style="color:#66d9ef">type</span>: EnumAsInfo
</code></pre></div><p>The above comes from the <a href="https://github.com/prometheus/snmp_exporter/tree/master/generator">snmp_exporter repository</a>.</p>
<p>Full instructions on how to generate the configuration are available in the repository above, but as a quick summary: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">## Assuming you have a Golang environment set up</span>
$ go get github.com/prometheus/snmp_exporter/generator
$ cp $GOPATH/bin/generator /usr/local/bin/
$ mkdir mibs
$ cd mibs

<span style="color:#75715e">## Place your SNMP MIBs in the MIBs directory</span>
$ cd ..
$ export MIBDIRS<span style="color:#f92672">=</span>mibs
$ generator generate
</code></pre></div><p>You will now find an <code>snmp.yml</code> configuration within the directory you ran then <strong>generator</strong> from.</p>
<h2 id="using-the-configuration">Using the configuration</h2>
<p>To use the generated <code>snmp.yml</code>, place it in a directory of your choice (I use <code>/etc/prometheus</code>), and reference it in your <code>snmp_exporter</code> command, e.g.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ /usr/local/bin/snmp_exporter --config.file<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/etc/prometheus/snmp.yml&#34;</span>
</code></pre></div><p>To get hold of <code>snmp_exporter</code>, go to the <a href="https://github.com/prometheus/snmp_exporter/releases">release</a> page, download the correct release for your OS and architecture, and place it in a directory in your <code>$PATH</code> (I prefer <code>/usr/local/bin</code>).</p>
<h2 id="example-openbsd-configuration">Example OpenBSD configuration</h2>
<p>I am using <code>snmp_exporter</code> with OpenBSD to pull stats for <code>pf(4)</code>. This includes the number of states, state inserts and removals, and other useful information.</p>
<p>On OpenBSD itself, the <code>snmpd(8)</code> configuration (<code>/etc/snmpd.conf</code>) looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">## $OpenBSD: snmpd.conf,v 1.1 2014/07/11 21:20:10 deraadt Exp $

listen_addr=&#34;192.168.10.250&#34;

## Restrict daemon to listen on localhost only
listen on $listen_addr

## Specify a number of trap receivers
##trap receiver nms.localdomain.local

## Adjust the local system information
system contact &#34;Stuart&#34;
system description &#34;bastion-01&#34;
system location &#34;Home&#34;
system services 74

## Enable SNMPv3 USM with authentication, encryption and two defined users
seclevel enc
user &#34;$SNMPv3-USER&#34; authkey &#34;$SNMP-AUTH-KEY&#34; enc aes enckey &#34;$SNMP-ENCRYPTION-KEY&#34;
</code></pre></div><p>The <code>generator.yml</code> configuration looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">modules</span>:
  <span style="color:#66d9ef">openbsd_pf</span>:
    <span style="color:#66d9ef">walk</span>:
      - pfRunning
      - pfRuntime
      - pfDebug
      - pfHostid
      - pfStateCount
      - pfStateSearches
      - pfStateInserts
      - pfStateRemovals
      - pfIfTable
      - pfIfDescr
      - pfIfType
      - pfIfIndex
    <span style="color:#66d9ef">version</span>: <span style="color:#ae81ff">3</span>
    <span style="color:#66d9ef">lookups</span>:
    - <span style="color:#66d9ef">source_indexes</span>: [pfIfIndex]
      <span style="color:#66d9ef">lookup</span>: pfIfDescr
      <span style="color:#66d9ef">drop_source_indexes</span>: <span style="color:#66d9ef">false</span>
    <span style="color:#66d9ef">auth</span>:
      <span style="color:#66d9ef">username</span>: $SNMPv3-USER
      <span style="color:#66d9ef">security_level</span>: authPriv
      <span style="color:#66d9ef">password</span>: <span style="color:#e6db74">&#34;$SNMP-AUTH-KEY&#34;</span>
      <span style="color:#66d9ef">auth_protocol</span>: SHA
      <span style="color:#66d9ef">priv_protocol</span>: AES
      <span style="color:#66d9ef">priv_password</span>: <span style="color:#e6db74">&#34;$SNMP-ENCRYPTION-KEY&#34;</span>
</code></pre></div><p>Note that I&rsquo;m using SNMPv3 here. This means I need to supply authentication and encryption keys, as well as what authentication and encryption protocols are in use. By default, <code>snmpd(8)</code> uses <strong>SHA-1</strong> as it&rsquo;s hashing algorithm for authentication. If you specify <strong>MD5</strong>, the <code>snmp_exporter</code> will not be able to run SNMP queries against the OpenBSD machine.</p>
<p>The OIDs I have chosen are the ones that I deemed most useful to my use case.</p>
<p>I have also added a <strong>lookup</strong> section, which will add the <code>pf(4)</code> interface name to any label that already contains the <code>pfIfIndex</code>. This means that most of the metrics will also return the name of the interface they are relevant to.</p>
<p>Again, run the following to generate the configuration</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ mkdir mibs
$ cd mibs
$ wget $MIB-URL
$ cd ..
$ generator generate
</code></pre></div><h3 id="where-do-i-get-the-mibs-from">Where do I get the MIBs from?</h3>
<p>For OpenBSD, you can get hold of the MIBs from <a href="https://github.com/openbsd/src/tree/master/share/snmp">here</a>.</p>
<p>A number of operating systems also come with their own utilities for retrieving SNMP MIBs (e.g. Debian&rsquo;s <code>snmp-mibs-downloader</code>).</p>
<p>Also, if you follow the instructions on the <code>generator</code> section for the <code>snmp_exporter</code>, it will download a number of MIBs that cover everything in the example <code>snmp.yml</code> file provided in the repository.</p>
<h3 id="using-the-openbsd-configuration">Using the OpenBSD configuration</h3>
<p>As shown previously, you would use the configuration like so: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ /usr/local/bin/snmp_exporter --config.file<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/etc/prometheus/snmp.yml&#34;</span>
</code></pre></div><p>You can then reference this as a target in your Prometheus configuration file: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  - <span style="color:#66d9ef">job_name</span>: openbsd_pf_snmp
    <span style="color:#66d9ef">scrape_interval</span>: 180s
    <span style="color:#66d9ef">metrics_path</span>: /snmp
    <span style="color:#66d9ef">params</span>:
      <span style="color:#66d9ef">module</span>: [openbsd_pf]
    <span style="color:#66d9ef">relabel_configs</span>:
     - <span style="color:#66d9ef">source_labels</span>: [__address__]
       <span style="color:#66d9ef">target_label</span>: __param_target
     - <span style="color:#66d9ef">source_labels</span>: [__param_target]
       <span style="color:#66d9ef">target_label</span>: instance
     - <span style="color:#66d9ef">target_label</span>: __address__
       <span style="color:#66d9ef">replacement</span>: <span style="color:#ae81ff">127.0.0.1</span>:<span style="color:#ae81ff">9116</span>   
    <span style="color:#66d9ef">static_configs</span>:
      - <span style="color:#66d9ef">targets</span>:
        - $IP-OF-OPENBSD-INSTANCE 
</code></pre></div><p>One point to note is that using the default Prometheus scrape intervals (usually around 15s) can generate a large amount of SNMP requests that can overwhelm some machines.</p>
<p>I have raised the scrape interval to <code>180s</code> (3 minutes) in the example above, to avoid placing unnecessary load on the machines. Play around with this value until you find a level you are happy with, taking into account the longer the scrape interval, the less granularity you will have on your metrics.</p>
<h2 id="in-action">In action</h2>
<p>Below are some Grafana graphs showing the kind of stats you can get out of using <code>snmp_exporter</code> with OpenBSD</p>
<p>Query: <code>pfStateCount</code>
<img src="/img/snmpexporter/pfstatecount.png" alt="pfStateCount"></p>
<p>Query: <code>rate(pfStateInserts[30m])</code>
<img src="/img/snmpexporter/pfstateinserts.png" alt="pfStateInserts"></p>
<p>Query: <code>rate(pfStateRemovals[30m])</code>
<img src="/img/snmpexporter/pfstateremovals.png" alt="pfStateRemovals"></p>
<p>Query: <code>rate(pfStateSearches[30m])</code>
<img src="/img/snmpexporter/pfstatesearches.png" alt="pfStateSearches"></p>
<p>In the last, I also added a Single Stat panel that uses the <code>pfRunning</code> query, with some value mappings (<code>1</code> to <code>Running</code>, <code>0</code> to <code>Not Running</code>)</p>
<p>To see all the available metrics, you can run something like <code>curl http://###IP-OF-SNMP-EXPORTER###:9116/snmp?target=###IP-OF-OPENBSD-MACHINE###&amp;module=openbsd_pf</code>, where you&rsquo;ll see an output like the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text"># HELP pfDebug Indicates the debug level that pf is running at. - 1.3.6.1.4.1.30155.1.1.3
# TYPE pfDebug gauge
pfDebug 3
# HELP pfHostid The (unique) host id of the machine running pf. - 1.3.6.1.4.1.30155.1.1.4
# TYPE pfHostid gauge
pfHostid{pfHostid=&#34;0xcd2bf746&#34;} 1
# HELP pfIfDescr The name of the interface. - 1.3.6.1.4.1.30155.1.8.128.1.2
# TYPE pfIfDescr gauge
pfIfDescr{pfIfDescr=&#34;all&#34;,pfIfIndex=&#34;1&#34;} 1
pfIfDescr{pfIfDescr=&#34;carp&#34;,pfIfIndex=&#34;2&#34;} 1
pfIfDescr{pfIfDescr=&#34;egress&#34;,pfIfIndex=&#34;3&#34;} 1
pfIfDescr{pfIfDescr=&#34;enc&#34;,pfIfIndex=&#34;4&#34;} 1
pfIfDescr{pfIfDescr=&#34;enc0&#34;,pfIfIndex=&#34;5&#34;} 1
pfIfDescr{pfIfDescr=&#34;lo&#34;,pfIfIndex=&#34;6&#34;} 1
pfIfDescr{pfIfDescr=&#34;lo0&#34;,pfIfIndex=&#34;7&#34;} 1
pfIfDescr{pfIfDescr=&#34;pflog&#34;,pfIfIndex=&#34;8&#34;} 1
pfIfDescr{pfIfDescr=&#34;pflog0&#34;,pfIfIndex=&#34;9&#34;} 1
pfIfDescr{pfIfDescr=&#34;vio0&#34;,pfIfIndex=&#34;10&#34;} 1
pfIfDescr{pfIfDescr=&#34;vio1&#34;,pfIfIndex=&#34;11&#34;} 1
# HELP pfIfIn4BlockBytes The number of incoming IPv4 bytes blocked. - 1.3.6.1.4.1.30155.1.8.128.1.9
# TYPE pfIfIn4BlockBytes counter
pfIfIn4BlockBytes{pfIfDescr=&#34;all&#34;,pfIfIndex=&#34;1&#34;} 0
pfIfIn4BlockBytes{pfIfDescr=&#34;carp&#34;,pfIfIndex=&#34;2&#34;} 0
pfIfIn4BlockBytes{pfIfDescr=&#34;egress&#34;,pfIfIndex=&#34;3&#34;} 0
[...]
</code></pre></div><p>The machine I am running this against does not usually experience high traffic levels. It functions bastion host rather than a dedicated firewall, so the statistics are not particularly large.</p>
<p>If you are running this against dedicated firewalls, the stats you can retrieve will be invaluable.</p>
<h2 id="why-use-snmp_exporter-over-other-tools">Why use <code>snmp_exporter</code> over other tools?</h2>
<p>The <code>snmp_exporter</code> can give you great insight into your infrastructure. However it can be quite daunting, especially when you first look at the <code>snmp.yml</code> configuration that the Generator creates.</p>
<p>If you are coming from monitor systems like LibreNMS, Solarwinds or other Network-focussed monitoring systems, you will have probably been spoiled by how easy it is to add devices (often just adding the hostname/IP address, and the relevant SNMP community/authentication details).</p>
<p>However it is also worth nothing that with the aforementioned monitoring systems, a lot of work has gone into the discovery mechanisms when adding new devices.</p>
<p>Also, especially with commercial/closed-source systems, getting new devices and/or new MIBs added can take time, or may never happen.</p>
<p>For example, at a previous workplace, our monitoring system was a closed-source offering. Despite numerous attempts to get CheckPoint Virtual Systems (VSX) working with it, the vendor never added the capabilities, or the relevant MIBs to be able to query customer Virtual Systems. If we had used something like Prometheus and <code>snmp_exporter</code>, we could have just generated the relevant <code>snmp.yml</code> file from the CheckPoint MIBs, and started monitoring them straight away.</p>
<p>The flexibility in being able define the OIDs you are interested in, as well as being able to use practically any MIB in existence, are why I would consider using the <code>snmp_exporter</code> over other tools.</p>
<h2 id="summary">Summary</h2>
<p>I am hoping this article helps people get to grips with using SNMP checks with Prometheus, and specifically with OpenBSD&rsquo;s <code>pf(4)</code>.</p>
<p>The approach shown can be used with just about any MIBs you can get hold of, from Cisco Wireless LAN Controllers to Raritan PDUs.</p>
]]></content>
        </item>
        
        <item>
            <title>OpenBSD: High-Availability Firewalling</title>
            <link>https://yetiops.net/posts/openbsd-firewall-ha/</link>
            <pubDate>Tue, 21 Jan 2020 11:40:33 +0000</pubDate>
            
            <guid>https://yetiops.net/posts/openbsd-firewall-ha/</guid>
            <description>While most posts on this site usually concern Linux, I have a bit of a soft spot for OpenBSD.
OpenBSD is an operating system from the Unix lineage, started in Bell Labs many years ago, eventually giving rise to the Berkeley Software Distribution (BSD). The most known versions of BSD are NetBSD (who focus on portability, running on pretty much any hardware), FreeBSD (who focus on covering as many purposes as possible) and OpenBSD (who focus on security, sometimes at the expense of performance).</description>
            <content type="html"><![CDATA[<p>While most posts on this site usually concern Linux, I have a bit of a soft spot for OpenBSD.</p>
<p>OpenBSD is an operating system from the Unix lineage, started in Bell Labs many years ago, eventually giving rise to the Berkeley Software Distribution (BSD). The most known versions of BSD are NetBSD (who focus on portability, running on pretty much any hardware), FreeBSD (who focus on covering as many purposes as possible) and OpenBSD (who focus on security, sometimes at the expense of performance).</p>
<p>OpenBSD has (in my opinion) amazing documentation, and with a combination of their <code>man</code> pages and example configuration (e.g. <code>/etc/examples</code> is full of configuration examples), you can get most of the included daemons working.</p>
<p>OpenBSD also provides quite a comprehensive suite of software already bundled in, everything from routing daemons, web servers, firewalling, mail servers and much more.</p>
<p>In this post, I&rsquo;m going to cover how you can setup a pair of OpenBSD machines to run as a highly-available firewall pair.</p>
<h2 id="diagram">Diagram</h2>
<p><img src="/img/openbsd-fw.png" alt="OpenBSD Firewalling"></p>
<p>In the diagram, we have a client machine (running on Debian 10), a server (<strong>net-01</strong>) and two OpenBSD virtual machines.</p>
<h2 id="installing-openbsd">Installing OpenBSD</h2>
<p>To set up OpenBSD, you will be presented with a text-based installer with a number of options to choose from. I won&rsquo;t go into details here, as the OpenBSD <a href="https://www.openbsd.org/faq/faq4.html">FAQ</a> can cover every aspect of it. Summing up my chosen options though: -</p>
<ul>
<li>Three main networks
<ul>
<li>hvn2 - The link between the firewalls (running in the <code>169.254.0.0/31</code> range)</li>
<li>hvn3 - The link to the server (running in the <code>192.0.2.0/24</code> range)</li>
<li>hvn4 - The link to the client (running in the <code>192.168.99.0/24</code> range)</li>
</ul>
</li>
<li>Only installing a minimal file set, with no <strong>X11</strong> dependencies, or the text-based games file sets</li>
<li>Automatic partitioning</li>
</ul>
<h2 id="setting-up-the-the-firewalls">Setting up the the firewalls</h2>
<p>To get the firewalls running, we need to setup the following daemons: -</p>
<ul>
<li><code>pf(4)</code> - PF, or Packet Filter, the OpenBSD packet filtering daemon</li>
<li><code>carp(4)</code> - CARP, or Common Address Redundancy Protocol, for highly available IPs (like VRRP or Cisco&rsquo;s HSRP)</li>
</ul>
<p>We also need to do the following: -</p>
<ul>
<li>Add the correct IPs to each interface</li>
<li>Allow IP forwarding through the firewalls</li>
<li>Enable the <code>pfsync(4)</code> device, to sync state between the firewalls</li>
<li>Create the <code>carp(4)</code> interfaces</li>
<li>Define the <code>pf(4)</code> ruleset</li>
</ul>
<h3 id="setting-up-the-ip-interfaces">Setting up the IP interfaces</h3>
<p>To define an interface in OpenBSD, you create a file called <code>hostname.$INTERFACE-NAME</code>, replacing <strong>$INTERFACE-NAME</strong> with your interfaces (e.g. <strong>hvn2</strong>, <strong>vio1</strong>), in the <code>/etc</code> directory.</p>
<p>To demonstrate this, see below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">## hostname.hvn2</span>
inet 169.254.0.1 255.255.255.254

<span style="color:#75715e">## hostname.hvn3</span>
inet 192.0.2.10 255.255.255.0

<span style="color:#75715e">## hostname.hvn4</span>
inet 192.168.99.1 255.255.255.0
</code></pre></div><p>You can also supply the network mask in hexadecimal format (e.g. a <code>/24</code> would be <code>0xffffff00</code>). To enable the interfaces, you can use <code>doas sh /etc/netstart hvn2</code>.</p>
<p>You should now be able to see the interfaces: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ifconfig hvn2
hvn2: flags<span style="color:#f92672">=</span>8a43&lt;UP,BROADCAST,RUNNING,ALLMULTI,SIMPLEX,MULTICAST&gt; mtu <span style="color:#ae81ff">1500</span>
        lladdr 00:15:5d:f4:91:ef
        index <span style="color:#ae81ff">3</span> priority <span style="color:#ae81ff">0</span> llprio <span style="color:#ae81ff">3</span>
        media: Ethernet manual
        status: active
        inet 169.254.0.1 netmask 0xfffffffe

$ ifconfig hvn3 
hvn3: flags<span style="color:#f92672">=</span>8b43&lt;UP,BROADCAST,RUNNING,PROMISC,ALLMULTI,SIMPLEX,MULTICAST&gt; mtu <span style="color:#ae81ff">1500</span>
        lladdr 00:15:5d:f4:91:f3
        index <span style="color:#ae81ff">4</span> priority <span style="color:#ae81ff">0</span> llprio <span style="color:#ae81ff">3</span>
        media: Ethernet manual
        status: active
        inet 192.0.2.10 netmask 0xffffff00 broadcast 192.0.2.255

$ ifconfig hvn4 
hvn4: flags<span style="color:#f92672">=</span>8b43&lt;UP,BROADCAST,RUNNING,PROMISC,ALLMULTI,SIMPLEX,MULTICAST&gt; mtu <span style="color:#ae81ff">1500</span>
        lladdr 00:15:5d:f4:91:f4
        index <span style="color:#ae81ff">5</span> priority <span style="color:#ae81ff">0</span> llprio <span style="color:#ae81ff">3</span>
        media: Ethernet manual
        status: active
        inet 192.168.99.1 netmask 0xffffff00 broadcast 192.168.99.255

</code></pre></div><p>You&rsquo;ll need to ensure that each firewall has a different IP on it&rsquo;s interfaces (i.e. using <code>192.168.99.2/24</code> on <strong>hvn4</strong> on the second firewall) so that they do not conflict.</p>
<h3 id="wait-what-is-doas">Wait, what is <code>doas</code>?</h3>
<p><code>doas(1)</code> is a privilege escalation utility, used to temporarily elevate a user&rsquo;s privileges, to allow them to perform commands that they typically cannot as a normal user. For those familiar with <code>sudo</code>, it is very similar. The reason for its existence is a smaller codebase than <code>sudo</code> to maintain, and also the configuration syntax is quite simple.</p>
<p>This is a good example of OpenBSD choosing tools that they can maintain and audit easily, rather than using external tools (or maintaining their own fork). For other examples, see the choice of their own <code>httpd(8)</code> over NGINX, or <code>bgpd(4)</code> over Quagga/BIRD.</p>
<p>To get a simple version of <code>doas(1)</code> running, you can look at the <code>/etc/examples</code> directory for a sample <code>doas.conf</code> file. Copy this to <code>/etc/doas.conf</code>, and edit it to your tastes.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">## $OpenBSD: doas.conf,v 1.1 2016/09/03 11:58:32 pirofti Exp $</span>
<span style="color:#75715e">## Configuration sample file for doas(1).</span>
<span style="color:#75715e">## See doas.conf(5) for syntax and examples.</span>

<span style="color:#75715e">## Non-exhaustive list of variables needed to build release(8) and ports(7)</span>
<span style="color:#75715e">##permit nopass setenv { \</span>
<span style="color:#75715e">##    FTPMODE PKG_CACHE PKG_PATH SM_PATH SSH_AUTH_SOCK \</span>
<span style="color:#75715e">##    DESTDIR DISTDIR FETCH_CMD FLAVOR GROUP MAKE MAKECONF \</span>
<span style="color:#75715e">##    MULTI_PACKAGES NOMAN OKAY_FILES OWNER PKG_DBDIR \</span>
<span style="color:#75715e">##    PKG_DESTDIR PKG_TMPDIR PORTSDIR RELEASEDIR SHARED_ONLY \</span>
<span style="color:#75715e">##    SUBPACKAGE WRKOBJDIR SUDO_PORT_V1 } :wsrc</span>

<span style="color:#75715e">## Allow wheel by default</span>
permit keepenv :wheel
</code></pre></div><p>The above does nothing more than allow the <strong>wheel</strong> group to use <code>doas(1)</code>. I add my user into the <strong>wheel</strong> group, and from then on I can use <code>doas(1)</code> to my hearts content.</p>
<h3 id="setting-up-pfsync4">Setting up <code>pfsync(4)</code></h3>
<p>For truly highly available firewalls, you need something that will synchronize the state between them. This is because if you just failover to another machine, the second machine would have no idea of what existing TCP connections are active, what UDP connections have been seen recently (e.g. VoIP calls) and how many hits each rule has seen (important for monitoring).</p>
<p>OpenBSD provides the <code>pfsync(4)</code> utility, which does exactly that. To quote the <code>man</code> page for <code>pfsync(4)</code></p>
<blockquote>
<p>If configured with a physical synchronisation interface, pfsync will also send state changes out on that interface, and insert state changes received on that interface from other systems into the state table.</p>
</blockquote>
<p>So if you have this running on an interface dedicated between two machines, they will share state information.</p>
<p>To make this work, you configure another <code>hostname.$INTERFACE-NAME</code> file, except this time it will be <code>hostname.pfsync</code>. The contents of the file will look something like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ cat /etc/hostname.pfsync0                                                                                         
up syncdev hvn2
</code></pre></div><p>This is the same on both firewalls. If you run <code>tcpdump(8)</code> on <strong>hvn2</strong> while the firewall is in operation, you will see numerous state messages passing between the two firewalls (try running <code>tcpdump -i hvn2</code> during operation to see).</p>
<h3 id="allow-ip-forwarding">Allow IP Forwarding</h3>
<p>IP Forwarding is configured within the <code>/etc/sysctl.conf</code> file as such: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ cat /etc/sysctl.conf
net.inet.ip.forwarding<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</code></pre></div><p>Without this, traffic will not work through the firewalls, so make sure you enable this!</p>
<h3 id="add-carp4-for-redundancy">Add <code>carp(4)</code> for redundancy</h3>
<p>Unless you use devices that support routing protocols, or can failover between multiple default gateways, you&rsquo;ll need to target an IP that can reside on both firewalls. In the Cisco world, they use <strong>HSRP</strong> (Hot Standby Router Protocol). The RFC version of <strong>HSRP</strong> is known as <strong>VRRP</strong> (Virtual Router Redundancy Protocol). However, despite <strong>VRRP</strong> being an RFC standard, it is patent-encumbered, meaning some elements of it are covered by patents (owned by Cisco).</p>
<p>Due to this, the OpenBSD developers implemented <strong>CARP</strong> instead. <strong>CARP</strong> does not infringe on any Cisco patents, although it does still use a few traits of VRRP (e.g. the IP Protocol number and Virtual MAC Addresses). This does mean that if you have <strong>VRRP</strong> and <strong>CARP</strong> on the same network, you will need to use different VRRP Group IDs and CARP Virtual Host IDs for them to coexist.</p>
<p>Again, to setup a <code>carp(4)</code> interface on the primary firewall, you use the <code>hostname.$INTERFACE-NAME</code> files. To demonstrate, see below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">## hostname.carp1</span>
inet 192.0.2.1 255.255.255.0 192.0.2.255 vhid <span style="color:#ae81ff">1</span> carpdev hvn3 pass hvn3pass

<span style="color:#75715e">## hostname carp2</span>
inet 192.168.99.254 255.255.255.0 192.168.99.255 vhid <span style="color:#ae81ff">1</span> carpdev hvn4 pass hvn4pass 
</code></pre></div><p>The syntax is as follows: -</p>
<p><code>inet $VIRTUAL-IP $SUBNET-MASK $BROADCAST-ADDRESS vhid $CARP-VIRTUAL-HOST-ID carpdev $PHYSICAL-INTERFACE pass $PASSWORD</code></p>
<p>You need to ensure the Virtual IP is in the same network as the interface it is running on. The Physical Interface parameter is used to tie the <strong>CARP</strong> virtual interface to a physical interface.</p>
<p>Again, use <code>doas /etc/netstart carpN</code> to start the interfaces: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ifconfig carp1
carp1: flags<span style="color:#f92672">=</span>8843&lt;UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST&gt; mtu <span style="color:#ae81ff">1500</span>
        lladdr 00:00:5e:00:01:01
        index <span style="color:#ae81ff">8</span> priority <span style="color:#ae81ff">15</span> llprio <span style="color:#ae81ff">3</span>
        carp: MASTER carpdev hvn3 vhid <span style="color:#ae81ff">1</span> advbase <span style="color:#ae81ff">1</span> advskew <span style="color:#ae81ff">0</span>
        groups: carp
        status: master
        inet 192.0.2.1 netmask 0xffffff00 broadcast 192.0.2.255

$ ifconfig carp2
carp2: flags<span style="color:#f92672">=</span>8843&lt;UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST&gt; mtu <span style="color:#ae81ff">1500</span>
        lladdr 00:00:5e:00:01:01
        index <span style="color:#ae81ff">9</span> priority <span style="color:#ae81ff">15</span> llprio <span style="color:#ae81ff">3</span>
        carp: MASTER carpdev hvn4 vhid <span style="color:#ae81ff">1</span> advbase <span style="color:#ae81ff">1</span> advskew <span style="color:#ae81ff">0</span>
        groups: carp
        status: master
        inet 192.168.99.254 netmask 0xffffff00 broadcast 192.168.99.255
</code></pre></div><p>Notice in the above that it shows the status of the interface (in this case, the <strong>MASTER</strong>).</p>
<p>The syntax for the <code>hostname</code> files on the secondary firewall differs slightly: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">## hostname.carp1</span>
inet 192.0.2.1 255.255.255.0 192.0.2.255 vhid <span style="color:#ae81ff">1</span> carpdev hvn3 pass hvn3pass advskew <span style="color:#ae81ff">128</span>

<span style="color:#75715e">## hostname.carp2</span>
inet 192.168.99.254 255.255.255.0 192.168.99.255 vhid <span style="color:#ae81ff">1</span> carpdev hvn4 pass hvn4pass advskew <span style="color:#ae81ff">128</span>
</code></pre></div><p>The primary difference is the <code>advskew</code> parameter. This is used to set a &ldquo;priority&rdquo; or &ldquo;weight&rdquo; on the interface, higher being less preferred.</p>
<p>The interface output on the secondary firewall looks like this: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ifconfig carp1
carp1: flags<span style="color:#f92672">=</span>8843&lt;UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST&gt; mtu <span style="color:#ae81ff">1500</span>
        lladdr 00:00:5e:00:01:01
        index <span style="color:#ae81ff">8</span> priority <span style="color:#ae81ff">15</span> llprio <span style="color:#ae81ff">3</span>
        carp: BACKUP carpdev hvn3 vhid <span style="color:#ae81ff">1</span> advbase <span style="color:#ae81ff">1</span> advskew <span style="color:#ae81ff">128</span>
        groups: carp
        status: backup
        inet 192.0.2.1 netmask 0xffffff00 broadcast 192.0.2.255

$ ifconfig carp2 
carp2: flags<span style="color:#f92672">=</span>8843&lt;UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST&gt; mtu <span style="color:#ae81ff">1500</span>
        lladdr 00:00:5e:00:01:01
        index <span style="color:#ae81ff">9</span> priority <span style="color:#ae81ff">15</span> llprio <span style="color:#ae81ff">3</span>
        carp: BACKUP carpdev hvn4 vhid <span style="color:#ae81ff">1</span> advbase <span style="color:#ae81ff">1</span> advskew <span style="color:#ae81ff">128</span>
        groups: carp
        status: backup
        inet 192.168.99.254 netmask 0xffffff00 broadcast 192.168.99.255

</code></pre></div><p>As you can see, the status of the interfaces are <strong>backup</strong>, meaning they will not respond on the Virtual IP, unless the <strong>master</strong> goes away (and they are promoted to <strong>master</strong>).</p>
<h3 id="pf">PF</h3>
<p>After all of the above, you&rsquo;ll now be ready to define a Packet Filter ruleset. There are a number of amazing resources on this, including <a href="https://nostarch.com/pf3">The Book of PF</a> by <a href="https://www.peterhansteen.com/">Peter Hansteen</a>, <a href="https://home.nuug.no/~peter/pftutorial/#1">The PF Tutorial</a> (also by Peter Hansteen, along with <a href="https://stucchi.ch">Massimiliano Stucchi</a>) and the <a href="https://www.openbsd.org/faq/pf/">OpenBSD PF FAQ</a>.</p>
<p>PF is very flexible, in that you can apply macros, arrays/lists of IPs, and refer to them with as a single variable with your ruleset.</p>
<p>I have set up a very small ruleset on the two firewalls: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">oas cat /etc/pf.conf 
doas <span style="color:#f92672">(</span>stuh84@openbsdfw-01.stubob.lab<span style="color:#f92672">)</span> password: 
<span style="color:#75715e">##       $OpenBSD: pf.conf,v 1.55 2017/12/03 20:40:04 sthen Exp $</span>
<span style="color:#75715e">##</span>
<span style="color:#75715e">## See pf.conf(5) and /etc/examples/pf.conf</span>
ext_if<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;hvn3&#34;</span>
carp_ext_if<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;carp1&#34;</span>
carp_int_if<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;carp2&#34;</span>
int_if<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;hvn4&#34;</span>
sync_if<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;hvn2&#34;</span>
internet_if<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;hvn0&#34;</span>

set skip on lo

block <span style="color:#66d9ef">return</span>    <span style="color:#75715e"># block stateless traffic</span>
pass            <span style="color:#75715e"># establish keep-state</span>

<span style="color:#75715e">## By default, do not permit remote connections to X11</span>
block <span style="color:#66d9ef">return</span> in on ! lo0 proto tcp to port 6000:6010

<span style="color:#75715e">## Port build user does not need network</span>
block <span style="color:#66d9ef">return</span> out log proto <span style="color:#f92672">{</span>tcp udp<span style="color:#f92672">}</span> user _pbuild

<span style="color:#75715e">## Allow PFSync</span>
pass quick on $sync_if proto pfsync 

<span style="color:#75715e">## Allow CARP on in and out ints</span>
pass quick on <span style="color:#f92672">{</span> $ext_if $int_if <span style="color:#f92672">}</span> proto carp

<span style="color:#75715e">## NAT to net-01</span>
pass out on $ext_if inet from $int_if:network to any nat-to $carp_ext_if
pass out on $ext_if inet from $carp_int_if:network to any nat-to $carp_ext_if

<span style="color:#75715e">## NAT to the Internet</span>
pass out on $internet_if inet from $int_if:network to any nat-to $internet_if
pass out on $internet_if inet from $carp_int_if:network to any nat-to $internet_if
</code></pre></div><p>A lot of the rules are from the examples, but I have also included a few extra rules: -</p>
<ul>
<li>Allowing PFSync on the <strong>$sync_if</strong> interface (in my case, <strong>hvn2</strong>)</li>
<li>Allowing CARP on the interfaces they reside on (in my case, <strong>hvn3</strong> and <strong>hvn4</strong>)</li>
<li>Any traffic that comes in from the internal interfaces (<strong>hvn4</strong> or <strong>carp2</strong>) going out of the interface facing <strong>net-01</strong>, to NAT it to the IP of the the <strong>$carp_ext_if</strong> (in my case, <strong>carp1</strong>)</li>
<li>Any traffic that comes in from the internal interfaces (<strong>hvn4</strong> or <strong>carp2</strong>) going out to the internet (the <strong>hvn0</strong> interface), to NAT it to the IP of the the <strong>$internet_if</strong> (in my case, also <strong>hvn0</strong>).</li>
</ul>
<p>The reason for not running <code>carp(4)</code> on the internet interface is due to these being virtual machines without a shared Internet-facing network, or shared IP space.</p>
<p>To apply this ruleset, you can use <code>doas pfctl -f /etc/pf.conf</code>. To enable <code>pf(4)</code> to run at boot, use <code>rcctl enable pf</code> (<code>rcctl(8)</code> being a tool to control running daemons).</p>
<p>One important point to note is that <code>pfsync(4)</code> does not synchronize configuration changes, only firewall state. You will need to manage keeping the rulesets synchronized separately using your chosen configuration management and deployment tool (e.g. <strong>rsync</strong>, <strong>ansible</strong>, <strong>rdist(1)</strong> or otherwise).</p>
<h2 id="verification">Verification</h2>
<p>To show the active ruleset, you can run <code>doas pfctl -s rules</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ doas pfctl -s rules
doas <span style="color:#f92672">(</span>stuh84@openbsdfw-01.stubob.lab<span style="color:#f92672">)</span> password: 
block <span style="color:#66d9ef">return</span> all
pass all flags S/SA
block <span style="color:#66d9ef">return</span> in on ! lo0 proto tcp from any to any port 6000:6010
block <span style="color:#66d9ef">return</span> out log proto tcp all user <span style="color:#f92672">=</span> <span style="color:#ae81ff">55</span>
block <span style="color:#66d9ef">return</span> out log proto udp all user <span style="color:#f92672">=</span> <span style="color:#ae81ff">55</span>
pass quick on hvn3 proto carp all
pass quick on hvn4 proto carp all
pass quick on hvn2 proto pfsync all
pass out on hvn3 inet from 192.168.99.0/24 to any flags S/SA nat-to 192.0.2.1
pass out on hvn0 inet from 192.168.99.0/24 to any flags S/SA nat-to 172.18.22.197
</code></pre></div><p>To view active statistics, you can run <code>doas pfctl -vsr</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">doas pfctl -vsr
doas <span style="color:#f92672">(</span>stuh84@openbsdfw-01.stubob.lab<span style="color:#f92672">)</span> password: 
block <span style="color:#66d9ef">return</span> all
  <span style="color:#f92672">[</span> Evaluations: <span style="color:#ae81ff">1169</span>      Packets: <span style="color:#ae81ff">0</span>         Bytes: <span style="color:#ae81ff">0</span>           States: <span style="color:#ae81ff">0</span>     <span style="color:#f92672">]</span>
  <span style="color:#f92672">[</span> Inserted: uid <span style="color:#ae81ff">0</span> pid <span style="color:#ae81ff">45241</span> State Creations: <span style="color:#ae81ff">0</span>     <span style="color:#f92672">]</span>
pass all flags S/SA
  <span style="color:#f92672">[</span> Evaluations: <span style="color:#ae81ff">1169</span>      Packets: <span style="color:#ae81ff">11829</span>     Bytes: <span style="color:#ae81ff">3762548</span>     States: <span style="color:#ae81ff">11</span>    <span style="color:#f92672">]</span>
  <span style="color:#f92672">[</span> Inserted: uid <span style="color:#ae81ff">0</span> pid <span style="color:#ae81ff">45241</span> State Creations: <span style="color:#ae81ff">6645</span>  <span style="color:#f92672">]</span>
block <span style="color:#66d9ef">return</span> in on ! lo0 proto tcp from any to any port 6000:6010
  <span style="color:#f92672">[</span> Evaluations: <span style="color:#ae81ff">1169</span>      Packets: <span style="color:#ae81ff">0</span>         Bytes: <span style="color:#ae81ff">0</span>           States: <span style="color:#ae81ff">0</span>     <span style="color:#f92672">]</span>
  <span style="color:#f92672">[</span> Inserted: uid <span style="color:#ae81ff">0</span> pid <span style="color:#ae81ff">45241</span> State Creations: <span style="color:#ae81ff">0</span>     <span style="color:#f92672">]</span>
block <span style="color:#66d9ef">return</span> out log proto tcp all user <span style="color:#f92672">=</span> <span style="color:#ae81ff">55</span>
  <span style="color:#f92672">[</span> Evaluations: <span style="color:#ae81ff">145</span>       Packets: <span style="color:#ae81ff">0</span>         Bytes: <span style="color:#ae81ff">0</span>           States: <span style="color:#ae81ff">0</span>     <span style="color:#f92672">]</span>
  <span style="color:#f92672">[</span> Inserted: uid <span style="color:#ae81ff">0</span> pid <span style="color:#ae81ff">45241</span> State Creations: <span style="color:#ae81ff">0</span>     <span style="color:#f92672">]</span>
block <span style="color:#66d9ef">return</span> out log proto udp all user <span style="color:#f92672">=</span> <span style="color:#ae81ff">55</span>
  <span style="color:#f92672">[</span> Evaluations: <span style="color:#ae81ff">1168</span>      Packets: <span style="color:#ae81ff">0</span>         Bytes: <span style="color:#ae81ff">0</span>           States: <span style="color:#ae81ff">0</span>     <span style="color:#f92672">]</span>
  <span style="color:#f92672">[</span> Inserted: uid <span style="color:#ae81ff">0</span> pid <span style="color:#ae81ff">45241</span> State Creations: <span style="color:#ae81ff">0</span>     <span style="color:#f92672">]</span>
pass quick on hvn3 proto carp all
  <span style="color:#f92672">[</span> Evaluations: <span style="color:#ae81ff">1169</span>      Packets: <span style="color:#ae81ff">23243</span>     Bytes: <span style="color:#ae81ff">1301608</span>     States: <span style="color:#ae81ff">2</span>     <span style="color:#f92672">]</span>
  <span style="color:#f92672">[</span> Inserted: uid <span style="color:#ae81ff">0</span> pid <span style="color:#ae81ff">45241</span> State Creations: <span style="color:#ae81ff">2</span>     <span style="color:#f92672">]</span>
pass quick on hvn4 proto carp all
  <span style="color:#f92672">[</span> Evaluations: <span style="color:#ae81ff">1161</span>      Packets: <span style="color:#ae81ff">23243</span>     Bytes: <span style="color:#ae81ff">1301608</span>     States: <span style="color:#ae81ff">2</span>     <span style="color:#f92672">]</span>
  <span style="color:#f92672">[</span> Inserted: uid <span style="color:#ae81ff">0</span> pid <span style="color:#ae81ff">45241</span> State Creations: <span style="color:#ae81ff">2</span>     <span style="color:#f92672">]</span>
pass quick on hvn2 proto pfsync all
  <span style="color:#f92672">[</span> Evaluations: <span style="color:#ae81ff">1167</span>      Packets: <span style="color:#ae81ff">0</span>         Bytes: <span style="color:#ae81ff">0</span>           States: <span style="color:#ae81ff">0</span>     <span style="color:#f92672">]</span>
  <span style="color:#f92672">[</span> Inserted: uid <span style="color:#ae81ff">0</span> pid <span style="color:#ae81ff">45241</span> State Creations: <span style="color:#ae81ff">0</span>     <span style="color:#f92672">]</span>
pass out on hvn3 inet from 192.168.99.0/24 to any flags S/SA nat-to 192.0.2.1
  <span style="color:#f92672">[</span> Evaluations: <span style="color:#ae81ff">1167</span>      Packets: <span style="color:#ae81ff">0</span>         Bytes: <span style="color:#ae81ff">0</span>           States: <span style="color:#ae81ff">0</span>     <span style="color:#f92672">]</span>
  <span style="color:#f92672">[</span> Inserted: uid <span style="color:#ae81ff">0</span> pid <span style="color:#ae81ff">45241</span> State Creations: <span style="color:#ae81ff">0</span>     <span style="color:#f92672">]</span>
pass out on hvn0 inet from 192.168.99.0/24 to any flags S/SA nat-to 172.18.22.197
  <span style="color:#f92672">[</span> Evaluations: <span style="color:#ae81ff">1160</span>      Packets: <span style="color:#ae81ff">34</span>        Bytes: <span style="color:#ae81ff">2676</span>        States: <span style="color:#ae81ff">0</span>     <span style="color:#f92672">]</span>
  <span style="color:#f92672">[</span> Inserted: uid <span style="color:#ae81ff">0</span> pid <span style="color:#ae81ff">45241</span> State Creations: <span style="color:#ae81ff">26</span>    <span style="color:#f92672">]</span>
</code></pre></div><p>To view the current stats, you can run <code>doas pfctl -ss</code></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ doas pfctl -ss  
doas <span style="color:#f92672">(</span>stuh84@openbsdfw-01.stubob.lab<span style="color:#f92672">)</span> password: 
all pfsync 169.254.0.1 -&gt; 224.0.0.240       SINGLE:NO_TRAFFIC
all pfsync 224.0.0.240 &lt;- 169.254.0.0       NO_TRAFFIC:SINGLE
all tcp 192.168.241.7:22 &lt;- 192.168.241.254:56448       ESTABLISHED:ESTABLISHED
all tcp 192.0.2.10:17742 -&gt; 192.0.2.20:179       ESTABLISHED:ESTABLISHED
all tcp 192.168.241.6:22 &lt;- 192.168.241.254:54168       ESTABLISHED:ESTABLISHED
all carp 192.0.2.10 -&gt; 224.0.0.18       SINGLE:NO_TRAFFIC
all carp 192.168.99.1 -&gt; 224.0.0.18       SINGLE:NO_TRAFFIC
all carp 224.0.0.18 &lt;- 192.0.2.10       NO_TRAFFIC:SINGLE
all carp 224.0.0.18 &lt;- 192.168.99.1       NO_TRAFFIC:SINGLE
all udp ff12::8384<span style="color:#f92672">[</span>21027<span style="color:#f92672">]</span> &lt;- fe80::adfb:c525:9a:71b5<span style="color:#f92672">[</span>56280<span style="color:#f92672">]</span>       NO_TRAFFIC:SINGLE
all udp 192.168.241.255:21027 &lt;- 192.168.241.1:64395       NO_TRAFFIC:SINGLE
all udp 172.18.22.207:21027 &lt;- 172.18.22.193:64395       NO_TRAFFIC:SINGLE
all tcp 192.0.2.11:179 &lt;- 192.0.2.20:57424       TIME_WAIT:TIME_WAIT
all udp 239.255.255.250:1900 &lt;- 192.168.241.1:58267       NO_TRAFFIC:SINGLE
all udp 239.255.255.250:1900 &lt;- 172.18.22.193:58273       NO_TRAFFIC:SINGLE
all udp 172.18.22.207:138 &lt;- 172.18.22.193:138       NO_TRAFFIC:SINGLE
all udp 192.168.241.255:138 &lt;- 192.168.241.1:138       NO_TRAFFIC:SINGLE
all udp 172.18.22.198:45469 -&gt; 193.150.34.2:123       MULTIPLE:SINGLE
all icmp 192.0.2.1:8 &lt;- 192.0.2.20:23408       0:0
all udp 172.18.22.197:15594 -&gt; 162.159.200.1:123       MULTIPLE:SINGLE
</code></pre></div><h3 id="state-replication-and-failover">State replication and failover</h3>
<p>To show the state replication, I have set a ping going from <strong>client-01</strong> to <strong>net-01</strong>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ping 192.0.2.20
PING 192.0.2.20 <span style="color:#f92672">(</span>192.0.2.20<span style="color:#f92672">)</span> 56<span style="color:#f92672">(</span>84<span style="color:#f92672">)</span> bytes of data.
<span style="color:#ae81ff">64</span> bytes from 192.0.2.20: icmp_seq<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span> ttl<span style="color:#f92672">=</span><span style="color:#ae81ff">63</span> time<span style="color:#f92672">=</span>0.750 ms
<span style="color:#ae81ff">64</span> bytes from 192.0.2.20: icmp_seq<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span> ttl<span style="color:#f92672">=</span><span style="color:#ae81ff">63</span> time<span style="color:#f92672">=</span>0.685 ms
</code></pre></div><p>Viewing the state table on <code>openbsdfw-01</code> for this, I can see: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ doas pfctl -ss | grep -i icmp 
doas <span style="color:#f92672">(</span>stuh84@openbsdfw-01.stubob.lab<span style="color:#f92672">)</span> password: 
all icmp 192.0.2.20:8 &lt;- 192.168.99.50:5463       0:0
all icmp 192.0.2.1:63341 <span style="color:#f92672">(</span>192.168.99.50:5463<span style="color:#f92672">)</span> -&gt; 192.0.2.20:8       0:0

$ doas tcpdump -i hvn4 icmp 
doas <span style="color:#f92672">(</span>stuh84@openbsdfw-01.stubob.lab<span style="color:#f92672">)</span> password: 
tcpdump: listening on hvn4, link-type EN10MB
13:16:46.519326 192.168.99.50 &gt; 192.0.2.20: icmp: echo request <span style="color:#f92672">(</span>DF<span style="color:#f92672">)</span>
13:16:46.519931 192.0.2.20 &gt; 192.168.99.50: icmp: echo reply

</code></pre></div><p>What do we see on <code>openbsdfw-02</code>?</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ doas pfctl -ss | grep -i icmp
doas <span style="color:#f92672">(</span>stuh84@openbsdfw-02.stubob.lab<span style="color:#f92672">)</span> password: 
all icmp 192.0.2.20:8 &lt;- 192.168.99.50:5463       0:0
all icmp 192.0.2.1:63341 <span style="color:#f92672">(</span>192.168.99.50:5463<span style="color:#f92672">)</span> -&gt; 192.0.2.20:8       0:0

$ doas tcpdump -i hvn4 icmp
doas <span style="color:#f92672">(</span>stuh84@openbsdfw-02.stubob.lab<span style="color:#f92672">)</span> password: 
tcpdump: listening on hvn4, link-type EN10MB


</code></pre></div><p>So as you can see, we actually have the state (i.e. the ICMP packets going through), but the packets themselves do not traverse the secondary firewall.</p>
<p>What about if we shutdown the primary firewall?</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ doas shutdown -h now
doas <span style="color:#f92672">(</span>stuh84@openbsdfw-01.stubob.lab<span style="color:#f92672">)</span> password: 
Shutdown NOW!
shutdown: <span style="color:#f92672">[</span>pid 52871<span style="color:#f92672">]</span>

*** FINAL System shutdown message from stuh84@openbsdfw-01.stubob.lab ***    
System going down IMMEDIATELY
</code></pre></div><p>Did the traffic failover?</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ doas tcpdump -i hvn4 icmp 
doas <span style="color:#f92672">(</span>stuh84@openbsdfw-02.stubob.lab<span style="color:#f92672">)</span> password: 
tcpdump: listening on hvn4, link-type EN10MB
13:20:02.105437 192.168.99.50 &gt; 192.0.2.20: icmp: echo request <span style="color:#f92672">(</span>DF<span style="color:#f92672">)</span>
13:20:02.105823 192.0.2.20 &gt; 192.168.99.50: icmp: echo reply
</code></pre></div><p>Looks like it did! Now lets check a few details on the secondary firewall: -</p>
<p><strong>CARP Status</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ifconfig carp1
carp1: flags<span style="color:#f92672">=</span>8843&lt;UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST&gt; mtu <span style="color:#ae81ff">1500</span>
	lladdr 00:00:5e:00:01:01
	index <span style="color:#ae81ff">8</span> priority <span style="color:#ae81ff">15</span> llprio <span style="color:#ae81ff">3</span>
	carp: MASTER carpdev hvn3 vhid <span style="color:#ae81ff">1</span> advbase <span style="color:#ae81ff">1</span> advskew <span style="color:#ae81ff">128</span>
	groups: carp
	status: master
	inet 192.0.2.1 netmask 0xffffff00 broadcast 192.0.2.255

$ ifconfig carp2 
carp2: flags<span style="color:#f92672">=</span>8843&lt;UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST&gt; mtu <span style="color:#ae81ff">1500</span>
	lladdr 00:00:5e:00:01:01
	index <span style="color:#ae81ff">9</span> priority <span style="color:#ae81ff">15</span> llprio <span style="color:#ae81ff">3</span>
	carp: MASTER carpdev hvn4 vhid <span style="color:#ae81ff">1</span> advbase <span style="color:#ae81ff">1</span> advskew <span style="color:#ae81ff">128</span>
	groups: carp
	status: master
	inet 192.168.99.254 netmask 0xffffff00 broadcast 192.168.99.255
</code></pre></div><p>Looks like we failed over, what about the firewall statistics and states?</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ doas pfctl -ss | grep -i icmp
doas <span style="color:#f92672">(</span>stuh84@openbsdfw-02.stubob.lab<span style="color:#f92672">)</span> password: 
all icmp 192.0.2.20:8 &lt;- 192.168.99.50:5463       0:0
all icmp 192.0.2.1:63341 <span style="color:#f92672">(</span>192.168.99.50:5463<span style="color:#f92672">)</span> -&gt; 192.0.2.20:8       0:0

$ doas pfctl -vsr               
doas <span style="color:#f92672">(</span>stuh84@openbsdfw-02.stubob.lab<span style="color:#f92672">)</span> password: 
block <span style="color:#66d9ef">return</span> all
  <span style="color:#f92672">[</span> Evaluations: <span style="color:#ae81ff">1445</span>      Packets: <span style="color:#ae81ff">0</span>         Bytes: <span style="color:#ae81ff">0</span>           States: <span style="color:#ae81ff">0</span>     <span style="color:#f92672">]</span>
  <span style="color:#f92672">[</span> Inserted: uid <span style="color:#ae81ff">0</span> pid <span style="color:#ae81ff">97984</span> State Creations: <span style="color:#ae81ff">0</span>     <span style="color:#f92672">]</span>
pass all flags S/SA
  <span style="color:#f92672">[</span> Evaluations: <span style="color:#ae81ff">1445</span>      Packets: <span style="color:#ae81ff">14751</span>     Bytes: <span style="color:#ae81ff">4848439</span>     States: <span style="color:#ae81ff">16</span>    <span style="color:#f92672">]</span>
  <span style="color:#f92672">[</span> Inserted: uid <span style="color:#ae81ff">0</span> pid <span style="color:#ae81ff">97984</span> State Creations: <span style="color:#ae81ff">6888</span>  <span style="color:#f92672">]</span>
block <span style="color:#66d9ef">return</span> in on ! lo0 proto tcp from any to any port 6000:6010
  <span style="color:#f92672">[</span> Evaluations: <span style="color:#ae81ff">1445</span>      Packets: <span style="color:#ae81ff">0</span>         Bytes: <span style="color:#ae81ff">0</span>           States: <span style="color:#ae81ff">0</span>     <span style="color:#f92672">]</span>
  <span style="color:#f92672">[</span> Inserted: uid <span style="color:#ae81ff">0</span> pid <span style="color:#ae81ff">97984</span> State Creations: <span style="color:#ae81ff">0</span>     <span style="color:#f92672">]</span>
block <span style="color:#66d9ef">return</span> out log proto tcp all user <span style="color:#f92672">=</span> <span style="color:#ae81ff">55</span>
  <span style="color:#f92672">[</span> Evaluations: <span style="color:#ae81ff">391</span>       Packets: <span style="color:#ae81ff">0</span>         Bytes: <span style="color:#ae81ff">0</span>           States: <span style="color:#ae81ff">0</span>     <span style="color:#f92672">]</span>
  <span style="color:#f92672">[</span> Inserted: uid <span style="color:#ae81ff">0</span> pid <span style="color:#ae81ff">97984</span> State Creations: <span style="color:#ae81ff">0</span>     <span style="color:#f92672">]</span>
block <span style="color:#66d9ef">return</span> out log proto udp all user <span style="color:#f92672">=</span> <span style="color:#ae81ff">55</span>
  <span style="color:#f92672">[</span> Evaluations: <span style="color:#ae81ff">1259</span>      Packets: <span style="color:#ae81ff">0</span>         Bytes: <span style="color:#ae81ff">0</span>           States: <span style="color:#ae81ff">0</span>     <span style="color:#f92672">]</span>
  <span style="color:#f92672">[</span> Inserted: uid <span style="color:#ae81ff">0</span> pid <span style="color:#ae81ff">97984</span> State Creations: <span style="color:#ae81ff">0</span>     <span style="color:#f92672">]</span>
pass quick on hvn3 proto carp all
  <span style="color:#f92672">[</span> Evaluations: <span style="color:#ae81ff">1445</span>      Packets: <span style="color:#ae81ff">47713</span>     Bytes: <span style="color:#ae81ff">2671928</span>     States: <span style="color:#ae81ff">1</span>     <span style="color:#f92672">]</span>
  <span style="color:#f92672">[</span> Inserted: uid <span style="color:#ae81ff">0</span> pid <span style="color:#ae81ff">97984</span> State Creations: <span style="color:#ae81ff">4</span>     <span style="color:#f92672">]</span>
pass quick on hvn4 proto carp all
  <span style="color:#f92672">[</span> Evaluations: <span style="color:#ae81ff">1257</span>      Packets: <span style="color:#ae81ff">47713</span>     Bytes: <span style="color:#ae81ff">2671928</span>     States: <span style="color:#ae81ff">1</span>     <span style="color:#f92672">]</span>
  <span style="color:#f92672">[</span> Inserted: uid <span style="color:#ae81ff">0</span> pid <span style="color:#ae81ff">97984</span> State Creations: <span style="color:#ae81ff">4</span>     <span style="color:#f92672">]</span>
pass quick on hvn2 proto pfsync all
  <span style="color:#f92672">[</span> Evaluations: <span style="color:#ae81ff">1441</span>      Packets: <span style="color:#ae81ff">0</span>         Bytes: <span style="color:#ae81ff">0</span>           States: <span style="color:#ae81ff">0</span>     <span style="color:#f92672">]</span>
  <span style="color:#f92672">[</span> Inserted: uid <span style="color:#ae81ff">0</span> pid <span style="color:#ae81ff">97984</span> State Creations: <span style="color:#ae81ff">0</span>     <span style="color:#f92672">]</span>
pass out on hvn3 inet from 192.168.99.0/24 to any flags S/SA nat-to 192.0.2.1
  <span style="color:#f92672">[</span> Evaluations: <span style="color:#ae81ff">1441</span>      Packets: <span style="color:#ae81ff">434</span>       Bytes: <span style="color:#ae81ff">36456</span>       States: <span style="color:#ae81ff">1</span>     <span style="color:#f92672">]</span>
  <span style="color:#f92672">[</span> Inserted: uid <span style="color:#ae81ff">0</span> pid <span style="color:#ae81ff">97984</span> State Creations: <span style="color:#ae81ff">3</span>     <span style="color:#f92672">]</span>
pass out on hvn0 inet from 192.168.99.0/24 to any flags S/SA nat-to 172.18.22.198
  <span style="color:#f92672">[</span> Evaluations: <span style="color:#ae81ff">1255</span>      Packets: <span style="color:#ae81ff">842</span>       Bytes: <span style="color:#ae81ff">807671</span>      States: <span style="color:#ae81ff">0</span>     <span style="color:#f92672">]</span>
  <span style="color:#f92672">[</span> Inserted: uid <span style="color:#ae81ff">0</span> pid <span style="color:#ae81ff">97984</span> State Creations: <span style="color:#ae81ff">27</span>    <span style="color:#f92672">]</span>
</code></pre></div><p>There we go, we&rsquo;re passing traffic. How many packets did we lose during all of this?</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#f92672">[</span>...<span style="color:#f92672">]</span>
--- 192.0.2.20 ping statistics ---
<span style="color:#ae81ff">500</span> packets transmitted, <span style="color:#ae81ff">500</span> received, 0% packet loss, time 675ms
rtt min/avg/max/mdev <span style="color:#f92672">=</span> 0.424/0.856/3.406/0.270 ms
</code></pre></div><p>Well then, not a single packet lost. Looks like we saw high delay on one of them, but no packets actually dropped. If we set the timeout within the <strong>ping</strong> utility, I&rsquo;m sure we would have seen a few drop. By default the time between packets is 1 second, which is more than enough to see in most practical situations that many people wouldn&rsquo;t notice the failover.</p>
<h2 id="summary">Summary</h2>
<p>OpenBSD is a very versatile operating system, with very sane defaults, and a high security focus. It also ships with a lot of software that means you may never need to add anything from external repositories/ports (everything from <code>bgpd(4)</code>, <code>snmpd(8)</code>, <code>relayd(8)</code> and much more) to have a fully functional system, especially for security and network functions.</p>
<p>I am just a beginner when it comes to OpenBSD. If you want to know more about OpenBSD, I would urge you to visit: -</p>
<ul>
<li><a href="https://www.openbsd.org/faq/">The OpenBSD FAQ</a></li>
<li><a href="https://www.openbsd.org/events.html">The OpenBSD events page</a> - Contains a number of talks and slides about OpenBSD</li>
<li><a href="https://reykfloeter.com/">Reyk Floeter</a> - Notable OpenBSD developer</li>
<li><a href="https://www.peterhansteen.com/">Peter Hansteen</a> - Notable OpenBSD developer</li>
<li><a href="https://bsd.network/@phessler">Peter Hessler</a> - Notable OpenBSD developer</li>
<li><a href="http://henningbrauer.com/">Henning Brauer</a> - Notable OpenBSD developer</li>
<li><a href="https://bsd.network/">bsd.network</a> - A Mastodon instance ran by Peter Hessler, with many users who contribute to OpenBSD</li>
<li><a href="https://openbsd.amsterdam/">OpenBSD Amsterdam</a> - OpenBSD VPSs running on the OpenBSD <code>vmd(8)</code> Virtualisation Daemon, ran by <a href="https://mischapeters.com/">Mischa Peters</a></li>
<li><a href="https://www.romanzolotarev.com/">Roman Zolotarev</a> - Roman is involved in the BSD community, providing great information for those new to the world of all the BSDs (not just OpenBSD)</li>
</ul>
<p>I&rsquo;m missing out on many more, but the above is a good place to start! I would highly recommend going through the Events page and watching some talks regarding OpenBSD.</p>
]]></content>
        </item>
        
        <item>
            <title>Prometheus: Consul Service Discovery for blackbox and snmp exporter</title>
            <link>https://yetiops.net/posts/prometheus-consul-blackbox/</link>
            <pubDate>Thu, 16 Jan 2020 12:38:20 +0000</pubDate>
            
            <guid>https://yetiops.net/posts/prometheus-consul-blackbox/</guid>
            <description>In a previous post I covered how to use Consul for service discovery of standard exporters, allowing Prometheus to automatically discover what services to monitor.
However, this configuration didn&amp;rsquo;t cater to exporters like the snmp_exporter or blackbox_exporter.
What is interesting about both of the above is that rather than generating metrics for a local application, they are a proxy for other services. For example, you can use BlackBox exporter to do ICMP checks or HTTPS checks, without running an exporter on the services themselves.</description>
            <content type="html"><![CDATA[<p>In a previous <a href="/posts/prometheus-consul-node_exporter">post</a> I covered how to use Consul for service discovery of standard exporters, allowing Prometheus to automatically discover what services to monitor.</p>
<p>However, this configuration didn&rsquo;t cater to exporters like the <a href="https://github.com/prometheus/snmp_exporter">snmp_exporter</a> or <a href="https://github.com/prometheus/blackbox_exporter">blackbox_exporter</a>.</p>
<p>What is interesting about both of the above is that rather than generating metrics for a local application, they are a proxy for other services. For example, you can use BlackBox exporter to do ICMP checks or HTTPS checks, without running an exporter on the services themselves. This means you could do checks on say, Cloudflare&rsquo;s <code>1.1.1.1</code> service, without running an exporter within Cloudflare.</p>
<p>The standard configuration for the Consul Service Discovery within Prometheus didn&rsquo;t lend itself to this kind of service. Through some searching though, I came across a couple of useful resources pertaining to how to configure these kind of services (see the end of this page).</p>
<h2 id="relabelling">Relabelling</h2>
<p>Prometheus can use a concept called relabelling, where information gleaned from the service discovery mechanism can override defaults. For example, you could override the module name (useful for SNMP, as the SNMP exporter can have multiple different modules configured in one instance).</p>
<p>You can also use it to relabel the target of the requests, essentially meaning that we can add a <strong>tag</strong> to a service in Consul, and use it to override the target that Blackbox or SNMP exporters will attempt to make requests to.</p>
<h2 id="consul-service-configuration---blackbox_exporter">Consul Service configuration - blackbox_exporter</h2>
<p>The below is a service definition for a Blackbox exporter service: -</p>
<pre><code>{&quot;service&quot;:
  {&quot;name&quot;: &quot;icmp&quot;,
   &quot;tags&quot;: [
     &quot;icmp:192.168.10.3&quot;,
     &quot;prometheus-bb-icmp&quot;
     ]
  }
}
</code></pre><p>In the above, we have given the service two tags, the <code>prometheus-bb-icmp</code> tag (which can be used by Prometheus to match service) and the <code>icmp:192.168.0.3</code> tag. With a little bit of regex magic, we can extract the IP from the service, and override the target of an ICMP Blackbox exporter check.</p>
<p>The corresponding section in the Prometheus configuration is as such: -</p>
<pre><code>  - job_name: 'blackbox_consul'
    metrics_path: /probe
    params:
      module: [icmp_ipv4]
    consul_sd_configs:
      - server: '192.168.0.7:8500'
    relabel_configs:
      - source_labels: [__meta_consul_tags]
        regex: .*,prometheus-bb-icmp,.*
        action: keep
      - source_labels: [__meta_consul_tags]
        regex: .*,icmp:([^,]+),.*
        replacement: '${1}'
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: 127.0.0.1:9115
</code></pre><p>As you can see, we are matching the <strong>__meta_consul_tags</strong> label. Within this, we are matching everything after <code>icmp:</code>, and then pushing it to the <strong>target_label</strong> of <strong>__param_target</strong>. This is what overrides the target parameter, which Prometheus will tell the blackbox_exporter to use as a target.</p>
<h3 id="multiple-service-definitions-on-one-node">Multiple Service Definitions on one node</h3>
<p>If you want to check multiple targets (say, multiple external targets), you can construct a file containing multiple services, and allow Consul to pick this up. An example of this is below: -</p>
<pre><code>{&quot;services&quot;: [
  {&quot;id&quot;: &quot;icmp_cloudflare_dns&quot;,
   &quot;name&quot;: &quot;icmp&quot;,
   &quot;tags&quot;: [
      &quot;icmp:1.1.1.1&quot;,
      &quot;prometheus-bb-icmp&quot;
       ]
  },
  {&quot;id&quot;: &quot;icmp_router&quot;,
   &quot;name&quot;: &quot;icmp&quot;,
   &quot;tags&quot;: [
      &quot;icmp:192.168.0.1&quot;,
      &quot;prometheus-bb-icmp&quot;
     ]
  },
  {&quot;id&quot;: &quot;icmp_mdns-01&quot;,
   &quot;name&quot;: &quot;icmp&quot;,
   &quot;tags&quot;: [
      &quot;icmp:192.168.0.21&quot;,
      &quot;prometheus-bb-icmp&quot;
     ]
  },
[...]
</code></pre><p>The structure of this file is quite important (a list of JSON statements, not replicating the services keyword etc), and you also must specify unique IDs for the services. I personally generate this using Ansible, but it is up to you how you would go about this.</p>
<p>Pay attention to the keyword <strong>services</strong> rather than <strong>service</strong>. Without this, the configuration will not have the intended effect.</p>
<h2 id="consul-service-configuration---snmp_exporter">Consul Service configuration - snmp_exporter</h2>
<p>For the SNMP exporter, the configuration is very similar. As I did this one later in my quest, it also includes the module override, as well as target override. This means I do not need configuration for each different SNMP module.</p>
<p>I can use the same configuration in Prometheus for Unifi, MikroTik, and anything else, only needing to update the Consul service, and the <strong>snmp_exporter</strong> configuration itself if I had further modules down the line.</p>
<pre><code>{&quot;services&quot;: [
  {&quot;id&quot;: &quot;snmp_ifmib_meshuggah&quot;,
   &quot;name&quot;: &quot;snmp&quot;,
   &quot;tags&quot;: [
      &quot;snmp:192.168.0.7&quot;,
      &quot;module:if_mib&quot;,
      &quot;prometheus-bb-snmp&quot;
     ]
  },
  {&quot;id&quot;: &quot;snmp_mikrotik_router&quot;,
   &quot;name&quot;: &quot;snmp&quot;,
   &quot;tags&quot;: [
      &quot;snmp:192.168.0.1&quot;,
      &quot;module:mikrotik&quot;,
      &quot;prometheus-bb-snmp&quot;
     ]
  },
  {&quot;id&quot;: &quot;snmp_ubiquiti_officeap&quot;,
   &quot;name&quot;: &quot;snmp&quot;,
   &quot;tags&quot;: [
      &quot;snmp:192.168.0.248&quot;,
      &quot;module:ubiquiti_unifi&quot;,
      &quot;prometheus-bb-snmp&quot;
     ]
  },
[...]
</code></pre><p>As you can see, we have the same style tags as per the Blackbox Exporter check, but also we have the <code>module</code> tag as well. This specifies the Module to override the SNMP exporter configuration with.</p>
<p>The corresponding Prometheus configuration is below: -</p>
<pre><code>  - job_name: 'blackbox_consul_snmp'
    metrics_path: /snmp
    params:
      module: [ifmib_snmp]
    consul_sd_configs:
      - server: '192.168.0.7:8500'
    relabel_configs:
      - source_labels: [__meta_consul_tags]
        regex: .*,prometheus-bb-snmp,.*
        action: keep
      - source_labels: [__meta_consul_tags]
        regex: .*,module:([^,]+),.*
        replacement: '${1}'
        target_label: __param_module
      - source_labels: [__param_module]
        target_label: job
      - source_labels: [__meta_consul_tags]
        regex: .*,snmp:([^,]+),.*
        replacement: '${1}'
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: 127.0.0.1:9116
</code></pre><p>In the above, you can see that we have the <strong>target</strong> configuration, similar to the blackbox exporter configuration. However, we also match the <strong>module</strong> tag, saying anything after <code>module:</code> will override the target label <strong>__param_module</strong>.</p>
<p>We have a default module parameter of <strong>ifmib_snmp</strong>, which means that if nothing is specified, we&rsquo;ll default to using the <strong>ifmib_snmp</strong> module (which in my configuration means nothing more than using the standard SNMP ifmib MIB definitions for stats).</p>
<p>Going back to the service definition above, we have tags with <code>module:ubiquiti_unifi</code> and <code>module:mikrotik</code>. These will override the SNMP configuration module, meaning that for MikroTik hosts the query to the SNMP exporter will use <strong>mikrotik</strong> as the module parameter, and <strong>ubiquiti_unifi</strong> for any Ubiquiti equipment.</p>
<h2 id="targets-in-prometheus">Targets in Prometheus</h2>
<p>The resulting targets in Prometheus are discovered as such: -</p>
<h3 id="icmp-targets">ICMP targets</h3>
<p><img src="/img/prometheus/prometheus-consul-icmp.png" alt="Prometheus Consul Service Discovery with ICMP"></p>
<h3 id="snmp-targets">SNMP targets</h3>
<p><img src="/img/prometheus/prometheus-consul-snmp.png" alt="Prometheus Consul Service Discovery with SNMP"></p>
<h2 id="summary">Summary</h2>
<p>After doing all of this, my Prometheus configuration is now almost entirely fed by Consul. I have two small services I need to move (with the hold up being about trying to not commit secrets in version control).</p>
<p>The two main resources allowing me to refactor my configuration like this are <a href="https://github.com/prometheus/prometheus/issues/2377">this</a> issue on the Prometheus GitHub and the below talk from PromCon 2019 on migrating from Nagios to Prometheus: -</p>

<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe src="https://www.youtube.com/embed/xSPNYXbAhQE" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allowfullscreen title="YouTube Video"></iframe>
</div>

<p>The talk (and all the others from PromCon 2019) are highly recommended.</p>
]]></content>
        </item>
        
        <item>
            <title>Prometheus: Discover services with DNS</title>
            <link>https://yetiops.net/posts/prometheus-srv-discovery/</link>
            <pubDate>Tue, 10 Dec 2019 12:13:24 +0000</pubDate>
            
            <guid>https://yetiops.net/posts/prometheus-srv-discovery/</guid>
            <description>In a previous post I covered how to use Consul for service discovery, allowing Prometheus to automatically discover what services to monitor.
There are some cases where either setting up Consul (or similar) is not viable, or adds complexity that is not required. If you are already running your own DNS nameservers, you could make use of DNS SRV records.
Common DNS record types The most common DNS records are A, AAAA and PTR.</description>
            <content type="html"><![CDATA[<p>In a previous <a href="/posts/prometheus-consul-node_exporter">post</a> I covered how to use Consul for service discovery, allowing Prometheus to automatically discover what services to monitor.</p>
<p>There are some cases where either setting up Consul (or similar) is not viable, or adds complexity that is not required. If you are already running your own DNS nameservers, you could make use of DNS SRV records.</p>
<h2 id="common-dns-record-types">Common DNS record types</h2>
<p>The most common DNS records are A, AAAA and PTR. An A record is a simple &ldquo;name to IPv4&rdquo; mapping, e.g. <code>one.one.one.one</code> would become <code>1.1.1.1</code>. A AAAA record is the same, except for IPv6.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ dig A one.one.one.one

; &lt;&lt;&gt;&gt; DiG 9.14.7 &lt;&lt;&gt;&gt; A one.one.one.one
;; global options: +cmd
;; Got answer:
;; -&gt;&gt;HEADER<span style="color:#e6db74">&lt;&lt;- opcode: QUERY, status: NOERROR, id: 34576
</span><span style="color:#e6db74">;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">;; OPT PSEUDOSECTION:
</span><span style="color:#e6db74">; EDNS: version: 0, flags:; udp: 4000
</span><span style="color:#e6db74">; COOKIE: 19b10b10c5d45d10 (echoed)
</span><span style="color:#e6db74">;; QUESTION SECTION:
</span><span style="color:#e6db74">;one.one.one.one.		IN	A
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">;; ANSWER SECTION:
</span><span style="color:#e6db74">one.one.one.one.	176	IN	A	1.1.1.1
</span><span style="color:#e6db74">one.one.one.one.	176	IN	A	1.0.0.1
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">;; Query time: 8 msec
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">$ dig AAAA one.one.one.one
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">; &lt;&lt;&gt;&gt; DiG 9.14.7 &lt;&lt;&gt;&gt; AAAA one.one.one.one
</span><span style="color:#e6db74">;; global options: +cmd
</span><span style="color:#e6db74">;; Got answer:
</span><span style="color:#e6db74">;; -&gt;&gt;HEADER&lt;&lt;- opcode</span>: QUERY, status: NOERROR, id: <span style="color:#ae81ff">12686</span>
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: <span style="color:#ae81ff">1</span>

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: <span style="color:#ae81ff">4000</span>
; COOKIE: a91fb8e973aa5e78 <span style="color:#f92672">(</span>echoed<span style="color:#f92672">)</span>
;; QUESTION SECTION:
;one.one.one.one.		IN	AAAA

;; ANSWER SECTION:
one.one.one.one.	299	IN	AAAA	2606:4700:4700::1111
one.one.one.one.	299	IN	AAAA	2606:4700:4700::1001

;; Query time: <span style="color:#ae81ff">24</span> msec
</code></pre></div><p>A PTR record, or Pointer, is what provides reverse DNS. When you see IPs translated to a hostname (for example, in a traceroute), it is PTR records that are providing this. Some tools, like <code>host</code> automatically translate the IP address into the correct format for PTR records: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ host 1.1.1.1
1.1.1.1.in-addr.arpa domain name pointer one.one.one.one.
</code></pre></div><p>However other tools do not: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ dig PTR 1.1.1.1
; &lt;&lt;&gt;&gt; DiG 9.14.7 &lt;&lt;&gt;&gt; PTR 1.1.1.1
;; global options: +cmd
;; Got answer:
;; -&gt;&gt;HEADER<span style="color:#e6db74">&lt;&lt;- opco</span>de: QUERY, status: NXDOMAIN, id: <span style="color:#ae81ff">40153</span>
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: <span style="color:#ae81ff">1</span>

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: <span style="color:#ae81ff">4000</span>
; COOKIE: 4ed0f8ba650f2734 <span style="color:#f92672">(</span>echoed<span style="color:#f92672">)</span>
;; QUESTION SECTION:
;1.1.1.1.			IN	PTR

;; AUTHORITY SECTION:
.			773	IN	SOA	a.root-servers.net. nstld.verisign-grs.com. <span style="color:#ae81ff">2019121000</span> <span style="color:#ae81ff">1800</span> <span style="color:#ae81ff">900</span> <span style="color:#ae81ff">604800</span> <span style="color:#ae81ff">86400</span>

;; Query time: <span style="color:#ae81ff">1</span> msec
</code></pre></div><p>To use <code>dig</code> to check a PTR record, you need to supply the IP address in the following format: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">1.1.1.1 -&gt; 1.1.1.1.in-addr.arpa

$ dig 1.1.1.1.in-addr-arpa

; &lt;&lt;&gt;&gt; DiG 9.14.7 &lt;&lt;&gt;&gt; PTR 1.1.1.1.in-addr.arpa
;; global options: +cmd
;; Got answer:
;; -&gt;&gt;HEADER<span style="color:#e6db74">&lt;&lt;- opco</span>de: QUERY, status: NOERROR, id: <span style="color:#ae81ff">19361</span>
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: <span style="color:#ae81ff">1</span>

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: <span style="color:#ae81ff">4000</span>
; COOKIE: 2d9b763cb41a84ed <span style="color:#f92672">(</span>echoed<span style="color:#f92672">)</span>
;; QUESTION SECTION:
;1.1.1.1.in-addr.arpa.		IN	PTR

;; ANSWER SECTION:
1.1.1.1.in-addr.arpa.	248	IN	PTR	one.one.one.one.

;; Query time: <span style="color:#ae81ff">1</span> msec
</code></pre></div><p>The same is true for IPv6 records, except the format is much longer: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">2606:4700:4700::1111 -&gt; 1.1.1.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.7.4.0.0.7.4.6.0.6.2.ip6.arpa

$ dig PTR 1.1.1.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.7.4.0.0.7.4.6.0.6.2.ip6.arpa

; &lt;&lt;&gt;&gt; DiG 9.14.7 &lt;&lt;&gt;&gt; PTR 1.1.1.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.7.4.0.0.7.4.6.0.6.2.ip6.arpa
;; global options: +cmd
;; Got answer:
;; -&gt;&gt;HEADER<span style="color:#e6db74">&lt;&lt;- opco</span>de: QUERY, status: NOERROR, id: <span style="color:#ae81ff">2723</span>
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: <span style="color:#ae81ff">1</span>

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: <span style="color:#ae81ff">4000</span>
; COOKIE: 5762756121316ea0 <span style="color:#f92672">(</span>echoed<span style="color:#f92672">)</span>
;; QUESTION SECTION:
;1.1.1.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.7.4.0.0.7.4.6.0.6.2.ip6.arpa. IN PTR

;; ANSWER SECTION:
1.1.1.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.7.4.0.0.7.4.6.0.6.2.ip6.arpa. <span style="color:#ae81ff">165</span> IN PTR one.one.one.one.

;; Query time: <span style="color:#ae81ff">1</span> msec
</code></pre></div><h2 id="what-is-an-srv-record">What is an SRV record?</h2>
<p>Rather than just being a mapping from a hostname to an IP (e.g. A or AAAA), or the reverse (PTR), an SRV record contains hostnames, ports and the protocols (TCP/UDP). Common usage of this include SIP and Active Directory Domain Controller discovery.</p>
<p>If you try to join a Windows Domain with just the domain name (e.g. <code>example.com</code>), the SRV record is providing a list of Domain Controllers under a DNS SRV record for <code>example.com</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">dig SRV _ldap._tcp.dc._msdcs.example.com

; &lt;&lt;&gt;&gt; DiG 9.14.7 &lt;&lt;&gt;&gt; SRV _ldap._tcp.dc._msdcs.example.com
;; global options: +cmd
;; Got answer:
;; -&gt;&gt;HEADER<span style="color:#e6db74">&lt;&lt;- opco</span>de: QUERY, status: NOERROR, id: <span style="color:#ae81ff">54352</span>
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 5, AUTHORITY: 0, ADDITIONAL: <span style="color:#ae81ff">6</span>

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: <span style="color:#ae81ff">4000</span>
; COOKIE: 00d2c28406648fbf <span style="color:#f92672">(</span>echoed<span style="color:#f92672">)</span>
;; QUESTION SECTION:
;_ldap._tcp.dc._msdcs.example.com. IN SRV

;; ANSWER SECTION:
_ldap._tcp.dc._msdcs.example.com. <span style="color:#ae81ff">600</span> IN SRV <span style="color:#ae81ff">0</span> <span style="color:#ae81ff">100</span> <span style="color:#ae81ff">389</span> dc-01.example.com.
_ldap._tcp.dc._msdcs.example.com. <span style="color:#ae81ff">600</span> IN SRV <span style="color:#ae81ff">0</span> <span style="color:#ae81ff">100</span> <span style="color:#ae81ff">389</span> dc-02.example.com.
_ldap._tcp.dc._msdcs.example.com. <span style="color:#ae81ff">600</span> IN SRV <span style="color:#ae81ff">0</span> <span style="color:#ae81ff">100</span> <span style="color:#ae81ff">389</span> dc-03.example.com.

;; ADDITIONAL SECTION:
dc-01.example.com. 3600	IN	A  192.168.20.1
dc-02.example.com. 3600	IN	A  192.168.20.2
dc-03.example.com. 3600	IN	A  192.168.20.3
</code></pre></div><p>It is worth noting that SRV records point to A/AAAA records (see the <code>ADDITIONAL SECTION</code>), so they must be set up too.</p>
<p>What the above gives you is the protocol, the port and the hostname to reach Active Directory.</p>
<h2 id="how-can-prometheus-use-this">How can Prometheus use this?</h2>
<p>To monitor a host, Prometheus requires the IP/hostname, port and protocol. This is exactly what an SRV record exposes, and so can be leveraged for service discovery. The exact implementation is documented <a href="https://prometheus.io/docs/prometheus/latest/configuration/configuration/#dns_sd_config">here</a></p>
<h3 id="example-etcd">Example: ETCD</h3>
<p>ETCD (a distributed key/value store) can discover what other members are in the cluster using DNS SRV records (documentation (here)[https://github.com/etcd-io/etcd/blob/master/Documentation/op-guide/clustering.md]). Additionally, we can use the same SRV records to monitor the ETCD instances too.</p>
<p>An example of an ETCD SRV record is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ dig _etcd-client-ssl._tcp.staging.example.com

; &lt;&lt;&gt;&gt; DiG 9.14.7 &lt;&lt;&gt;&gt; SRV _etcd-client-ssl._tcp.staging.example.com.
;; global options: +cmd
;; Got answer:
;; -&gt;&gt;HEADER<span style="color:#e6db74">&lt;&lt;- opco</span>de: QUERY, status: NOERROR, id: <span style="color:#ae81ff">20086</span>
;; flags: qr rd ra; QUERY: 1, ANSWER: 5, AUTHORITY: 0, ADDITIONAL: <span style="color:#ae81ff">2</span>

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: <span style="color:#ae81ff">4000</span>
; COOKIE: db9a9582f9740d45 <span style="color:#f92672">(</span>echoed<span style="color:#f92672">)</span>
;; QUESTION SECTION:
;_etcd-client-ssl._tcp.staging.example.com. IN SRV

;; ANSWER SECTION:
_etcd-client-ssl._tcp.staging.example.com. <span style="color:#ae81ff">204</span> IN	SRV <span style="color:#ae81ff">10</span> <span style="color:#ae81ff">50</span> <span style="color:#ae81ff">2379</span> etcd-10-11-99-42.staging.example.com.
_etcd-client-ssl._tcp.staging.example.com. <span style="color:#ae81ff">204</span> IN	SRV <span style="color:#ae81ff">10</span> <span style="color:#ae81ff">50</span> <span style="color:#ae81ff">2379</span> etcd-10-11-160-216.staging.example.com.
_etcd-client-ssl._tcp.staging.example.com. <span style="color:#ae81ff">204</span> IN	SRV <span style="color:#ae81ff">10</span> <span style="color:#ae81ff">50</span> <span style="color:#ae81ff">2379</span> etcd-10-11-164-63.staging.example.com.
_etcd-client-ssl._tcp.staging.example.com. <span style="color:#ae81ff">204</span> IN	SRV <span style="color:#ae81ff">10</span> <span style="color:#ae81ff">50</span> <span style="color:#ae81ff">2379</span> etcd-10-11-46-92.staging.example.com.
_etcd-client-ssl._tcp.staging.example.com. <span style="color:#ae81ff">204</span> IN	SRV <span style="color:#ae81ff">10</span> <span style="color:#ae81ff">50</span> <span style="color:#ae81ff">2379</span> etcd-10-11-97-104.staging.example.com.

;; ADDITIONAL SECTION:
etcd-10-11-99-42.staging.example.com. <span style="color:#ae81ff">4</span> IN A 10.11.99.42

;; Query time: <span style="color:#ae81ff">26</span> msec
</code></pre></div><p>To make use of this within Prometheus, you need to format the scrape configuration like so: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  - <span style="color:#66d9ef">job_name</span>: <span style="color:#e6db74">&#39;etcd-scrape&#39;</span>
    <span style="color:#66d9ef">scheme</span>: https
    <span style="color:#66d9ef">dns_sd_configs</span>:
    - <span style="color:#66d9ef">names</span>:
      - <span style="color:#e6db74">&#39;_etcd-client-ssl._tcp.staging.example.com.&#39;</span>
    <span style="color:#66d9ef">tls_config</span>:
      <span style="color:#66d9ef">ca_file</span>: etcd-certs/ca.pem
      <span style="color:#66d9ef">cert_file</span>: etcd-certs/client.pem
      <span style="color:#66d9ef">key_file</span>: etcd-certs/client-key.pem
</code></pre></div><p>The only part you need for DNS discovery is the <code>dns_sd_configs</code> section. The rest are to allow you to speak HTTPS to the ETCD API. These will then appear as targets in Prometheus.</p>
<h2 id="how-to-update-the-srv-record">How to update the SRV record?</h2>
<p>It all depends on your use case. In some cases, this may be a manual process. It can also be done by the systems themselves (Active Directory being a good example). Alternatively, use whatever automation method you feel is appropriate.</p>
<p>For example, I built a small Golang utility for ETCD that will scrape AWS tags, and for those that have the correct <code>etcd-cluster</code> tag, it will update the SRV record for that cluster. This has the advantage of all cluster nodes being able to run the utility, rather than reliant on one node to make the updates.</p>
<h2 id="summary">Summary</h2>
<p>My personal preference for service discovery is definitely using Consul. However if you already have DNS records that are getting created (e.g. ETCD, Active Directory), or the additional complexity of Consul will not provide enough benefit, DNS service discovery could be the way to go.</p>
]]></content>
        </item>
        
        <item>
            <title>DNS Anycast: Using BGP for DNS High-Availability</title>
            <link>https://yetiops.net/posts/anycast-bgp/</link>
            <pubDate>Thu, 05 Dec 2019 13:30:25 +0000</pubDate>
            
            <guid>https://yetiops.net/posts/anycast-bgp/</guid>
            <description>DNS has a number of mechanisms for redundancy and high availability. More often than not, clients will have a primary and secondary nameserver to talk to. However, if the primary nameserver fails for whatever reason, then the queries to the primary usually need to timeout before attempting queries to the secondary.
Also the speed of general web browsing can often be dictated by how long it takes to receive a valid DNS response to the query.</description>
            <content type="html"><![CDATA[<p>DNS has a number of mechanisms for redundancy and high availability. More often than not, clients will have a primary and secondary nameserver to talk to. However, if the primary nameserver fails for whatever reason, then the queries to the primary usually need to timeout before attempting queries to the secondary.</p>
<p>Also the speed of general web browsing can often be dictated by how long it takes to receive a valid DNS response to the query. If you are going to multiple sites one after the other, then you are likely to need to wait briefly while DNS does its thing.</p>
<p>To get around this, there is a mechanism known as Anycast. This allows multiple servers to use the same IP, and then routing takes care of which server to go to. This has a couple of notable benefits: -</p>
<ul>
<li>Requests to an Anycast IP are not dependant on the availability of a single server</li>
<li>Requests can be forwarded to the &ldquo;closest&rdquo; server with the Anycast IP</li>
</ul>
<p>The term &ldquo;closest&rdquo; means shortest in terms of routing. You might find that the &ldquo;closest&rdquo; in terms of how a packet is routed is not physically the closest server with said IP.</p>
<p>Typically though, providers who serve DNS requests (e.g. Google&rsquo;s <code>8.8.8.8</code>, CloudFlare&rsquo;s <code>1.1.1.1</code>) will have enough presence internationally to place DNS servers close to the users.</p>
<h2 id="bgp">BGP</h2>
<p>The routing protocol most often used for Anycast (and for routing on the Internet generally) is the Border Gateway Protocol (or BGP). For those who do not know, a routing protocol is used to dynamically advertise and receive routes between neighbouring devices. BGP is one such protocol.</p>
<p>I won&rsquo;t go into an in-depth discussion about BGP, but if you would like to know more about it, I would refer you to the <a href="https://blog.cdemi.io/beginners-guide-to-understanding-bgp/">Beginner&rsquo;s Guide to Understanding BGP</a>.</p>
<h2 id="anycast-ip">Anycast IP?</h2>
<p>The Wikipedia definition of <a href="https://en.wikipedia.org/wiki/Anycast">Anycast</a> is as such: -</p>
<blockquote>
<p>Anycast is a network addressing and routing methodology in which a single destination address has multiple routing paths to two or more endpoint destinations.</p>
<p>Routers will select the desired path on the basis of number of hops, distance, lowest cost, latency measurements or based on the least congested route.</p>
</blockquote>
<p>An Anycast IP is no different from any other IP address. They are not allocated from a specific range like multicast (<code>224.0.0.0/4</code>).</p>
<p>What makes an IP <em>anycast</em> is it being configured on multiple servers and using a routing protocol to advertise it. Technically you could also do this with static routes (rather than a routing protocol), but I wouldn&rsquo;t advise it!</p>
<h2 id="how-does-it-work">How does it work?</h2>
<p>To demonstrate Anycast, I&rsquo;m going to go through a lab with: -</p>
<ul>
<li>Two nameservers, one running BIND9, the other running Unbound</li>
<li>Two client machines, configured to use the Anycast IP for DNS requests</li>
<li>Two VyOS routers acting as a gateway to the client machines, and BGP peers to the nameservers</li>
</ul>
<p>One of the main points to note is that to provide Anycast services, you need to run a routing protocol on the nameservers directly, not just on the routers. Without this, you are reliant on BGP timeouts or interfaces going down to see if a server has gone down.</p>
<p>The diagram below shows the setup: -</p>
<p><img src="/img/anycast/anycast.png" alt="Anycast Lab Diagram"></p>
<h2 id="nameserver-preparation">Nameserver Preparation</h2>
<p>I chose to use BIND9 and Unbound, partly to show that the DNS software running doesn&rsquo;t matter, but also because I had never used Unbound before. Both servers are running Debian Buster.</p>
<h3 id="install-dns-software">Install DNS Software</h3>
<p>To install BIND9 in Debian, run <code>sudo apt-get install bind9</code>. After this is done, BIND9 should be running already: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo systemctl status bind9
* bind9.service - BIND Domain Name Server
   Loaded: loaded <span style="color:#f92672">(</span>/lib/systemd/system/bind9.service; enabled; vendor preset: enabled<span style="color:#f92672">)</span>
   Active: active <span style="color:#f92672">(</span>running<span style="color:#f92672">)</span> since Mon 2019-12-02 10:34:10 GMT; <span style="color:#ae81ff">1</span> day 2h ago
     Docs: man:named<span style="color:#f92672">(</span>8<span style="color:#f92672">)</span>
  Process: <span style="color:#ae81ff">539</span> ExecStart<span style="color:#f92672">=</span>/usr/sbin/named $OPTIONS <span style="color:#f92672">(</span>code<span style="color:#f92672">=</span>exited, status<span style="color:#f92672">=</span>0/SUCCESS<span style="color:#f92672">)</span>
 Main PID: <span style="color:#ae81ff">543</span> <span style="color:#f92672">(</span>named<span style="color:#f92672">)</span>
    Tasks: <span style="color:#ae81ff">4</span> <span style="color:#f92672">(</span>limit: 453<span style="color:#f92672">)</span>
   Memory: 20.2M
   CGroup: /system.slice/bind9.service
           <span style="color:#e6db74">`</span>-543 /usr/sbin/named -u bind

Dec <span style="color:#ae81ff">02</span> 10:34:34 ns-01 named<span style="color:#f92672">[</span>543<span style="color:#f92672">]</span>: configuring command channel from <span style="color:#e6db74">&#39;/etc/bind/rndc.key&#39;</span>
Dec <span style="color:#ae81ff">02</span> 10:34:34 ns-01 named<span style="color:#f92672">[</span>543<span style="color:#f92672">]</span>: reloading configuration succeeded
Dec <span style="color:#ae81ff">02</span> 10:34:34 ns-01 named<span style="color:#f92672">[</span>543<span style="color:#f92672">]</span>: scheduled loading new zones
Dec <span style="color:#ae81ff">02</span> 10:34:34 ns-01 named<span style="color:#f92672">[</span>543<span style="color:#f92672">]</span>: any newly configured zones are now loaded
Dec <span style="color:#ae81ff">02</span> 10:34:34 ns-01 named<span style="color:#f92672">[</span>543<span style="color:#f92672">]</span>: running
Dec <span style="color:#ae81ff">02</span> 10:34:34 ns-01 named<span style="color:#f92672">[</span>543<span style="color:#f92672">]</span>: managed-keys-zone: Key <span style="color:#ae81ff">20326</span> <span style="color:#66d9ef">for</span> zone . acceptance timer complete: key now trusted
Dec <span style="color:#ae81ff">02</span> 10:34:34 ns-01 named<span style="color:#f92672">[</span>543<span style="color:#f92672">]</span>: resolver priming query complete
Dec <span style="color:#ae81ff">03</span> 10:34:34 ns-01 named<span style="color:#f92672">[</span>543<span style="color:#f92672">]</span>: _default: sending trust-anchor-telemetry query <span style="color:#e6db74">&#39;_ta-4f66/NULL&#39;</span>
Dec <span style="color:#ae81ff">03</span> 10:34:34 ns-01 named<span style="color:#f92672">[</span>543<span style="color:#f92672">]</span>: resolver priming query complete
Dec <span style="color:#ae81ff">03</span> 10:34:34 ns-01 named<span style="color:#f92672">[</span>543<span style="color:#f92672">]</span>: managed-keys-zone: Key <span style="color:#ae81ff">20326</span> <span style="color:#66d9ef">for</span> zone . acceptance timer complete: key now trusted
</code></pre></div><p>I also tend to install <code>dnsutils</code> to give access to <code>dig</code> and other useful tools.</p>
<p>I have configured the following options for BIND, to ensure it responds to DNS requests for hosts not on its local subnet. This is configured in <code>/etc/bind/named.conf.options</code>: -
<br /></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">acl goodclients {
 192.168.0.0/16;
 localhost;
};


options {
        directory &#34;/var/cache/bind&#34;;
        allow-query { goodclients; };
        forwarders {
                9.9.9.9;
        };
        dnssec-validation auto;
        listen-on { any; };
        listen-on-v6 { any; };
};
</code></pre></div><p>To check if this works, run <code>dig yetiops.net @127.0.0.1</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ dig yetiops.net @127.0.0.1                                                                                               12:38:31

; &lt;&lt;&gt;&gt; DiG 9.11.5-P4-5.1-Debian &lt;&lt;&gt;&gt; yetiops.net @127.0.0.1
;; global options: +cmd
;; Got answer:
;; -&gt;&gt;HEADER<span style="color:#e6db74">&lt;&lt;- opcode: QUERY, status: NOERROR, id: 61410
</span><span style="color:#e6db74">;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 13, ADDITIONAL: 1
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">;; OPT PSEUDOSECTION:
</span><span style="color:#e6db74">; EDNS: version: 0, flags:; udp: 4096
</span><span style="color:#e6db74">; COOKIE: 91c9c41a84a8b5791d1c12705de658e7efbd7e6b847735de (good)
</span><span style="color:#e6db74">;; QUESTION SECTION:
</span><span style="color:#e6db74">;yetiop</span>s.net.                   IN      A

;; ANSWER SECTION:
yetiops.net.            <span style="color:#ae81ff">300</span>     IN      A       104.31.77.84
yetiops.net.            <span style="color:#ae81ff">300</span>     IN      A       104.31.76.84

;; AUTHORITY SECTION:
.                       <span style="color:#ae81ff">25648</span>   IN      NS      a.root-servers.net.
.                       <span style="color:#ae81ff">25648</span>   IN      NS      c.root-servers.net.
.                       <span style="color:#ae81ff">25648</span>   IN      NS      i.root-servers.net.
.                       <span style="color:#ae81ff">25648</span>   IN      NS      d.root-servers.net.
.                       <span style="color:#ae81ff">25648</span>   IN      NS      h.root-servers.net.
.                       <span style="color:#ae81ff">25648</span>   IN      NS      f.root-servers.net.
.                       <span style="color:#ae81ff">25648</span>   IN      NS      g.root-servers.net.
.                       <span style="color:#ae81ff">25648</span>   IN      NS      l.root-servers.net.
.                       <span style="color:#ae81ff">25648</span>   IN      NS      b.root-servers.net.
.                       <span style="color:#ae81ff">25648</span>   IN      NS      m.root-servers.net.
.                       <span style="color:#ae81ff">25648</span>   IN      NS      k.root-servers.net.
.                       <span style="color:#ae81ff">25648</span>   IN      NS      j.root-servers.net.
.                       <span style="color:#ae81ff">25648</span>   IN      NS      e.root-servers.net.

;; Query time: <span style="color:#ae81ff">95</span> msec
;; SERVER: 127.0.0.1#53<span style="color:#f92672">(</span>127.0.0.1<span style="color:#f92672">)</span>
;; WHEN: Tue Dec <span style="color:#ae81ff">03</span> 12:45:27 GMT <span style="color:#ae81ff">2019</span>
;; MSG SIZE  rcvd: <span style="color:#ae81ff">308</span>
</code></pre></div><p>To install <code>unbound</code> instead, do <code>sudo apt-get install unbound</code> instead. Again, it should start straight away once installed: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo systemctl status unbound
* unbound.service - Unbound DNS server
   Loaded: loaded <span style="color:#f92672">(</span>/lib/systemd/system/unbound.service; enabled; vendor preset: enabled<span style="color:#f92672">)</span>
   Active: active <span style="color:#f92672">(</span>running<span style="color:#f92672">)</span> since Tue 2019-12-03 12:47:48 GMT; 1s ago
     Docs: man:unbound<span style="color:#f92672">(</span>8<span style="color:#f92672">)</span>
  Process: <span style="color:#ae81ff">4580</span> ExecStartPre<span style="color:#f92672">=</span>/usr/lib/unbound/package-helper chroot_setup <span style="color:#f92672">(</span>code<span style="color:#f92672">=</span>exited, status<span style="color:#f92672">=</span>0/SUCCESS<span style="color:#f92672">)</span>
  Process: <span style="color:#ae81ff">4583</span> ExecStartPre<span style="color:#f92672">=</span>/usr/lib/unbound/package-helper root_trust_anchor_update <span style="color:#f92672">(</span>code<span style="color:#f92672">=</span>exited, status<span style="color:#f92672">=</span>0/SUCCESS<span style="color:#f92672">)</span>
 Main PID: <span style="color:#ae81ff">4587</span> <span style="color:#f92672">(</span>unbound<span style="color:#f92672">)</span>
    Tasks: <span style="color:#ae81ff">1</span> <span style="color:#f92672">(</span>limit: 453<span style="color:#f92672">)</span>
   Memory: 6.4M
   CGroup: /system.slice/unbound.service
           <span style="color:#e6db74">`</span>-4587 /usr/sbin/unbound -d

Dec <span style="color:#ae81ff">03</span> 12:47:48 ns-02 systemd<span style="color:#f92672">[</span>1<span style="color:#f92672">]</span>: Starting Unbound DNS server...
Dec <span style="color:#ae81ff">03</span> 12:47:48 ns-02 package-helper<span style="color:#f92672">[</span>4583<span style="color:#f92672">]</span>: /var/lib/unbound/root.key has content
Dec <span style="color:#ae81ff">03</span> 12:47:48 ns-02 package-helper<span style="color:#f92672">[</span>4583<span style="color:#f92672">]</span>: success: the anchor is ok
Dec <span style="color:#ae81ff">03</span> 12:47:48 ns-02 unbound<span style="color:#f92672">[</span>4587<span style="color:#f92672">]</span>: <span style="color:#f92672">[</span>4587:0<span style="color:#f92672">]</span> notice: init module 0: subnet
Dec <span style="color:#ae81ff">03</span> 12:47:48 ns-02 unbound<span style="color:#f92672">[</span>4587<span style="color:#f92672">]</span>: <span style="color:#f92672">[</span>4587:0<span style="color:#f92672">]</span> notice: init module 1: validator
Dec <span style="color:#ae81ff">03</span> 12:47:48 ns-02 unbound<span style="color:#f92672">[</span>4587<span style="color:#f92672">]</span>: <span style="color:#f92672">[</span>4587:0<span style="color:#f92672">]</span> notice: init module 2: iterator
Dec <span style="color:#ae81ff">03</span> 12:47:48 ns-02 systemd<span style="color:#f92672">[</span>1<span style="color:#f92672">]</span>: Started Unbound DNS server.
Dec <span style="color:#ae81ff">03</span> 12:47:48 ns-02 unbound<span style="color:#f92672">[</span>4587<span style="color:#f92672">]</span>: <span style="color:#f92672">[</span>4587:0<span style="color:#f92672">]</span> info: start of service <span style="color:#f92672">(</span>unbound 1.9.0<span style="color:#f92672">)</span>.
</code></pre></div><p>The configuration for Unbound, using multiple forwarders, looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">include: &#34;/etc/unbound/unbound.conf.d/*.conf&#34;

server:
  access-control: 10.0.0.0/8 allow
  access-control: 127.0.0.0/8 allow
  access-control: 192.168.0.0/16 allow
  aggressive-nsec: yes
  cache-max-ttl: 14400
  cache-min-ttl: 1200
  hide-identity: yes
  hide-version: yes
  interface: 169.254.0.1
  prefetch: yes
  rrset-roundrobin: yes
  use-caps-for-id: yes
  verbosity: 1


forward-zone:
   name: &#34;.&#34;
   forward-addr: 1.0.0.1@53#one.one.one.one
   forward-addr: 1.1.1.1@53#one.one.one.one
   forward-addr: 8.8.4.4@53#dns.google
   forward-addr: 8.8.8.8@53#dns.google
   forward-addr: 9.9.9.9@53#dns.quad9.net
   forward-addr: 149.112.112.112@53#dns.quad9.net
</code></pre></div><p>This configuration was taken from this <a href="https://calomel.org/unbound_dns.html">Unbound DNS Tutorial</a>.</p>
<p>Again, testing should give a similar result: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">dig yetiops.net @169.254.0.1

; &lt;&lt;&gt;&gt; DiG 9.11.5-P4-5.1-Debian &lt;&lt;&gt;&gt; yetiops.net @169.254.0.1
;; global options: +cmd
;; Got answer:
;; -&gt;&gt;HEADER<span style="color:#e6db74">&lt;&lt;- opcode: QUERY, status: NOERROR, id: 52738
</span><span style="color:#e6db74">;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">;; OPT PSEUDOSECTION:
</span><span style="color:#e6db74">; EDNS: version: 0, flags:; udp: 4096
</span><span style="color:#e6db74">;; QUESTION SECTION:
</span><span style="color:#e6db74">;yetiop</span>s.net.                   IN      A

;; ANSWER SECTION:
yetiops.net.            <span style="color:#ae81ff">1200</span>    IN      A       104.31.77.84
yetiops.net.            <span style="color:#ae81ff">1200</span>    IN      A       104.31.76.84

;; Query time: <span style="color:#ae81ff">154</span> msec
;; SERVER: 169.254.0.1#53<span style="color:#f92672">(</span>169.254.0.1<span style="color:#f92672">)</span>
;; WHEN: Tue Dec <span style="color:#ae81ff">03</span> 12:51:43 GMT <span style="color:#ae81ff">2019</span>
;; MSG SIZE  rcvd: <span style="color:#ae81ff">72</span>
</code></pre></div><p>The reason for doing the tests to <code>169.254.0.1</code> is that Unbound appears to respond on the physical interface IP, rather than the interface the query was received upon. I shall do a follow up on Unbound when I have used it more, but for now this serves the purpose that we need.</p>
<h3 id="network-interface-configuration">Network interface configuration</h3>
<p>The network interface configuration on Debian will require a &ldquo;loopback&rdquo; interface. Rather than applying the Anycast IP directly to a physical interface, it is applied to a logical interface instead (the loopback).</p>
<p>This has benefits, in that you can use multiple physical interfaces as links to multiple routers, but advertising the same anycast IP (rather than being tied to a physical interface). Also, it means that you only have to use a host route (i.e. a <code>/32</code> IP address), and cut down on your IP address usage. If you are using private address space, this probably isn&rsquo;t much of a concern, but public IPv4 addresses are scarce (IPv6 is another matter entirely, but most clients still talk IPv4).</p>
<p>To apply this configuration on a Debian machine, you will need to add it into <code>/etc/network/interfaces</code> like so: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">## The loopback network interface
auto lo
iface lo inet loopback

## The anycast IP
auto lo:1
iface lo:1 inet static
 address 169.254.0.1/32

## The physical interface
auto eth2
iface eth2 inet static
 address 10.21.2.1/31
</code></pre></div><p>The above is the configuration on <code>ns-01</code>. The configuration on <code>ns-02</code> will be the same, except that the IP address of <code>eth2</code> would be <code>10.21.2.3/31</code>·</p>
<h3 id="frr">FRR</h3>
<p><a href="https://frrouting.org/">FRR</a>, or Free Range Routing, is a notable fork of Quagga that provides a number of routing protocols (and other useful network protocols, like VRRP and LDP) on Linux. It also has the <code>vtysh</code> shell package, which allows you to configure, verify and monitor using very Cisco-like syntax.</p>
<p>To install on Debian or Ubuntu (or other Debian-like distributions), go to the <a href="https://deb.frrouting.org/">FRR Debian Repository page</a>. For other systems, please see the FRR <a href="http://docs.frrouting.org/en/latest/overview.html#how-to-get-frr">documentation</a>.</p>
<p>Once installed, the only changes I make are to enable the BGP daemon, and to add my user to the <code>frr</code> and <code>frrvty</code> groups. This allows me to administer FRR without requiring escalated privileges.</p>
<p>To enable the BGP daemon, open up <code>/etc/frr/daemons</code>, find the line which says <code>bgpd=no</code>, and change it to <code>bgpd=yes</code>. After reloading (<code>systemctl reload frr</code>), the BGP daemon should be available: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">* frr.service - FRRouting
   Loaded: loaded <span style="color:#f92672">(</span>/lib/systemd/system/frr.service; enabled; vendor preset: enabled<span style="color:#f92672">)</span>
   Active: active <span style="color:#f92672">(</span>running<span style="color:#f92672">)</span> since Mon 2019-12-02 10:34:09 GMT; <span style="color:#ae81ff">1</span> day 2h ago
     Docs: https://frrouting.readthedocs.io/en/latest/setup.html
  Process: <span style="color:#ae81ff">443</span> ExecStart<span style="color:#f92672">=</span>/usr/lib/frr/frrinit.sh start <span style="color:#f92672">(</span>code<span style="color:#f92672">=</span>exited, status<span style="color:#f92672">=</span>0/SUCCESS<span style="color:#f92672">)</span>
  Process: <span style="color:#ae81ff">7115</span> ExecReload<span style="color:#f92672">=</span>/usr/lib/frr/frrinit.sh reload <span style="color:#f92672">(</span>code<span style="color:#f92672">=</span>exited, status<span style="color:#f92672">=</span>0/SUCCESS<span style="color:#f92672">)</span>
    Tasks: <span style="color:#ae81ff">12</span> <span style="color:#f92672">(</span>limit: 453<span style="color:#f92672">)</span>
   Memory: 26.0M
   CGroup: /system.slice/frr.service
           |- <span style="color:#ae81ff">531</span> /usr/lib/frr/zebra -d -A 127.0.0.1 -s <span style="color:#ae81ff">90000000</span>
           |- <span style="color:#ae81ff">536</span> /usr/lib/frr/staticd -d -A 127.0.0.1
           |-7131 /usr/lib/frr/watchfrr -d zebra bgpd staticd
           <span style="color:#e6db74">`</span>-7140 /usr/lib/frr/bgpd -d -A 127.0.0.1

Dec <span style="color:#ae81ff">03</span> 13:02:19 ns-01 watchfrr<span style="color:#f92672">[</span>7131<span style="color:#f92672">]</span>: <span style="color:#f92672">[</span>EC 268435457<span style="color:#f92672">]</span> bgpd state -&gt; down : initial connection attempt failed
Dec <span style="color:#ae81ff">03</span> 13:02:19 ns-01 watchfrr<span style="color:#f92672">[</span>7131<span style="color:#f92672">]</span>: staticd state -&gt; up : connect succeeded
Dec <span style="color:#ae81ff">03</span> 13:02:19 ns-01 watchfrr<span style="color:#f92672">[</span>7131<span style="color:#f92672">]</span>: <span style="color:#f92672">[</span>EC 100663303<span style="color:#f92672">]</span> Forked background command <span style="color:#f92672">[</span>pid 7132<span style="color:#f92672">]</span>: /usr/lib/frr/watchfrr.sh restart bgpd
Dec <span style="color:#ae81ff">03</span> 13:02:19 ns-01 watchfrr.sh<span style="color:#f92672">[</span>7138<span style="color:#f92672">]</span>: Cannot stop bgpd: pid file not found
Dec <span style="color:#ae81ff">03</span> 13:02:19 ns-01 zebra<span style="color:#f92672">[</span>531<span style="color:#f92672">]</span>: client <span style="color:#ae81ff">31</span> says hello and bids fair to announce only vnc routes vrf<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>
Dec <span style="color:#ae81ff">03</span> 13:02:19 ns-01 zebra<span style="color:#f92672">[</span>531<span style="color:#f92672">]</span>: client <span style="color:#ae81ff">28</span> says hello and bids fair to announce only bgp routes vrf<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>
Dec <span style="color:#ae81ff">03</span> 13:02:19 ns-01 watchfrr<span style="color:#f92672">[</span>7131<span style="color:#f92672">]</span>: bgpd state -&gt; up : connect succeeded
Dec <span style="color:#ae81ff">03</span> 13:02:19 ns-01 watchfrr<span style="color:#f92672">[</span>7131<span style="color:#f92672">]</span>: all daemons up, doing startup-complete notify
Dec <span style="color:#ae81ff">03</span> 13:02:19 ns-01 frrinit.sh<span style="color:#f92672">[</span>7115<span style="color:#f92672">]</span>: Started watchfrr.
Dec <span style="color:#ae81ff">03</span> 13:02:20 ns-01 systemd<span style="color:#f92672">[</span>1<span style="color:#f92672">]</span>: Reloaded FRRouting.
</code></pre></div><p>A couple of error messages appear, but this is because BGP is not already running when the reload is performed. After this, future reloads shouldn&rsquo;t show the same.</p>
<p>After running <code>sudo usermod -aG frr $MY-USER</code> and <code>sudo usermod -aG frrvty $MY-USER</code>, I should now be able to access to the <code>vtysh</code> shell and start BGP: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-txt" data-lang="txt">$ vtysh

Hello, this is FRRouting (version 7.2).

Copyright 1996-2005 Kunihiro Ishiguro, et al.

ns-01# conf t
ns-01(config)# router bgp 65001
ns-01(config-router)# exit
ns-01(config)# end
ns-01# show bgp summary
% No BGP neighbors found
</code></pre></div><p>No BGP neighbours were found, but none have been configured, so this is expected behaviour.</p>
<h2 id="nameserver-routing-protocol-configuration">Nameserver Routing Protocol Configuration</h2>
<p>To setup BGP between the Nameservers and the VyOS routers, you&rsquo;ll need to choose some Autonomous System numbers (ASNs). The private ranges (i.e. those that anyone can use, and should never be seen on the public internet) are <strong>64512-65534</strong> (for 2-byte ASNs) and <strong>4200000000-4294967294</strong> (for 4-byte ASNs). I&rsquo;m going to use both, to show that none of this is dependent on the type used.</p>
<ul>
<li><strong>ns-01</strong> - BGP ASN 64520</li>
<li><strong>ns-02</strong> - BGP ASN 64530</li>
<li><strong>VyOS Routers</strong> - BGP ASN 4290001234</li>
</ul>
<h3 id="frr-1">FRR</h3>
<p>The following configuration will be applied via <code>vtysh</code>: -</p>
<p><strong>ns-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">ns-01# conf t
ns-01(config)# router bgp 64520
ns-01(config-router)# neighbor 10.21.2.0 remote-as 4290001234
ns-01(config-router)# address-family ipv4 unicast 
ns-01(config-router-af)# neighbor 10.21.2.0 activate 
ns-01(config-router-af)# network 169.254.0.1/32
ns-01(config-router-af)# end
ns-01# wr mem
Note: this version of vtysh never writes vtysh.conf
Building Configuration...
Warning: /etc/frr/frr.conf.sav unlink failed
Integrated configuration saved to /etc/frr/frr.conf
[OK]
</code></pre></div><p><strong>ns-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">ns-02# conf t
ns-02(config)# router bgp 64530
ns-02(config-router)# neighbor 10.21.2.2 remote-as 4290001234
ns-02(config-router)# address-family ipv4 unicast 
ns-02(config-router-af)# neighbor 10.21.2.2 activate 
ns-02(config-router-af)# network 169.254.0.1/32
ns-02(config-router-af)# end
ns-02# wr mem
Note: this version of vtysh never writes vtysh.conf
Building Configuration...
Warning: /etc/frr/frr.conf.sav unlink failed
Integrated configuration saved to /etc/frr/frr.conf
[OK]
</code></pre></div><p>For anyone who has configured a Cisco router, switch or similar, the syntax should be very familiar.</p>
<p>The main thing to notice is the <code>network 169.254.0.1/32</code> statement. The same statement is configured on both Nameservers, because they are going to advertise the same IP (the Anycast IP). The network statement imports the route into BGP, and allows it to be advertised out to it&rsquo;s peers.</p>
<h2 id="vyos-bgp-configuration">VyOS BGP Configuration</h2>
<p>VyOS configuration looks like a mixture of Juniper&rsquo;s JunOS and Cisco&rsquo;s IOS. It can look a little odd if you are heavily in either of the Cisco or Juniper camps, but it doesn&rsquo;t take too long to get used to.</p>
<p><strong>vyos-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">vyos@vyos-01:~$ configure
[edit]
vyos@vyos-01# set protocols bgp 4290001234 neighbor 10.21.2.1 remote-as 64520
[edit]
vyos@vyos-01# set protocols bgp 4290001234 neighbor 10.21.1.1 remote-as 4290001234
[edit]
vyos@vyos-01# set protocols bgp 4290001234 address-family ipv4-unicast network 192.168.2.0/24 
[edit]
vyos@vyos-02# set protocols bgp 4290001234 address-family ipv4-unicast network 10.21.2.0/31 
[edit]
vyos@vyos-01# commit
[edit]
vyos@vyos-01# save
Saving configuration to &#39;/config/config.boot&#39;...
Done
</code></pre></div><p><strong>vyos-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">vyos@vyos-02:~$ configure
[edit]
vyos@vyos-02# set protocols bgp 4290001234 neighbor 10.21.2.3 remote-as 64530
[edit]
vyos@vyos-02# set protocols bgp 4290001234 neighbor 10.21.1.0 remote-as 4290001234
[edit]
vyos@vyos-02# set protocols bgp 4290001234 address-family ipv4-unicast network 192.168.3.0/24 
[edit]
vyos@vyos-02# set protocols bgp 4290001234 address-family ipv4-unicast network 10.21.2.2/31 
[edit]
vyos@vyos-02# commit
[edit]
vyos@vyos-02# save
Saving configuration to &#39;/config/config.boot&#39;...
Done
</code></pre></div><p>The configuration does not apply until you commit it (like JunOS and Cisco IOS-XR), and also if you do not save it, it will not be there on reboot.</p>
<p>The network statements are to ensure that the Nameservers know about the IP ranges of the clients.</p>
<h2 id="verification">Verification</h2>
<h3 id="check-routing">Check Routing</h3>
<p>After this, we should be able to see the Anycast IP appear in the routing tables of both VyOS routers: -</p>
<p><strong>vyos-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">vyos@vyos-01:~$ show ip route 169.254.0.1
Routing entry <span style="color:#66d9ef">for</span> 169.254.0.1/32
  Known via <span style="color:#e6db74">&#34;bgp&#34;</span>, distance 20, metric 0, best
  Last update 00:07:47 ago
  * 10.21.2.1, via eth3
</code></pre></div><p><strong>vyos-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">vyos@vyos-02:~$ show ip route 169.254.0.1
Routing entry <span style="color:#66d9ef">for</span> 169.254.0.1/32
  Known via <span style="color:#e6db74">&#34;bgp&#34;</span>, distance 20, metric 0, best
  Last update 00:00:48 ago
  * 10.21.2.3, via eth3
</code></pre></div><p>The last line on each route shows where it was received from. For <strong>vyos-01</strong>, this was received from <code>10.21.2.1</code> (the physical IP of <strong>ns-01</strong>). For <strong>vyos-02</strong>, this was received from <code>10.21.2.3</code> (the physical IP of <strong>ns-02</strong>).</p>
<p>This is the basis of Anycast, the same IP originating from multiple origins.</p>
<h3 id="test-a-dns-query">Test a DNS query</h3>
<p>Testing DNS from the clients should show responses: -</p>
<p><strong>client-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ dig www.google.com @169.254.0.1

; &lt;&lt;&gt;&gt; DiG 9.11.5-P4-5.1-Debian &lt;&lt;&gt;&gt; www.google.com @169.254.0.1
;; global options: +cmd
;; Got answer:
;; -&gt;&gt;HEADER<span style="color:#e6db74">&lt;&lt;- opco</span>de: QUERY, status: NOERROR, id: <span style="color:#ae81ff">52511</span>
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 13, ADDITIONAL: <span style="color:#ae81ff">1</span>

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: <span style="color:#ae81ff">4096</span>
; COOKIE: 53c833de2eb0124a47b5c3195de8f2646dfe1769e95f3929 <span style="color:#f92672">(</span>good<span style="color:#f92672">)</span>
;; QUESTION SECTION:
;www.google.com.                        IN      A

;; ANSWER SECTION:
www.google.com.         <span style="color:#ae81ff">167</span>     IN      A       172.217.20.100

;; AUTHORITY SECTION:
.                       <span style="color:#ae81ff">21700</span>   IN      NS      b.root-servers.net.
.                       <span style="color:#ae81ff">21700</span>   IN      NS      g.root-servers.net.
.                       <span style="color:#ae81ff">21700</span>   IN      NS      i.root-servers.net.
.                       <span style="color:#ae81ff">21700</span>   IN      NS      j.root-servers.net.
.                       <span style="color:#ae81ff">21700</span>   IN      NS      a.root-servers.net.
.                       <span style="color:#ae81ff">21700</span>   IN      NS      m.root-servers.net.
.                       <span style="color:#ae81ff">21700</span>   IN      NS      f.root-servers.net.
.                       <span style="color:#ae81ff">21700</span>   IN      NS      e.root-servers.net.
.                       <span style="color:#ae81ff">21700</span>   IN      NS      c.root-servers.net.
.                       <span style="color:#ae81ff">21700</span>   IN      NS      h.root-servers.net.
.                       <span style="color:#ae81ff">21700</span>   IN      NS      d.root-servers.net.
.                       <span style="color:#ae81ff">21700</span>   IN      NS      k.root-servers.net.
.                       <span style="color:#ae81ff">21700</span>   IN      NS      l.root-servers.net.

;; Query time: <span style="color:#ae81ff">11</span> msec
;; SERVER: 169.254.0.1#53<span style="color:#f92672">(</span>169.254.0.1<span style="color:#f92672">)</span>
;; WHEN: Thu Dec <span style="color:#ae81ff">05</span> 12:04:52 GMT <span style="color:#ae81ff">2019</span>
;; MSG SIZE  rcvd: <span style="color:#ae81ff">298</span>
</code></pre></div><p><strong>client-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ dig www.google.com @169.254.0.1

; &lt;&lt;&gt;&gt; DiG 9.11.5-P4-5.1-Debian &lt;&lt;&gt;&gt; www.google.com @169.254.0.1
;; global options: +cmd
;; Got answer:
;; -&gt;&gt;HEADER<span style="color:#e6db74">&lt;&lt;- opco</span>de: QUERY, status: NOERROR, id: <span style="color:#ae81ff">44703</span>
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: <span style="color:#ae81ff">1</span>

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: <span style="color:#ae81ff">4096</span>
;; QUESTION SECTION:
;www.google.com.                        IN      A

;; ANSWER SECTION:
www.google.com.         <span style="color:#ae81ff">857</span>     IN      A       216.58.208.100

;; Query time: <span style="color:#ae81ff">1</span> msec
;; SERVER: 169.254.0.1#53<span style="color:#f92672">(</span>169.254.0.1<span style="color:#f92672">)</span>
;; WHEN: Thu Dec <span style="color:#ae81ff">05</span> 12:06:18 GMT <span style="color:#ae81ff">2019</span>
;; MSG SIZE  rcvd: <span style="color:#ae81ff">59</span>
</code></pre></div><p>Interestingly, we get different responses based upon whether we are hitting BIND (<strong>ns-01</strong>) or Unbound (<strong>ns-02</strong>), however they are running different forwarders so this would explain it.</p>
<p>How to prove that traffic is going to <strong>ns-01</strong> or <strong>ns-02</strong>? <code>tcpdump</code> of course!</p>
<p><strong>ns-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ tcpdump -i eth2 port <span style="color:#ae81ff">53</span>
tcpdump: verbose output suppressed, use -v or -vv <span style="color:#66d9ef">for</span> full protocol decode
listening on eth2, link-type EN10MB <span style="color:#f92672">(</span>Ethernet<span style="color:#f92672">)</span>, capture size <span style="color:#ae81ff">262144</span> bytes
12:08:55.596445 IP 192.168.2.10.33692 &gt; 169.254.0.1.domain: 6718+ <span style="color:#f92672">[</span>1au<span style="color:#f92672">]</span> A? www.google.com. <span style="color:#f92672">(</span>55<span style="color:#f92672">)</span>
12:08:55.609152 IP 169.254.0.1.domain &gt; 192.168.2.10.33692: <span style="color:#ae81ff">6718</span> 1/13/1 A 172.217.17.100 <span style="color:#f92672">(</span>298<span style="color:#f92672">)</span>
</code></pre></div><p><strong>ns-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ tcpdump -i eth2 port <span style="color:#ae81ff">53</span>
tcpdump: verbose output suppressed, use -v or -vv <span style="color:#66d9ef">for</span> full protocol decode
listening on eth2, link-type EN10MB <span style="color:#f92672">(</span>Ethernet<span style="color:#f92672">)</span>, capture size <span style="color:#ae81ff">262144</span> bytes
12:08:57.377725 IP 192.168.3.10.42406 &gt; 169.254.0.1.domain: 55171+ <span style="color:#f92672">[</span>1au<span style="color:#f92672">]</span> A? www.google.com. <span style="color:#f92672">(</span>55<span style="color:#f92672">)</span>
12:08:57.392048 IP 169.254.0.1.domain &gt; 192.168.3.10.42406: <span style="color:#ae81ff">55171</span> 1/0/1 A 216.58.208.100 <span style="color:#f92672">(</span>59<span style="color:#f92672">)</span>
</code></pre></div><p>So as we can see, <strong>client-01</strong> (which is in the 192.168.2.10 subnet) is getting a response from <strong>ns-01</strong>, whereas <strong>client-02</strong> is getting a response from <strong>ns-02</strong>. The destination address of the requests is <code>169.254.0.1</code>, but <strong>vyos-01</strong> and <strong>vyos-02</strong> have different routes for the IP address, therefore they arrive on different servers.</p>
<h2 id="what-if-one-server-goes-away">What if one server goes away?</h2>
<p>We have already seen that DNS queries are being routed to the <em>closest</em> nameserver. In our scenario, this means that queries travel from the Client, to its connected router, and then to the nameserver connected to the same router.</p>
<p>What happens if say, the BGP peering failed to <strong>ns-01</strong>, or the server failed? Lets see!</p>
<p><strong>ns-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ shutdown -h now
</code></pre></div><p>Now lets check the routing tables on <strong>vyos-01</strong></p>
<p><strong>vyos-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">vyos@vyos-01:~$ show ip route 169.254.0.1
Routing entry <span style="color:#66d9ef">for</span> 169.254.0.1/32
  Known via <span style="color:#e6db74">&#34;bgp&#34;</span>, distance 200, metric 0, best
  Last update 00:00:27 ago
    10.21.2.3 <span style="color:#f92672">(</span>recursive<span style="color:#f92672">)</span>
  *   10.21.1.1, via eth2
</code></pre></div><p>Now <strong>vyos-01</strong> thinks that <code>169.254.0.1</code> is available via <strong>vyos-02</strong>. Lets run another packet capture on <strong>ns-02</strong>, and see if DNS queries from <strong>client-01</strong> and <strong>client-02</strong> reach it: -</p>
<p><strong>ns-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo tcpdump -i eth2 port <span style="color:#ae81ff">53</span>
tcpdump: verbose output suppressed, use -v or -vv <span style="color:#66d9ef">for</span> full protocol decode
listening on eth2, link-type EN10MB <span style="color:#f92672">(</span>Ethernet<span style="color:#f92672">)</span>, capture size <span style="color:#ae81ff">262144</span> bytes
12:31:12.245274 IP 192.168.2.10.51313 &gt; 169.254.0.1.domain: 60437+ <span style="color:#f92672">[</span>1au<span style="color:#f92672">]</span> A? www.google.com. <span style="color:#f92672">(</span>55<span style="color:#f92672">)</span>
12:31:12.245449 IP 169.254.0.1.domain &gt; 192.168.2.10.51313: <span style="color:#ae81ff">60437</span> 1/0/1 A 172.217.169.36 <span style="color:#f92672">(</span>59<span style="color:#f92672">)</span>
12:31:14.158140 IP 192.168.3.10.54927 &gt; 169.254.0.1.domain: 40789+ <span style="color:#f92672">[</span>1au<span style="color:#f92672">]</span> A? www.google.com. <span style="color:#f92672">(</span>55<span style="color:#f92672">)</span>
12:31:14.158233 IP 169.254.0.1.domain &gt; 192.168.3.10.54927: <span style="color:#ae81ff">40789</span> 1/0/1 A 172.217.169.36 <span style="color:#f92672">(</span>59<span style="color:#f92672">)</span>
</code></pre></div><p>Success! We will no longer be waiting for DNS queries to timeout to the first nameserver the client attempts, instead routing to the next closest server.</p>
<h2 id="what-happens-if-the-dns-software-stops-working">What happens if the DNS software stops working?</h2>
<p>Rather than shutting down the server, this time we will just take down <strong>BIND</strong> on <strong>ns-01</strong></p>
<p><strong>ns-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo systemctl stop bind9
</code></pre></div><p>Lets test from <strong>client-01</strong> and <strong>client-02</strong></p>
<p><strong>client-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ dig www.google.com @169.254.0.1

; &lt;&lt;&gt;&gt; DiG 9.11.5-P4-5.1-Debian &lt;&lt;&gt;&gt; www.google.com @169.254.0.1
;; global options: +cmd
;; connection timed out; no servers could be reached
</code></pre></div><p>Well that isn&rsquo;t good.</p>
<p><strong>client-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ dig www.google.com @169.254.0.1

; &lt;&lt;&gt;&gt; DiG 9.11.5-P4-5.1-Debian &lt;&lt;&gt;&gt; www.google.com @169.254.0.1
;; global options: +cmd
;; Got answer:
;; -&gt;&gt;HEADER<span style="color:#e6db74">&lt;&lt;- opco</span>de: QUERY, status: NOERROR, id: <span style="color:#ae81ff">24561</span>
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: <span style="color:#ae81ff">1</span>

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: <span style="color:#ae81ff">4096</span>
;; QUESTION SECTION:
;www.google.com.                        IN      A

;; ANSWER SECTION:
www.google.com.         <span style="color:#ae81ff">532</span>     IN      A       172.217.169.36

;; Query time: <span style="color:#ae81ff">1</span> msec
;; SERVER: 169.254.0.1#53<span style="color:#f92672">(</span>169.254.0.1<span style="color:#f92672">)</span>
;; WHEN: Thu Dec <span style="color:#ae81ff">05</span> 12:35:29 GMT <span style="color:#ae81ff">2019</span>
;; MSG SIZE  rcvd: <span style="color:#ae81ff">59</span>
</code></pre></div><p><strong>client-02</strong> still works though. Why is this?</p>
<p>FRR is a routing daemon, and is used to provide routing updates from servers (or Linux-based network hardware). It does not track the state of the applications running, and whether they are health or not. This isn&rsquo;t a limitation of FRR, but merely what FRR is designed to do (or where you would typically use it).</p>
<p>If you are using FRR to provide connectivity to a machine over several Layer 3 links (rather than using LACP/bonded interfaces), FRR would shine here. It also can be used to provide <strong>unnumbered</strong> neighbour relationships, but this is a topic for another day.</p>
<h2 id="how-do-we-track-the-dns-software">How do we track the DNS software?</h2>
<p>One of the best examples of a routing daemon that can also react to the application state is <a href="https://github.com/Exa-Networks/exabgp">ExaBGP</a>, written by <a href="https://exa.net.uk">Exa Networks</a>.</p>
<p>What ExaBGP does is periodically runs a script, and checks the output of said script. This <em>script</em> could be a BASH one-liner, or it could be a full application that checks an API for responses, or anything in between.</p>
<p>It has an inbuilt healthcheck tool (useful for BASH one-liners) or you can check the results of <code>STDOUT</code> on running some form of script.</p>
<p>ExaBGP is written in Python, and can be installed using PIP: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo pip3 install exabgp
Collecting exabgp
  Downloading https://files.pythonhosted.org/packages/cf/34/41fc2017d6e61038079738dda32509dc40538f383489c84976807b4834ab/exabgp-4.1.2-py3-none-any.whl <span style="color:#f92672">(</span>557kB<span style="color:#f92672">)</span>
    100% |████████████████████████████████| 563kB 2.7MB/s 
Installing collected packages: exabgp
Successfully installed exabgp-4.1.2
</code></pre></div><p>First, I create a script to check the DNS response from the local server: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">##!/bin/bash</span>

<span style="color:#66d9ef">while</span> true; <span style="color:#66d9ef">do</span>
  /usr/bin/dig yetiops.net @169.254.0.1 &gt; /dev/null;
  <span style="color:#66d9ef">if</span> <span style="color:#f92672">[[</span> $? !<span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
          echo <span style="color:#e6db74">&#34;withdraw route 169.254.0.1 next-hop 10.21.2.1\n&#34;</span>
  <span style="color:#66d9ef">else</span>
          echo <span style="color:#e6db74">&#34;announce route 169.254.0.1 next-hop 10.21.2.1\n&#34;</span>
  <span style="color:#66d9ef">fi</span>
<span style="color:#66d9ef">done</span>
</code></pre></div><p>We are checking the output status of the command, and if it is anything other than <code>0</code>, then we withdraw the route. If the command succeeds (i.e. output status of <code>0</code>), then we will announce the route.</p>
<p>The ExaBGP configuration looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">process announce-routes {
  run /etc/exabgp/dns-check.sh;
  encoder text;
}

neighbor 10.21.2.0 {
    local-address 10.21.2.1;
    local-as 64520;
    peer-as 4290001234; 

    api {
        processes [ announce-routes ];
   }

}
</code></pre></div><p>So we are running a BGP peering session to 10.21.2.1 (i.e. <strong>vyos-01</strong>), and then running a process. The process in question is the script created previously, ExaBGP takes the results from it, and turns them into BGP messages.</p>
<p>In this case, we are doing simple route announcement and withdrawal (with a next-hop set). However you could also add other parameters like <strong>Local Preference</strong> or <strong>MED (Multi-Exit Discriminator)</strong>), extend the <strong>AS-Path</strong>, or apply <strong>BGP Communities</strong>. All of this is beyond the scope of this article (I&rsquo;ll probably do a bit of a BGP deep dive in a future post).</p>
<p>To ensure ExaBGP runs as a service, the following SystemD unit file was created: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#66d9ef">[Unit]</span>
<span style="color:#a6e22e">Description</span><span style="color:#f92672">=</span><span style="color:#e6db74">ExaBGP</span>
<span style="color:#a6e22e">After</span><span style="color:#f92672">=</span><span style="color:#e6db74">network.target</span>
<span style="color:#a6e22e">ConditionPathExists</span><span style="color:#f92672">=</span><span style="color:#e6db74">/etc/exabgp/exabgp.conf</span>

<span style="color:#66d9ef">[Service]</span>
<span style="color:#a6e22e">Environment</span><span style="color:#f92672">=</span><span style="color:#e6db74">exabgp_daemon_daemonize=false</span>
<span style="color:#a6e22e">Environment</span><span style="color:#f92672">=</span><span style="color:#e6db74">ETC=/etc</span>
<span style="color:#a6e22e">ExecStart</span><span style="color:#f92672">=</span><span style="color:#e6db74">/usr/local/bin/exabgp /etc/exabgp/exabgp.conf</span>
<span style="color:#a6e22e">ExecReload</span><span style="color:#f92672">=</span><span style="color:#e6db74">/bin/kill -USR1 $MAINPID</span>

<span style="color:#66d9ef">[Install]</span>
<span style="color:#a6e22e">WantedBy</span><span style="color:#f92672">=</span><span style="color:#e6db74">multi-user.target</span>
</code></pre></div><p>So now lets follow the same process as before.</p>
<h3 id="verification-1">Verification</h3>
<h4 id="check-routing-1">Check routing</h4>
<p><strong>vyos-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">vyos@vyos-01:~$ show ip route 169.254.0.1
Routing entry <span style="color:#66d9ef">for</span> 169.254.0.1/32
  Known via <span style="color:#e6db74">&#34;bgp&#34;</span>, distance 20, metric 0, best
  Last update 00:00:17 ago
  * 10.21.2.1, via eth3
</code></pre></div><p><strong>vyos-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">vyos@vyos-02:~$ show ip route 169.254.0.1
Routing entry <span style="color:#66d9ef">for</span> 169.254.0.1/32
  Known via <span style="color:#e6db74">&#34;bgp&#34;</span>, distance 20, metric 0, best
  Last update 00:00:17 ago
  * 10.21.2.3, via eth3
</code></pre></div><h4 id="packet-captures">Packet captures</h4>
<p><strong>ns-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo tcpdump -i eth2 port <span style="color:#ae81ff">53</span>
tcpdump: verbose output suppressed, use -v or -vv <span style="color:#66d9ef">for</span> full protocol decode
listening on eth2, link-type EN10MB <span style="color:#f92672">(</span>Ethernet<span style="color:#f92672">)</span>, capture size <span style="color:#ae81ff">262144</span> bytes
13:26:28.835140 IP 192.168.2.10.57147 &gt; 169.254.0.1.domain: 11108+ <span style="color:#f92672">[</span>1au<span style="color:#f92672">]</span> A? www.google.com. <span style="color:#f92672">(</span>55<span style="color:#f92672">)</span>
13:26:28.835546 IP 169.254.0.1.domain &gt; 192.168.2.10.57147: <span style="color:#ae81ff">11108</span> 1/13/1 A 172.217.19.196 <span style="color:#f92672">(</span>298<span style="color:#f92672">)</span>
</code></pre></div><p><strong>ns-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo tcpdump -i eth2 port <span style="color:#ae81ff">53</span>
tcpdump: verbose output suppressed, use -v or -vv <span style="color:#66d9ef">for</span> full protocol decode
listening on eth2, link-type EN10MB <span style="color:#f92672">(</span>Ethernet<span style="color:#f92672">)</span>, capture size <span style="color:#ae81ff">262144</span> bytes
13:26:30.652179 IP 192.168.3.10.43694 &gt; 169.254.0.1.domain: 52311+ <span style="color:#f92672">[</span>1au<span style="color:#f92672">]</span> A? www.google.com. <span style="color:#f92672">(</span>55<span style="color:#f92672">)</span>
13:26:30.672239 IP 169.254.0.1.domain &gt; 192.168.3.10.43694: <span style="color:#ae81ff">52311</span> 1/0/1 A 172.217.20.100 <span style="color:#f92672">(</span>59<span style="color:#f92672">)</span>
</code></pre></div><h3 id="taking-down-bind">Taking down BIND</h3>
<p>Lets take down BIND, and see if the routing changes at all: -</p>
<p><strong>ns-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo systemctl stop bind9
</code></pre></div><p><strong>vyos-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">vyos@vyos-01:~$ show ip route 169.254.0.1
Routing entry <span style="color:#66d9ef">for</span> 169.254.0.1/32
  Known via <span style="color:#e6db74">&#34;bgp&#34;</span>, distance 200, metric 0, best
  Last update 00:00:25 ago
    10.21.2.3 <span style="color:#f92672">(</span>recursive<span style="color:#f92672">)</span>
  *   10.21.1.1, via eth2
</code></pre></div><p>Oh! It changed. Lets see what ExaBGP had to say: -</p>
<p><strong>ns-01</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo journalctl -xeu exabgp
Dec <span style="color:#ae81ff">05</span> 13:32:04 ns-01 exabgp<span style="color:#f92672">[</span>13277<span style="color:#f92672">]</span>: 13:32:04 | <span style="color:#ae81ff">13277</span>  | api             | route added to neighbor 10.21.2.0 local-ip 10.21.2.1 local-as <span style="color:#ae81ff">64520</span> peer-as <span style="color:#ae81ff">4290001234</span> router-id 10.21.2.1 family-allowed in-open : 169.254.0.1/32 next-hop 10.21.2.1
Dec <span style="color:#ae81ff">05</span> 13:32:24 ns-01 exabgp<span style="color:#f92672">[</span>13277<span style="color:#f92672">]</span>: 13:32:24 | <span style="color:#ae81ff">13277</span>  | api             | route removed from neighbor 10.21.2.0 local-ip 10.21.2.1 local-as <span style="color:#ae81ff">64520</span> peer-as <span style="color:#ae81ff">4290001234</span> router-id 10.21.2.1 family-allowed in-open : 169.254.0.1/32 next-hop 10.21.2.1
</code></pre></div><p>And lets prove it with a packet capture</p>
<p><strong>ns-02</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo tcpdump -i eth2 port <span style="color:#ae81ff">53</span>
tcpdump: verbose output suppressed, use -v or -vv <span style="color:#66d9ef">for</span> full protocol decode
listening on eth2, link-type EN10MB <span style="color:#f92672">(</span>Ethernet<span style="color:#f92672">)</span>, capture size <span style="color:#ae81ff">262144</span> bytes
13:36:02.930962 IP 192.168.2.10.42220 &gt; 169.254.0.1.domain: 2489+ <span style="color:#f92672">[</span>1au<span style="color:#f92672">]</span> A? www.google.com. <span style="color:#f92672">(</span>55<span style="color:#f92672">)</span>
13:36:02.931810 IP 169.254.0.1.domain &gt; 192.168.2.10.42220: <span style="color:#ae81ff">2489</span> 1/0/1 A 172.217.20.100 <span style="color:#f92672">(</span>59<span style="color:#f92672">)</span>
13:36:04.285632 IP 192.168.3.10.56140 &gt; 169.254.0.1.domain: 57423+ <span style="color:#f92672">[</span>1au<span style="color:#f92672">]</span> A? www.google.com. <span style="color:#f92672">(</span>55<span style="color:#f92672">)</span>
13:36:04.285944 IP 169.254.0.1.domain &gt; 192.168.3.10.56140: <span style="color:#ae81ff">57423</span> 1/0/1 A 172.217.20.100 <span style="color:#f92672">(</span>59<span style="color:#f92672">)</span>
</code></pre></div><p>There we go, both clients made it!</p>
<h2 id="summary">Summary</h2>
<p>There is a lot to process here, especially if you are new to BGP and Anycast. The main things to take away from it though are: -</p>
<ul>
<li>Anycast is just an IP that exists in multiple places
<ul>
<li>It is not from a reserved range or anything similar</li>
</ul>
</li>
<li>Using a routing daemon (e.g. FRR) directly on a server is preferable to make it work</li>
<li>Failover at a basic level can be achieved quite easily (i.e. server failure)</li>
<li>To track application state, you need to look at something like ExaBGP</li>
</ul>
<p>Hopefully this will help in understanding, and getting people to play with Anycast more. It can be used for just about anything you want to make highly available. UDP applications work best (due to their connectionless nature), but it is quite possible to use this for TCP. I have seen ExaBGP used to make a RabbitMQ cluster anycast, rather than using DNS or other forms of service discovery.</p>
]]></content>
        </item>
        
        <item>
            <title>Configuration Seasoning: Getting started with Saltstack</title>
            <link>https://yetiops.net/posts/saltstack-introduction/</link>
            <pubDate>Wed, 20 Nov 2019 11:25:41 +0000</pubDate>
            
            <guid>https://yetiops.net/posts/saltstack-introduction/</guid>
            <description>Configuration management is the practice of deploying and managing your application and infrastructure configuration through automated tooling, rather than managing all of your infrastructure manually.
This can cover everything from Linux servers, to network equipment, installing packages to updating existing services. The primary benefits are that you can manage more infrastructure without the operational burden increasing significantly, and that your configuration is consistent across your estate.
There are already a number of tools which achieve this: -</description>
            <content type="html"><![CDATA[<p>Configuration management is the practice of deploying and managing your application and infrastructure configuration through automated tooling, rather than managing all of your infrastructure manually.</p>
<p>This can cover everything from Linux servers, to network equipment, installing packages to updating existing services. The primary benefits are that you can manage more infrastructure without the operational burden increasing significantly, and that your configuration is consistent across your estate.</p>
<p>There are already a number of tools which achieve this: -</p>
<ul>
<li><a href="https://cfengine.com/">CFEngine</a> - One of the first</li>
<li><a href="https://puppet.com/">Puppet</a> - Still one of the most popular</li>
<li><a href="https://www.chef.io/">Chef</a> - Also very popular still</li>
<li><a href="https://www.ansible.com/">Ansible</a> - Probably the most used tool (currently)</li>
<li>and many more&hellip;</li>
</ul>
<p>In this post, I&rsquo;m going to cover another popular choice, <a href="https://www.saltstack.com/">Saltstack</a>. Before that, I&rsquo;ll explain a few of the differences between the most popular tools.</p>
<h2 id="agent-based-vs-agentless">Agent-based vs Agentless</h2>
<h3 id="agent-based">Agent-based</h3>
<p>Agent-based configuration management is where something is installed on the infrastructure being managed that responds to a central &ldquo;master&rdquo; or &ldquo;control&rdquo; server. This can be to report back details about the host, and also makes changes to the host that the central server requests.</p>
<p>For example, if your central server sent an instruction to install <code>tcpdump</code>, the agent would ensure the host goes through the process of installing it. Once the installation is complete, it would report back to the central server to say that the job is finished.</p>
<p>Puppet and Chef are mostly agent-based. They do have the option of running agentless, but the most common way of using them is with a central server.</p>
<h3 id="agentless">Agentless</h3>
<p>Agentless configuration management is where the changes to the infrastructure are made from a remote server. This means that when you run the configuration management tool, it will log in to the infrastructure and make the changes, rather than relying on an agent that is installed on the machine.</p>
<p>This has advantages over Agent-based configuration management, in that infrastructure that cannot have an agent installed (for example, networking equipment) can be managed by the same tooling as other infrastructure (i.e. servers, applications)</p>
<p>The disadvantage of this is your &ldquo;fact gathering&rdquo; process (e.g. what operating system the host runs, what version, CPU architecture) is typically executed on every configuration run. In an agent-based scenario, the control server already knows this due to having a running agent on said Infrastructure.</p>
<p>Ansible is agentless. Other tools have also began to adopt Agentless approaches (Puppet has Bolt, Chef has Solo).</p>
<h2 id="configuration-language">Configuration Language</h2>
<p>Chef and Puppet use a DSL (domain-specific language), meaning a configuration language that is specific to the tool itself. This means that to use Chef and Puppet, you have to learn the language it uses (along with its quirks and caveats).</p>
<p>Ansible uses YAML (Yet Another Markup Language), meaning if you are already familiar with YAML, you&rsquo;ll feel at home with the syntax. It also can make use of Jinja2, to allow for templating, basic conditional logic (if/else, for loops).</p>
<h2 id="what-is-saltstack">What is Saltstack?</h2>
<p>Saltstack is an agent-based configuration management tool. However unlike Chef and Puppet, it does not use a DSL, instead using YAML and Jinja2 (like Ansible). If you have used Ansible at any point, Saltstack isn&rsquo;t too difficult to get started with.</p>
<p>Salt uses the terms <strong>Master</strong> and <strong>Minion</strong> to refer the control server and the agents respectively. As Salt uses agents, its &ldquo;fact gathering&rdquo; process is quicker than using Ansible (in my experience).</p>
<h2 id="states-pillars-and-grains">States, Pillars and Grains</h2>
<p>The three main concepts to understand with Saltstack are States, Pillars and Grains.</p>
<h3 id="states">States</h3>
<p>States are your desired configuration. This can be as simple as installing some packages, to deploying entire application stacks on a server. For Ansible users, these are like your playbooks.</p>
<h3 id="pillars">Pillars</h3>
<p>Pillars are variables, that can be associated with a server, a group of servers, or with all hosts managed by the Salt Master. For anyone familiar with Ansible, these are similar to your <em>host_vars</em> and <em>group_vars</em></p>
<h3 id="grains">Grains</h3>
<p>Grains are the facts/details about the infrastructure you are managing. This could be the operating system (Linux, Windows, Cisco, VyOS), the distribution (Debian, Ubuntu, IOS-XR), the version  (Buster, OpenBSD 6.6) and can even include IPs assigned to interfaces.</p>
<p>By combing your Pillars and Grains, you can take a generic State configuration, and make it specific to the host in question (e.g. what IP an application listens on), while still making it consistent across your infrastructure.</p>
<h2 id="getting-started">Getting started</h2>
<h3 id="master-configuration">Master configuration</h3>
<p>To start using Saltstack, you&rsquo;ll need to set up either a Linux or BSD system to act as your master. Solaris is possible, but is not officially supported. Windows can run as a Minion, but it cannot act as a master.</p>
<p>Many package repositories for Linux and BSD will include packages for Salt. If you want to run on the latest however, go to the <a href="https://repo.saltstack.com/">Saltstack Repository</a> page.</p>
<p>I&rsquo;ll go through the process on a Debian Buster-based master.</p>
<h4 id="prerequisites">Prerequisites</h4>
<p>On Debian Buster (at least the minimal ISO), no version of <code>gnupg</code> is shipped by default. This is required for using <code>apt</code> to add GPG keys, so you can install <code>gnupg2</code> like so: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo apt install gnupg2
</code></pre></div><h4 id="add-the-repository">Add the repository</h4>
<p>First, add the GPG key: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ wget -O - https://repo.saltstack.com/py3/debian/10/amd64/latest/SALTSTACK-GPG-KEY.pub | sudo apt-key add -
</code></pre></div><p>Then, add the repository: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ echo <span style="color:#e6db74">&#34;deb http://repo.saltstack.com/py3/debian/10/amd64/latest buster main&#34;</span> | sudo tee /etc/apt/sources.list.d/saltstack.list
</code></pre></div><p>Run <code>sudo apt update</code> after this, and the Saltstack repo should be added.</p>
<h4 id="install-the-master-package">Install the Master package</h4>
<p>Install the Salt Master package like so: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo apt install salt-master salt-minion
</code></pre></div><p>This will install the master, and also make the master a minion of itself.</p>
<p>While having the Master also a minion of itself may seem counter-intuitive, it means you can control your master in the same way you do the rest of your infrastructure.</p>
<p>If for example you have a base set of applications you like to have installed (e.g. <code>vim</code>, <code>lldpd</code>) , this would also apply to the master as well</p>
<h4 id="configure-the-master">Configure the master</h4>
<p>The configuration for the master is in <code>/etc/salt/master</code>. A very basic configuration would be something like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">file_roots</span>:
  <span style="color:#66d9ef">base</span>:
    - /srv/salt/states

<span style="color:#66d9ef">pillar_roots</span>:
  <span style="color:#66d9ef">base</span>:
    - /srv/salt/pillars
</code></pre></div><p>A number of other options are available, but the above will give you a basic installation that has all of its state files in <code>/srv/salt/states</code>, and all of the pillars in <code>/srv/salt/pillars</code></p>
<p>Apply something like the above, and then run <code>systemctl reload salt-master</code> to apply it.</p>
<h4 id="salt-directory-structure">Salt Directory Structure</h4>
<p>The directory structure I tend to use is a State directory containing all my configuration, and Pillar directory with all my variables in. This is separated out per application, for example: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ls -l /srv/salt/states
total <span style="color:#ae81ff">12</span>
drwxrwx---    <span style="color:#ae81ff">2</span> root     salt-adm      <span style="color:#ae81ff">4096</span> Nov <span style="color:#ae81ff">12</span> 08:00 base
drwxrwx---    <span style="color:#ae81ff">3</span> stuh84   stuh84        <span style="color:#ae81ff">4096</span> Nov  <span style="color:#ae81ff">9</span> 14:12 consul
-rw-rwx---    <span style="color:#ae81ff">1</span> root     salt-adm        <span style="color:#ae81ff">37</span> Oct <span style="color:#ae81ff">31</span> 08:56 top.sls

$ ls -l /srv/salt/pillars                
total <span style="color:#ae81ff">8</span>
drwxrwx---    <span style="color:#ae81ff">2</span> stuh84   stuh84        <span style="color:#ae81ff">4096</span> Nov <span style="color:#ae81ff">10</span> 16:24 consul
-rw-rwx---    <span style="color:#ae81ff">1</span> stuh84   stuh84          <span style="color:#ae81ff">51</span> Nov  <span style="color:#ae81ff">9</span> 12:14 top.sls
</code></pre></div><h3 id="installing-minions">Installing Minions</h3>
<p>Minions are installed in a similar way to masters. Follow the above instructions for adding the Salt repository, and then when it comes to installing packages, only install <code>salt-minion</code> (rather than both the minion and the master.</p>
<h4 id="minion-configuration">Minion Configuration</h4>
<p>The Minion configuration has many options, but at its most basic looks like this: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">master: salt-master.example.com
id: config-01.example.com
nodename: config-01
</code></pre></div><p>Apply something like the above, and then run <code>systemctl reload salt-minion</code> to apply it.</p>
<p>The ID can be generated by Salt itself, or you can override it in the configuration above. The <code>nodename</code> will be a grain that is accessible for your pillar configuration (as well as state configuration if you choose to).</p>
<p>You don&rsquo;t need to have a difference <code>nodename</code> and <code>id</code>, but it can be useful in case you want to keep your configuration across multiple environments consistent. For example, you may have <code>config-01.preprod.example.com</code> and <code>config-01.prod.example.com</code>. You would want these to potentially go to different masters, but if they have the same <code>nodename</code>, you can reuse the same configuration files for Production and Preprod</p>
<p>You also need to make sure that the hostname specified for the master is resolvable. This could be via DNS, or it could be via an entry in your hosts file (e.g. <code>10.1.1.1    salt-master</code>). So long as it can be resolved (and reachable) that is all that is required.</p>
<h3 id="salt-keys">Salt Keys</h3>
<p>Minions are authenticated against the Master. Until they are accepted, configuration cannot be applied to the Minions.</p>
<p>To view the current list of keys, do: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo salt-key -L

Accepted Keys:
config-01
vyos-01
db-01
Denied Keys:
Unaccepted Keys:
meshuggah
</code></pre></div><p>As you can see in the above, it has three accepted keys, and one unaccepted. To accepted this key do: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">$ sudo salt-key -a <span style="color:#e6db74">&#39;meshuggah&#39;</span>
</code></pre></div><p>You can match multiple hosts using regular expressions, or you can use the <code>-A</code> switch instead to accept all currently unaccepted keys (caution: only accept all keys in a situation where you are in complete control of the environment, do NOT do this in production!)</p>
<h3 id="salt-configuration-files">Salt Configuration Files</h3>
<h4 id="top-files">Top Files</h4>
<p>The top files (<code>top.sls</code>) contains the list of states and/or pillars that apply to a group of hosts. The top file in my states directory looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">base</span>:
  <span style="color:#66d9ef">&#39;*&#39;</span>:
    - base
    - consul
</code></pre></div><p>The base designation matches the master configuration that location refers to <code>base:</code>. You can set up multiple state directories, so that you can have a shared set of hosts, but potentially configured by different teams for different applications.</p>
<p>This is very simple, but you can extend how you match hosts with regular expressions. For example: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">base</span>:
  <span style="color:#66d9ef">&#39;*&#39;</span>:
    - base
    - consul

  <span style="color:#66d9ef">&#39;frr*&#39;</span>:
    - frr

  <span style="color:#66d9ef">&#39;frr-01.domain&#39;</span>:
    - frr.bgp-edge
</code></pre></div><p>The second part of the above top file would match any host that has an ID (we&rsquo;ll get to the host IDs in the Minion configuration section) that starts <code>frr</code>, whereas the third part only matches the host <code>frr-01.domain</code>.</p>
<p>It&rsquo;s also worth noting that if you specify just the directory name of your application (e.g. <code>frr</code>), Salt would look for an <code>init.sls</code> file within that directory. This typically would contain all your standard configuration, and may also refer to other state files to finish off configuration.</p>
<p>If you specify something like <code>frr.bgp-edge</code>, the state file that would be applied to the host would be <code>$STATE-DIRECTORY/frr/bgp-edge.sls</code></p>
<p>Similarly, the pillars file is laid out similarly: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">base</span>:
  <span style="color:#66d9ef">&#39;*&#39;</span>:
    - consul.{{ grains[<span style="color:#e6db74">&#39;nodename&#39;</span>] }}
</code></pre></div><p>In here, I&rsquo;m referring to a grain (a fact of the host), in this case the nodename. Looking at the contents of the Consul directory, this will start to make sense: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ls consul
config-01.sls  exodus.sls     meshuggah.sls  teramaze.sls   vanhalen.sls   vpn-01.sls     vyos-01.sls
db-01.sls      git-01.sls     ns-03.sls      testament.sls  vektor.sls     vps-shme.sls
</code></pre></div><p>Within here, I can apply node specific variables, without having to refer to every individual host in the top file.</p>
<h4 id="state-files">State files</h4>
<p>A basic state file looks something like the below: -</p>
<p><strong>/srv/salt/states/base/init.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">base_packages</span>:
  <span style="color:#66d9ef">pkg.installed</span>:
  - <span style="color:#66d9ef">pkgs</span>:
    - tcpdump
    - lldpd
    - jq
    - moreutils
</code></pre></div><p>The first line (<code>base_packages</code>) refers to the name of the action you are taking. The second refers to the action (i.e. installing packages). Below that, there is the <code>pkgs</code> field, followed by a list of packages to install.</p>
<p>The equivalent in Ansible would be: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">tasks</span>:
  - <span style="color:#66d9ef">name</span>: Install base packages
    <span style="color:#66d9ef">package</span>:
      <span style="color:#66d9ef">state</span>: present
      <span style="color:#66d9ef">name</span>:
        - tcpdump
        - lldpd
        - jq
        - moreutils
</code></pre></div><p>In some ways, the Ansible configuration can be easier to understand, but is more verbose. You can often achieve similar results in Salt using less configuration.</p>
<p>Another example would be: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">{% if grains[<span style="color:#e6db74">&#39;os&#39;</span>] != <span style="color:#e6db74">&#39;Alpine&#39;</span> %}
<span style="color:#66d9ef">/etc/systemd/system/consul.service</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://consul/files/consul.service
    - <span style="color:#66d9ef">user</span>: root
    - <span style="color:#66d9ef">group</span>: root
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0644</span>

<span style="color:#66d9ef">service.systemctl_reload</span>:
  <span style="color:#66d9ef">module.run</span>:
    - <span style="color:#66d9ef">onchanges</span>:
      - <span style="color:#66d9ef">file</span>: /etc/systemd/system/consul.service
{% endif %}

<span style="color:#66d9ef">/etc/consul.d/consul.hcl</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://consul/files/consul.hcl
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0640</span>
    - <span style="color:#66d9ef">template</span>: jinja

{% for service in pillar[<span style="color:#e6db74">&#39;consul&#39;</span>][<span style="color:#e6db74">&#39;prometheus_services&#39;</span>] %}
/etc/consul.d/{{ <span style="color:#66d9ef">service }}.hcl</span>:
  <span style="color:#66d9ef">file.managed</span>:
    - <span style="color:#66d9ef">source</span>: salt://consul/files/{{ service }}.hcl
    - <span style="color:#66d9ef">user</span>: consul
    - <span style="color:#66d9ef">group</span>: consul
    - <span style="color:#66d9ef">mode</span>: <span style="color:#ae81ff">0640</span>
{% endfor %}

<span style="color:#66d9ef">consul_service</span>:
  <span style="color:#66d9ef">service.running</span>:
  - <span style="color:#66d9ef">name</span>: consul
  - <span style="color:#66d9ef">enable</span>: True
  - <span style="color:#66d9ef">reload</span>: True
  - <span style="color:#66d9ef">watch</span>:
    - <span style="color:#66d9ef">file</span>: /etc/consul.d/consul.hcl
</code></pre></div><p>This is a snippet from a state file I have that installs Consul, and also adds additional services. The main points to note are the Jinja2 syntax (i.e. the <code>if</code> blocks, the <code>for</code> blocks) which allow you to apply some conditional and looping logic within the state file itself.</p>
<p>In the above, I&rsquo;m saying: -</p>
<ul>
<li>If the OS is not Alpine Linux, install the SystemD unit file (Alpine uses OpenRC rather than SystemD, so this wouldn&rsquo;t work on alpine)</li>
<li>I&rsquo;m applying a templated Consul configuration (as noted with the field <code>template: jinja</code></li>
<li>I&rsquo;m looping through a list of services, as identified by the <code>consul.prometheus_services</code> Pillar, and copying files to the destination host that are named <code>$SERVICE.hcl</code> (e.g. BIND, node_exporter etc)</li>
</ul>
<h4 id="pillars-1">Pillars</h4>
<p>A pillar file is referred to in the top file via its relative location, without the <code>.sls</code> extension (e.g. <code>$PILLAR-DIRECTORY/consul/config-01.sls</code>). Within a state file however, you do not refer to the file directory, instead referring to the Pillar it has created.</p>
<p>This is useful because it means that hosts can have common-named Pillars, (e.g. <code>consul.prometheus_services</code>) but can be host specific. An example is below: -</p>
<p><strong>config-01.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul</span>:
  <span style="color:#66d9ef">bind_int</span>: eth0
  <span style="color:#66d9ef">prometheus_services</span>:
    - node_exporter
</code></pre></div><p>In the above, the list of Prometheus Services available is just the <code>node_exporter</code>. Compare this to the below that is used on my virtual machine host: -</p>
<p><strong>meshuggah.sls</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">consul</span>:
  <span style="color:#66d9ef">bind_int</span>: br0
  <span style="color:#66d9ef">prometheus_services</span>:
    - node_exporter
    - cadvisor
    - libvirt
    - mikrotik
    - traefik
    - bird
</code></pre></div><p>As you can see, a lot more services are available on here. However if you refer back to the state configuration, I didn&rsquo;t need to specify the pillar <code>consul.prometheus_services.meshuggah</code>. This is why pillars are referred to by the Pillar name and structure, rather than the filename.</p>
<h2 id="running-salt">Running Salt</h2>
<p>Once you have put together some of your first state and pillar files, you can begin a Salt run. You have multiple ways of doing this: -</p>
<ul>
<li>Apply a specific state - This can be done per host, across hosts, or matching a regular expression</li>
<li>Apply all states available - As above, you can match all hosts or a subset</li>
</ul>
<h3 id="applying-a-specific-state">Applying a specific state</h3>
<p>To apply a specific state, you can run the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ salt <span style="color:#e6db74">&#39;vps*&#39;</span> state.apply consul
</code></pre></div><p>This would apply the state <code>consul</code>, and would apply it to one host only. You can also do a dry run by appending <code>test=True</code> to the end of the command, so that you can see what would happen if you applied it.</p>
<p>You can apply across all hosts using</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ salt <span style="color:#e6db74">&#39;*&#39;</span> state.apply consul
</code></pre></div><h3 id="applying-all-states">Applying all states</h3>
<p>To apply all states, you use the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ salt <span style="color:#e6db74">&#39;vps*&#39;</span> state.highstate
</code></pre></div><p>This will apply every state that applies to this node in one run. This can be useful to ensure the host has the latest configuration of everything, or if you are bringing up a new host and want to apply all the configuration to bring it into service.</p>
<p>To run against all hosts use: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ salt <span style="color:#e6db74">&#39;*&#39;</span> state.highstate
</code></pre></div><p>Again, apply <code>test=True</code> at the end of the commands (e.g. <code>salt 'vps*' state.highstate test=True</code>) to see what changes would be made, without actually applying them.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ salt <span style="color:#e6db74">&#39;*&#39;</span> state.highstate test<span style="color:#f92672">=</span>True
<span style="color:#f92672">[</span>...<span style="color:#f92672">]</span>
----------
          ID: consul_service
    Function: service.running
        Name: consul
      Result: True
     Comment: The service consul is already running
     Started: 10:17:56.194608
    Duration: 96.399 ms
     Changes:   
----------
          ID: consul_reload
    Function: cmd.run
        Name: consul reload
      Result: None
     Comment: Command <span style="color:#e6db74">&#34;consul reload&#34;</span> would have been executed
     Started: 10:17:56.302612
    Duration: 0.929 ms
     Changes:   

Summary <span style="color:#66d9ef">for</span> db-01
-------------
Succeeded: <span style="color:#ae81ff">11</span> <span style="color:#f92672">(</span>unchanged<span style="color:#f92672">=</span>1<span style="color:#f92672">)</span>
Failed:     <span style="color:#ae81ff">0</span>
-------------
Total states run:     <span style="color:#ae81ff">11</span>
Total run time:    8.170 s
<span style="color:#f92672">[</span>...<span style="color:#f92672">]</span>
</code></pre></div><h2 id="version-control-and-cross-team-usage">Version control and cross team usage</h2>
<p>As with most configuration management systems, you should be managing it using version control. I (like most of the planet it seems) use Git for this, and so I place my <code>/srv/salt</code> directory under Git-based version control. To do this, you would do something like: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ cd /srv/salt
$ git init
$ git remote add git://git@gitserver.com/username/salt.git
$ git add .
$ git commit -m <span style="color:#e6db74">&#34;First commit&#34;</span>
$ git push -u origin master
</code></pre></div><p>In terms of using Salt across teams (especially when using version control), it makes sense to having a Salt Admin group that all the people who administer Salt would be in. By doing this, people do not need to use <code>sudo</code> or <code>doas</code> to update files, but also Git commits would be tied to a username (rather than root).</p>
<p>To do this, first create a group: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo groupadd salt-admin
$ sudo usermod -aG salt-admin $USERNAME
</code></pre></div><p>Once you have all of your admin users in the group, you&rsquo;ll need to apply a File ACL to the Salt directory. The <code>acl</code> package is not installed by default in Debian, so first do <code>sudo apt install acl</code>, and then do the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ cd /srv
$ sudo setfacl -Rm g:salt-admin:rwx salt
$ sudo setfacl -d -Rm u::rwx salt
$ sudo setfacl -d -Rm g:salt-admin:rwx salt
$ sudo setfacl -d -Rm other::--- salt
$ sudo setfacl -d -Rm other::rx salt
$ sudo setfacl -Rm other::--- salt
</code></pre></div><p>When the above is applied, you should see the following if you check using <code>getfacl</code>: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">sudo getfacl /srv/salt  
<span style="color:#75715e">## file: salt</span>
<span style="color:#75715e">## owner: root</span>
<span style="color:#75715e">## group: salt-admin</span>
user::rwx
group::r-x
group:salt-admin:rwx
mask::rwx
other::---
default:user::rwx
default:group::r-x
default:group:salt-admin:rwx
default:mask::rwx
default:other::r-x
</code></pre></div><p>Now any user in the <code>salt-admin</code> group should be able to edit the files in the Salt directory, and also push them to version control (e.g. Git) as their own user.</p>
<h2 id="saltstack-caveats">Saltstack Caveats</h2>
<p>In using Salt, I have come across the following caveats.</p>
<h3 id="python-versions">Python versions</h3>
<p>Upgrading Python versions can break the Salt Master and Minions. You could consider pinning Python package versions or use Python virtual environments to avoid this.</p>
<h3 id="memory-usage">Memory usage</h3>
<p>The memory usage of the Salt Minion can be a bit high. At times I have encountered the minion using more memory than applications on running systems. Bare in mind that it is still only a couple of hundred megabytes at peak, but if you are running virtual machines with 512Mb of RAM this could cause issues.</p>
<h2 id="documentation">Documentation</h2>
<p>The documentation for Salt can be found <a href="https://docs.saltstack.com/en/latest/contents.html">here</a>. It covers everything from the modules you can use (e.g. package installation, file modules, system service control etc) to how to use ad-hoc Salt commands and everything in between.</p>
<p>A combination of this and looking at examples should help you if you&rsquo;re new to configuration management, or already familiar with an existing system.</p>
<h2 id="summary">Summary</h2>
<p>In summary, Saltstack is a great option for configuration management across your estate. I haven&rsquo;t delved into how you can manage network infrastructure with it (using <strong>salt-proxy</strong>) as I haven&rsquo;t used it as of yet. Instead, I would refer to this post by <a href="https://mirceaulinic.net/2018-09-27-network-automation-at-scale/">Mircea Ulinic</a> (who is a noted contributor to Salt) for more information about network automation using Saltstack.</p>
<p>For those of you who already know Ansible, it would be worthwhile to look into Saltstack. The initial setup takes longer, but once this is out of the way, I have found the speed that it applies updates is improved over Ansible, mainly due to the fact that tasks are being ran by the Minions themselves (rather than your host machine).</p>
<p>I still prefer Ansible in the situations where you want to bootstrap a node, but for ongoing management of configuration and applications I would be happy using either Ansible or Salt to do so.</p>
]]></content>
        </item>
        
        <item>
            <title>Prometheus: Monitoring node statistics on other operating systems</title>
            <link>https://yetiops.net/posts/prometheus-monitoring-other-os/</link>
            <pubDate>Thu, 14 Nov 2019 12:18:46 +0000</pubDate>
            
            <guid>https://yetiops.net/posts/prometheus-monitoring-other-os/</guid>
            <description>In my previous posts on Prometheus, most of the monitoring has been geared to either getting metrics from Linux hosts. There have been a couple of exceptions, like the blackbox_exporter (for ICMP/Ping, HTTP(S) and TCP socket monitoring) or the snmp_exporter (more commonly used for networking gear).
Linux is not the only operating system you can monitor node metrics for though. Other operating systems have either their own port of the node_exporter, or via other exporters entirely.</description>
            <content type="html"><![CDATA[<p>In my previous posts on Prometheus, most of the monitoring has been geared to either getting metrics from Linux hosts. There have been a couple of exceptions, like the <code>blackbox_exporter</code> (for ICMP/Ping, HTTP(S) and TCP socket monitoring) or the <code>snmp_exporter</code> (more commonly used for networking gear).</p>
<p>Linux is not the only operating system you can monitor node metrics for though. Other operating systems have either their own port of the <code>node_exporter</code>, or via other exporters entirely.</p>
<p>I&rsquo;m going to cover the following: -</p>
<ul>
<li>OpenWRT</li>
<li>Windows</li>
<li>OpenBSD</li>
<li>FreeBSD</li>
</ul>
<h2 id="openwrt">OpenWRT</h2>
<p>OpenWRT has a Linux base, but is more akin to an embedded version of Linux (running a modified form of BusyBox) than your &ldquo;full fat&rdquo; Linux that you&rsquo;d find in something like Debian or CentOS.</p>
<p>Typically, OpenWRT devices have very little flash storage and RAM. For example, a TP-Link C58 Archer v1 has the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#f92672">[</span>root@tpap<span style="color:#f92672">]</span> ~ <span style="color:#75715e"># free -mh</span>
             total       used       free     shared    buffers     cached
Mem:         <span style="color:#ae81ff">60288</span>      <span style="color:#ae81ff">39632</span>      <span style="color:#ae81ff">20656</span>       <span style="color:#ae81ff">1408</span>       <span style="color:#ae81ff">1168</span>       <span style="color:#ae81ff">7076</span>
-/+ buffers/cache:      <span style="color:#ae81ff">31388</span>      <span style="color:#ae81ff">28900</span>
Swap:            <span style="color:#ae81ff">0</span>          <span style="color:#ae81ff">0</span>          <span style="color:#ae81ff">0</span>

<span style="color:#f92672">[</span>root@tpap<span style="color:#f92672">]</span> ~ <span style="color:#75715e"># df -h</span>
Filesystem                Size      Used Available Use% Mounted on
/dev/root                 2.8M      2.8M         <span style="color:#ae81ff">0</span> 100% /rom
tmpfs                    29.4M      1.2M     28.2M   4% /tmp
/dev/mtdblock5            3.5M      2.3M      1.2M  66% /overlay
overlayfs:/overlay        3.5M      2.3M      1.2M  66% /
tmpfs                   512.0K         <span style="color:#ae81ff">0</span>    512.0K   0% /dev
</code></pre></div><p>In total we have 64M of RAM available, and 4M of flash storage. If you take a look at the <a href="https://github.com/prometheus/node_exporter/releases">releases</a> page of the Prometheus <code>node_exporter</code>, the smallest binary available for Linux is already 7M. So what do we do?</p>
<h3 id="openwrt-node-exporter">OpenWRT Node Exporter</h3>
<p>Thankfully, someone has already written a <code>node_exporter</code> <a href="https://openwrt.org/packages/pkgdata/prometheus-node-exporter-lua">package</a> for OpenWRT. This exposes metrics the same way the standard <code>node_exporter</code> does (a HTTP server on TCP port <strong>9100</strong>)</p>
<p>To install this, you can do it via the OpenWRT GUI in <strong>System</strong> &gt; <strong>Software</strong>, search for <strong>promtheus-node-exporter</strong>, and you&rsquo;ll get a number of results. Alternatively, done via the OpenWRT CLI: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#f92672">[</span>root@tpap<span style="color:#f92672">]</span> ~ <span style="color:#75715e"># opkg list *prometheus*</span>
prometheus-node-exporter-lua - 2019.08.14-1 - Provides node metrics as Prometheus scraping endpoint.
 
 This service is a lightweight rewrite in LUA of the offical Prometheus node_exporter.
prometheus-node-exporter-lua-bmx6 - 2019.08.14-1 - Prometheus node exporter <span style="color:#f92672">(</span>bmx6 links collector<span style="color:#f92672">)</span>
prometheus-node-exporter-lua-bmx7 - 2019.08.14-1 - Prometheus node exporter <span style="color:#f92672">(</span>bmx7 links collector<span style="color:#f92672">)</span>
prometheus-node-exporter-lua-nat_traffic - 2019.08.14-1 - Prometheus node exporter <span style="color:#f92672">(</span>nat_traffic collector<span style="color:#f92672">)</span>
prometheus-node-exporter-lua-netstat - 2019.08.14-1 - Prometheus node exporter <span style="color:#f92672">(</span>netstat collector<span style="color:#f92672">)</span>
prometheus-node-exporter-lua-openwrt - 2019.08.14-1 - Prometheus node exporter <span style="color:#f92672">(</span>openwrt collector<span style="color:#f92672">)</span>
prometheus-node-exporter-lua-textfile - 2019.08.14-1 - Prometheus node exporter <span style="color:#f92672">(</span>textfile collector<span style="color:#f92672">)</span>
prometheus-node-exporter-lua-wifi - 2019.08.14-1 - Prometheus node exporter <span style="color:#f92672">(</span>wifi collector<span style="color:#f92672">)</span>
prometheus-node-exporter-lua-wifi_stations - 2019.08.14-1 - Prometheus node exporter <span style="color:#f92672">(</span>wifi_stations collector<span style="color:#f92672">)</span>
</code></pre></div><p>As you can see by the descriptions, there are a number of extensions to the <code>node_exporter</code> to export additional statistics. If you are only interested in the statistics for Network Address Translation, you can install the <code>prometheus-node-exporter-lua-nat_traffic</code> module. In my installation I have tended to use the <code>openwrt</code>, <code>netstat</code>, <code>wifi</code> and <code>wifi_stations</code> modules.</p>
<p>To install the packages, just do <code>opkg install prometheus-node-exporter-lua</code> and <code>opkg install prometheus-node-exporter-lua-$MODULE</code></p>
<h3 id="configuration">Configuration</h3>
<p>A small amount of configuration is required after you have installed the packages, to make the <code>node_exporter</code> listen on the correct IP: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ cat /etc/config/prometheus-node-exporter-lua 

config prometheus-node-exporter-lua <span style="color:#e6db74">&#39;main&#39;</span>
	option listen_address <span style="color:#e6db74">&#39;192.168.0.245&#39;</span>
	option listen_port <span style="color:#e6db74">&#39;9100&#39;</span>
</code></pre></div><p>Change the <code>listen_address</code> to the IP of your router, and run <code>/etc/init.d/prometheus-node-exporter-lua restart</code></p>
<h3 id="verification">Verification</h3>
<p>You can quickly check if the port is listening correctly with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e">## netstat -an | grep -i 9100</span>
tcp        <span style="color:#ae81ff">0</span>      <span style="color:#ae81ff">0</span> 192.168.0.245:9100      0.0.0.0:*               LISTEN  
</code></pre></div><p>You should then be able to visit the IP address, either via your browser or CuRL: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ curl 192.168.0.245:9100/metrics 
<span style="color:#75715e"># TYPE node_scrape_collector_duration_seconds gauge</span>
<span style="color:#75715e"># TYPE node_scrape_collector_success gauge</span>
<span style="color:#75715e"># TYPE node_nf_conntrack_entries gauge</span>
node_nf_conntrack_entries <span style="color:#ae81ff">51</span>
<span style="color:#75715e"># TYPE node_nf_conntrack_entries_limit gauge</span>
node_nf_conntrack_entries_limit <span style="color:#ae81ff">16384</span>
<span style="color:#f92672">[</span>...<span style="color:#f92672">]</span>
wifi_stations<span style="color:#f92672">{</span>ifname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;wlan1&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>
node_scrape_collector_duration_seconds<span style="color:#f92672">{</span>collector<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;wifi_stations&#34;</span><span style="color:#f92672">}</span> 0.010385036468506
node_scrape_collector_success<span style="color:#f92672">{</span>collector<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;wifi_stations&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>
</code></pre></div><p>Add this to your Prometheus instance using your chosen method of configuration management, and the statistics should appear in Prometheus.</p>
<h2 id="windows">Windows</h2>
<p>Currently, <code>node_exporter</code> is not built for Windows. Instead there is an exporter called the <code>wmi_exporter</code>, which can be obtained <a href="https://github.com/martinlindhe/wmi_exporter">here</a>. This exposes the same kind of metrics you&rsquo;d expect from <code>node_exporter</code> (CPU, disk, services, storage and more).</p>
<p>Binaries are available from the GitHub repository to install it, as well as instructions for installing it. It is also worth noting that the <a href="https://chocolatey.org/packages/prometheus-wmi-exporter.install">Chocolatey</a> package is up to date at the time of writing.</p>
<p>My chosen method of installation is with Chocolatey</p>
<h3 id="chocolatey">Chocolatey</h3>
<p>Chocolatey is a package manager for Windows. It is not included with Windows by default, so you&rsquo;ll need install it from <a href="https://chocolatey.org/docs/installation">here</a>.</p>
<p>Once this is done, you can install <code>wmi_exporter</code> like so: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-powershell" data-lang="powershell">PS C:\Users\Administrator&gt; choco install prometheus-wmi-exporter.install
Chocolatey v0.10.15
Installing the following packages<span style="color:#960050;background-color:#1e0010">:</span>
prometheus-wmi-exporter.install
By installing you accept licenses <span style="color:#66d9ef">for</span> the packages.
Progress<span style="color:#960050;background-color:#1e0010">:</span> Downloading chocolatey-core.extension 1.3.4... 100%
Progress<span style="color:#960050;background-color:#1e0010">:</span> Downloading prometheus-wmi-exporter.install 0.9.0... 100%

chocolatey-core.extension v1.3.4 <span style="color:#66d9ef">[Approved]</span>
chocolatey-core.extension package files install completed. Performing other installation steps.
 Installed/updated chocolatey-core extensions.
 The install of chocolatey-core.extension was successful.
  Software installed to <span style="color:#e6db74">&#39;C:\ProgramData\chocolatey\extensions\chocolatey-core&#39;</span>

prometheus-wmi-exporter.install v0.9.0 <span style="color:#66d9ef">[Approved]</span>
prometheus-wmi-exporter.install package files install completed. Performing other installation steps.
The package prometheus-wmi-exporter.install wants to run <span style="color:#e6db74">&#39;chocolateyinstall.ps1&#39;</span>.
Note<span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">If</span> you don<span style="color:#e6db74">&#39;t run this script, the installation will fail.
</span><span style="color:#e6db74">Note: To confirm automatically next time, use &#39;</span>-y<span style="color:#e6db74">&#39; or consider:
</span><span style="color:#e6db74">choco feature enable -n allowGlobalConfirmation
</span><span style="color:#e6db74">Do you want to run the script?([Y]es/[A]ll - yes to all/[N]o/[P]rint): A
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">Downloading prometheus-wmi-exporter.install 64 bit
</span><span style="color:#e6db74">  from &#39;</span>https<span style="color:#960050;background-color:#1e0010">:</span>//github.com/martinlindhe/wmi_exporter/releases/download/v0.9.0/wmi_exporter-0.9.0-amd64.msi<span style="color:#e6db74">&#39;
</span><span style="color:#e6db74">Progress: 100% - Completed download of C:\Users\Administrator\AppData\Local\Temp\chocolatey\prometheus-wmi-exporter.install\0.9.0\wmi_exporter-0.9.0-amd64.msi (7.18 MB).
</span><span style="color:#e6db74">Download of wmi_exporter-0.9.0-amd64.msi (7.18 MB) completed.
</span><span style="color:#e6db74">Hashes match.
</span><span style="color:#e6db74">Installing prometheus-wmi-exporter.install...
</span><span style="color:#e6db74">prometheus-wmi-exporter.install has been installed.
</span><span style="color:#e6db74">  prometheus-wmi-exporter.install may be able to be automatically uninstalled.
</span><span style="color:#e6db74"> The install of prometheus-wmi-exporter.install was successful.
</span><span style="color:#e6db74">  Software installed as &#39;</span>MSI<span style="color:#960050;background-color:#1e0010">&#39;</span>, install location is likely <span style="color:#66d9ef">default</span>.

Chocolatey installed 2/2 packages.
 See the log <span style="color:#66d9ef">for</span> details (C:\ProgramData\chocolatey\logs\chocolatey.log).
</code></pre></div><h3 id="verification-1">Verification</h3>
<p>To verify that it is running, you can check with: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-powershell" data-lang="powershell">PS C:\Users\Administrator&gt; netstat -an | Select-String 9182

  TCP    0.0.0.0<span style="color:#960050;background-color:#1e0010">:</span>9182           0.0.0.0<span style="color:#960050;background-color:#1e0010">:</span>0              LISTENING
  TCP    [::]<span style="color:#960050;background-color:#1e0010">:</span>9182              [::]<span style="color:#960050;background-color:#1e0010">:</span>0                 LISTENING
</code></pre></div><p>You should now also be able to retrieve metrics from the HTTP endpoint: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ curl 192.168.56.181:9182/metrics 
<span style="color:#f92672">[</span>...<span style="color:#f92672">]</span>
<span style="color:#75715e"># HELP wmi_system_context_switches_total Total number of context switches (WMI source: PerfOS_System.ContextSwitchesPersec)</span>
<span style="color:#75715e"># TYPE wmi_system_context_switches_total counter</span>
wmi_system_context_switches_total <span style="color:#ae81ff">580353</span>
<span style="color:#75715e"># HELP wmi_system_exception_dispatches_total Total number of exceptions dispatched (WMI source: PerfOS_System.ExceptionDispatchesPersec)</span>
<span style="color:#75715e"># TYPE wmi_system_exception_dispatches_total counter</span>
wmi_system_exception_dispatches_total <span style="color:#ae81ff">33872</span>
<span style="color:#75715e"># HELP wmi_system_processor_queue_length Length of processor queue (WMI source: PerfOS_System.ProcessorQueueLength)</span>
<span style="color:#75715e"># TYPE wmi_system_processor_queue_length gauge</span>
wmi_system_processor_queue_length <span style="color:#ae81ff">4</span>
<span style="color:#75715e"># HELP wmi_system_system_calls_total Total number of system calls (WMI source: PerfOS_System.SystemCallsPersec)</span>
<span style="color:#75715e"># TYPE wmi_system_system_calls_total counter</span>
wmi_system_system_calls_total 6.957803e+06
<span style="color:#75715e"># HELP wmi_system_system_up_time System boot time (WMI source: PerfOS_System.SystemUpTime)</span>
<span style="color:#75715e"># TYPE wmi_system_system_up_time gauge</span>
wmi_system_system_up_time 1.573736799799976e+09
<span style="color:#75715e"># HELP wmi_system_threads Current number of threads (WMI source: PerfOS_System.Threads)</span>
<span style="color:#75715e"># TYPE wmi_system_threads gauge</span>
wmi_system_threads <span style="color:#ae81ff">417</span>
<span style="color:#75715e"># HELP wmi_textfile_scrape_error 1 if there was an error opening or reading a file, 0 otherwise</span>
<span style="color:#75715e"># TYPE wmi_textfile_scrape_error gauge</span>
wmi_textfile_scrape_error <span style="color:#ae81ff">0</span>
</code></pre></div><p>Add this into your Prometheus configuration (<code>$IP-OF-WINDOWS-MACHINE:9182</code>) and you should see metrics available.</p>
<h3 id="dashboards">Dashboards</h3>
<p>A number of Grafana dashboards are available on the Grafana Dashboards site for the <code>wmi_exporter</code>. I personally like <a href="https://grafana.com/grafana/dashboards/6593">this</a> one.</p>
<h2 id="openbsd">OpenBSD</h2>
<p>On the latest available stable version of OpenBSD (<code>6.6</code> at the time of writing), <code>node_exporter</code> is available in the OpenBSD package repository by default (rather than in the Ports tree).</p>
<p>To check, do: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">openbsd-util$ pkg_info -Q node_exporter
node_exporter-0.18.0
</code></pre></div><p>To install: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">openbsd-util$ doas pkg_add node_exporter
doas <span style="color:#f92672">(</span>stuh84@openbsd-util<span style="color:#f92672">)</span> password: 
quirks-3.182 signed on 2019-11-13T20:45:48Z
quirks-3.182: ok
node_exporter-0.18.0: ok
The following new rcscripts were installed: /etc/rc.d/node_exporter
See rcctl<span style="color:#f92672">(</span>8<span style="color:#f92672">)</span> <span style="color:#66d9ef">for</span> details.
</code></pre></div><p>And to run it: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">openbsd-util$ doas rcctl enable node_exporter
doas <span style="color:#f92672">(</span>stuh84@openbsd-util<span style="color:#f92672">)</span> password: 
openbsd-util$ doas rcctl start node_exporter 
doas <span style="color:#f92672">(</span>stuh84@openbsd-util<span style="color:#f92672">)</span> password: 
node_exporter<span style="color:#f92672">(</span>ok<span style="color:#f92672">)</span>
</code></pre></div><h3 id="verification-2">Verification</h3>
<p>As before, check with netstat: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">openbsd-util$ netstat -an | grep -i <span style="color:#ae81ff">9100</span>
tcp          <span style="color:#ae81ff">0</span>      <span style="color:#ae81ff">0</span>  *.9100                 *.*                    LISTEN
</code></pre></div><p>Check with CuRL or your browser: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">curl 192.168.56.182:9100/metrics       
<span style="color:#75715e"># HELP go_gc_duration_seconds A summary of the GC invocation durations.</span>
<span style="color:#75715e"># TYPE go_gc_duration_seconds summary</span>
go_gc_duration_seconds<span style="color:#f92672">{</span>quantile<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;0&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">0</span>
go_gc_duration_seconds<span style="color:#f92672">{</span>quantile<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;0.25&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">0</span>
go_gc_duration_seconds<span style="color:#f92672">{</span>quantile<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;0.5&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">0</span>
go_gc_duration_seconds<span style="color:#f92672">{</span>quantile<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;0.75&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">0</span>
go_gc_duration_seconds<span style="color:#f92672">{</span>quantile<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;1&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">0</span>
go_gc_duration_seconds_sum <span style="color:#ae81ff">0</span>
<span style="color:#f92672">[</span>...<span style="color:#f92672">]</span>
<span style="color:#75715e"># HELP node_network_transmit_bytes_total Network device statistic transmit_bytes.</span>
<span style="color:#75715e"># TYPE node_network_transmit_bytes_total counter</span>
node_network_transmit_bytes_total<span style="color:#f92672">{</span>device<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;enc0&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">0</span>
node_network_transmit_bytes_total<span style="color:#f92672">{</span>device<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;hvn0&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">354191</span>
node_network_transmit_bytes_total<span style="color:#f92672">{</span>device<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;hvn1&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">94687</span>
node_network_transmit_bytes_total<span style="color:#f92672">{</span>device<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;lo0&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">0</span>
node_network_transmit_bytes_total<span style="color:#f92672">{</span>device<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pflog0&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">0</span>
<span style="color:#f92672">[</span>...<span style="color:#f92672">]</span>
</code></pre></div><h3 id="metrics">Metrics</h3>
<p>The metrics you will get out of OpenBSD compared to Linux differ, due to differences in how they are calculated. A good example is <strong>Load</strong>.</p>
<p>In the Linux world, load is a metric that takes into account CPU and demand for disks (see Brendan Gregg&rsquo;s brilliant and mind-bending <a href="http://www.brendangregg.com/blog/2017-08-08/linux-load-averages.html">blog post</a> on this). In the BSD world however, Load is purely a CPU metric.</p>
<p>If you intend to graph metrics across Linux and BSD systems, the same queries will give drastically different results.</p>
<h2 id="freebsd">FreeBSD</h2>
<p>On the latest available stable version of FreeBSD (<code>12.1</code> at the time of writing), <code>node_exporter</code> is available in the FreeBSD package repository by default (rather than in the Ports tree).</p>
<p>To check: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ pkg search node_exporter
node_exporter-0.18.1           Prometheus exporter <span style="color:#66d9ef">for</span> machine metrics
</code></pre></div><p>To install: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo pkg install node_exporter
Updating FreeBSD repository catalogue...
FreeBSD repository is up to date.
All repositories are up to date.
The following <span style="color:#ae81ff">1</span> package<span style="color:#f92672">(</span>s<span style="color:#f92672">)</span> will be affected <span style="color:#f92672">(</span>of <span style="color:#ae81ff">0</span> checked<span style="color:#f92672">)</span>:

New packages to be INSTALLED:
	node_exporter: 0.18.1

Number of packages to be installed: <span style="color:#ae81ff">1</span>

The process will require <span style="color:#ae81ff">10</span> MiB more space.
<span style="color:#ae81ff">3</span> MiB to be downloaded.

Proceed with this action? <span style="color:#f92672">[</span>y/N<span style="color:#f92672">]</span>: y
<span style="color:#f92672">[</span>1/1<span style="color:#f92672">]</span> Fetching node_exporter-0.18.1.txz: 100%    <span style="color:#ae81ff">3</span> MiB   1.1MB/s    00:03    
Checking integrity... <span style="color:#66d9ef">done</span> <span style="color:#f92672">(</span><span style="color:#ae81ff">0</span> conflicting<span style="color:#f92672">)</span>
<span style="color:#f92672">[</span>1/1<span style="color:#f92672">]</span> Installing node_exporter-0.18.1...
<span style="color:#f92672">[</span>1/1<span style="color:#f92672">]</span> Extracting node_exporter-0.18.1: 100%
<span style="color:#f92672">=====</span>
Message from node_exporter-0.18.1:

--
If upgrading from a version of node_exporter &lt;0.15.0 you<span style="color:#960050;background-color:#1e0010">&#39;</span>ll need to update any
custom command line flags that you may have set as it now requires a
double-dash <span style="color:#f92672">(</span>--flag<span style="color:#f92672">)</span> instead of a single dash <span style="color:#f92672">(</span>-flag<span style="color:#f92672">)</span>.
The collector flags in 0.15.0 have now been replaced with individual boolean
flags and the -collector.procfs<span style="color:#e6db74">`</span> and -collector.sysfs<span style="color:#e6db74">`</span> flags have been renamed
to --path.procfs and --path.sysfs respectively.
</code></pre></div><p>To enable and start: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ sudo sysrc node_exporter_enable<span style="color:#f92672">=</span>YES
node_exporter_enable:  -&gt; YES
$ sudo service node_exporter start
Starting node_exporter.
</code></pre></div><h3 id="verification-3">Verification</h3>
<p>Again, check with netstat: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ netstat -an | grep -i <span style="color:#ae81ff">9100</span>
tcp46      <span style="color:#ae81ff">0</span>      <span style="color:#ae81ff">0</span> *.9100                 *.*                    LISTEN  
</code></pre></div><p>Check with CuRL or your browser: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">curl 192.168.56.182:9100/metrics       
<span style="color:#75715e"># HELP go_gc_duration_seconds A summary of the GC invocation durations.</span>
<span style="color:#75715e"># TYPE go_gc_duration_seconds summary</span>
go_gc_duration_seconds<span style="color:#f92672">{</span>quantile<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;0&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">0</span>
go_gc_duration_seconds<span style="color:#f92672">{</span>quantile<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;0.25&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">0</span>
go_gc_duration_seconds<span style="color:#f92672">{</span>quantile<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;0.5&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">0</span>
go_gc_duration_seconds<span style="color:#f92672">{</span>quantile<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;0.75&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">0</span>
<span style="color:#f92672">[</span>...<span style="color:#f92672">]</span>
<span style="color:#75715e"># HELP node_uname_info Labeled system information as provided by the uname system call.</span>
<span style="color:#75715e"># TYPE node_uname_info gauge</span>
node_uname_info<span style="color:#f92672">{</span>domainname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(none)&#34;</span>,machine<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;amd64&#34;</span>,nodename<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;freebsd-util&#34;</span>,release<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;12.1-RELEASE&#34;</span>,sysname<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;FreeBSD&#34;</span>,version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;FreeBSD 12.1-RELEASE r354233 GENERIC &#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">1</span>
<span style="color:#75715e"># HELP promhttp_metric_handler_requests_in_flight Current number of scrapes being served.</span>
<span style="color:#75715e"># TYPE promhttp_metric_handler_requests_in_flight gauge</span>
promhttp_metric_handler_requests_in_flight <span style="color:#ae81ff">1</span>
<span style="color:#75715e"># HELP promhttp_metric_handler_requests_total Total number of scrapes by HTTP status code.</span>
<span style="color:#75715e"># TYPE promhttp_metric_handler_requests_total counter</span>
promhttp_metric_handler_requests_total<span style="color:#f92672">{</span>code<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;200&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">0</span>
promhttp_metric_handler_requests_total<span style="color:#f92672">{</span>code<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;500&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">0</span>
promhttp_metric_handler_requests_total<span style="color:#f92672">{</span>code<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;503&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">0</span>
</code></pre></div><h3 id="metrics-1">Metrics</h3>
<p>As noted in the OpenBSD section, the metrics you will scrape from FreeBSD are calculated differently than in Linux. Take this into account when building your queries and dashboards, especially for dashboards mixing BSD and Linux metrics.</p>
<h2 id="other-systems">Other systems</h2>
<p>In future posts I&rsquo;ll try and cover other operating systems (e.g. Illumos).</p>
<p>Hopefully this post has given you a good indication of the usual steps you take to verify an installation of <code>node_exporter</code> (or <code>wmi_exporter</code> for Windows). On other systems it should just be a case of finding out where to source <code>node_exporter</code> from, whether that is through binaries, package management or building from source.</p>
<h2 id="update-28112019">Update (28/11/2019)</h2>
<p>Not long after I posted this blog, a project for monitoring FreeBSD <code>GEOM</code> devices appeared called <code>gstat_exporter</code>. If you are wanting to monitor ZFS and Disk Utilization, this is a great way to start.</p>
<p>Go look at this excellent blog post &ldquo;<a href="https://blog.tyk.nu/blog/all-in-a-days-work-prometheus-gstat_exporter-and-grafana-dashboard/">All in a days work: Prometheus gstat_exporter and Grafana Dashboard</a>&rdquo; by <a href="https://blog.tyk.nu/about/">Tykling</a></p>
]]></content>
        </item>
        
        <item>
            <title>Prometheus: Discovering Services with Consul</title>
            <link>https://yetiops.net/posts/prometheus-consul-node_exporter/</link>
            <pubDate>Tue, 05 Nov 2019 12:22:02 +0000</pubDate>
            
            <guid>https://yetiops.net/posts/prometheus-consul-node_exporter/</guid>
            <description>In my previous post, I detailed moving my home monitoring over to Prometheus. I&amp;rsquo;ve gained huge insights into my home network (and a few external services I rely on), and have been very happy with it.
Adding new endpoints has been pretty straightforward. I have been using Ansible to generate the prometheus.yml configuration file, using variables to generate each section of the scrape configuration. This has worked equally well for both services exposing native Prometheus endpoints (e.</description>
            <content type="html"><![CDATA[<p>In my previous post, I detailed moving my home monitoring over to Prometheus. I&rsquo;ve gained huge insights into my home network (and a few external services I rely on), and have been very happy with it.</p>
<p>Adding new endpoints has been pretty straightforward. I have been using Ansible to generate the <code>prometheus.yml</code> configuration file, using variables to generate each section of the scrape configuration. This has worked equally well for both services exposing native Prometheus endpoints (e.g. Cadvisor or Traefik) and for the numerous exporters I am running.</p>
<p>The issue with this approach is that it requires reloading the Prometheus configuration every time I add a service and/or an endpoint. It also requires a central point of configuration management to decide what is monitored, rather than hosts notifying <em>something</em> that it has a Prometheus-compatible metrics endpoint.</p>
<p>Enter <strong>Service Discovery</strong></p>
<h2 id="service-discovery">Service Discovery</h2>
<p>As Wikipedia <a href="https://en.wikipedia.org/wiki/Service_discovery">describes</a>: -</p>
<blockquote>
<p>Service discovery is the automatic detection of devices and services offered by these devices on a computer network.</p>
</blockquote>
<p>Put another way, it allows an application to dynamically discover services, rather than the services being statically defined in the applications configuration.</p>
<p>For Prometheus, there a number of methods it can use for service discovery. These range from talking to cloud provider APIs (like AWS, Azure, GCE), DNS-based discovery (using SRV records) to querying the Kubernetes API for running services.</p>
<p>I have chosen <a href="https://www.consul.io/">Consul</a>, a configuration and service store by Hashicorp (who also created Terraform, Packer and Vagrant).</p>
<h2 id="consul">Consul</h2>
<p>Consul uses a Server/Client-style approach. The recommendation for production usage is a minimum of three servers (for <a href="https://en.wikipedia.org/wiki/Quorum_(distributed_computing)">quorum</a>), however in a home environment you can run it as low as one.</p>
<p>Agents register with the server(s), and will supply a list of services that are running on them. Adding additional services can be done via the Consul CLI, the API, or you can do it using files that are in the Consul configuration directory.</p>
<p>The Hashicorp documentation on setting up a cluster is very good, so I would advise reading <a href="https://learn.hashicorp.com/consul/datacenter-deploy/deployment-guide">this</a> and following it if you want to set up a cluster of your own. I made a couple of changes to it to suit my environment, as detailed below.</p>
<h3 id="server-configuration">Server Configuration</h3>
<p>My setup on the server is configured as such: -</p>
<p><strong>/etc/systemd/system/consul.service</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-systemd" data-lang="systemd"><span style="color:#66d9ef">[Unit]</span>
<span style="color:#a6e22e">Description</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;HashiCorp Consul - A service mesh solution&#34;</span>
<span style="color:#a6e22e">Documentation</span><span style="color:#f92672">=</span><span style="color:#e6db74">https://www.consul.io/</span>
<span style="color:#a6e22e">Requires</span><span style="color:#f92672">=</span><span style="color:#e6db74">network-online.target</span>
<span style="color:#a6e22e">After</span><span style="color:#f92672">=</span><span style="color:#e6db74">network-online.target</span>
<span style="color:#a6e22e">ConditionFileNotEmpty</span><span style="color:#f92672">=</span><span style="color:#e6db74">/etc/consul.d/consul.hcl</span>

<span style="color:#66d9ef">[Service]</span>
<span style="color:#a6e22e">Type</span><span style="color:#f92672">=</span><span style="color:#e6db74">notify</span>
<span style="color:#a6e22e">User</span><span style="color:#f92672">=</span><span style="color:#e6db74">consul</span>
<span style="color:#a6e22e">Group</span><span style="color:#f92672">=</span><span style="color:#e6db74">consul</span>
<span style="color:#a6e22e">ExecStart</span><span style="color:#f92672">=</span><span style="color:#e6db74">/usr/local/bin/consul agent -config-dir=/etc/consul.d/</span>
<span style="color:#a6e22e">ExecReload</span><span style="color:#f92672">=</span><span style="color:#e6db74">/usr/local/bin/consul reload</span>
<span style="color:#a6e22e">KillMode</span><span style="color:#f92672">=</span><span style="color:#e6db74">process</span>
<span style="color:#a6e22e">Restart</span><span style="color:#f92672">=</span><span style="color:#e6db74">on-failure</span>
<span style="color:#a6e22e">LimitNOFILE</span><span style="color:#f92672">=</span><span style="color:#e6db74">65536</span>

<span style="color:#66d9ef">[Install]</span>
<span style="color:#a6e22e">WantedBy</span><span style="color:#f92672">=</span><span style="color:#e6db74">multi-user.target</span>
</code></pre></div><p>The above runs the Consul binary, and looks to the <code>/etc/consul.d/</code> directory for configuration</p>
<p><strong>/etc/consul.d/consul.hcl</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#a6e22e">datacenter</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;noisepalace&#34;</span>
<span style="color:#a6e22e">data_dir</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/opt/consul&#34;</span>
<span style="color:#a6e22e">encrypt</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${CONSUL_ENCRYPTION_KEY}&#34;</span>
<span style="color:#a6e22e">retry_join</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">[&#34;192.168.0.7&#34;]</span>
<span style="color:#a6e22e">bind_addr</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;192.168.0.7&#34;</span>

<span style="color:#a6e22e">performance {</span>
  <span style="color:#a6e22e">raft_multiplier</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">1</span>
<span style="color:#a6e22e">}</span>
</code></pre></div><p>The <code>bind_addr</code> is statically set in this, because the server runs a number of Docker containers too, so Consul doesn&rsquo;t know which interface to bind to without it. The <code>retry_join</code> parameter is used to discover the server. In this case, it is discovering itself, but the Consul server also runs the client too, so it needs to know how to contact the server.</p>
<p>To create the encryption key, use <code>consul keygen</code>. This key will be used by all your nodes in the &ldquo;datacenter&rdquo; (in this case, the confines of my house).</p>
<p><strong>/etc/consul.d/server.hcl</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-ini" data-lang="ini"><span style="color:#a6e22e">server</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">true</span>
<span style="color:#a6e22e">bootstrap_expect</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">1</span>
<span style="color:#a6e22e">ui</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">true</span>
<span style="color:#a6e22e">bind_addr</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;192.168.0.7&#34;</span>
<span style="color:#a6e22e">client_addr</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>
</code></pre></div><p>In the above, the <code>ui</code> variable is used to enable the Consul Web UI. The <code>bootstrap_expect</code> variable is used to say how many servers are required to form the first Consul cluster. I have set it to 1, to allow a cluster with a single server.</p>
<p>The <code>client_addr</code> variable is used to say what address the API and UI listen on. In a production environment you will want to lock this down to one IP.</p>
<h3 id="client-configuration">Client Configuration</h3>
<p>The Client configuration is exactly the same as described in the Server Configuration section, except without the <strong>server.hcl</strong> file. You will want to change the <code>bind_addr</code> variable to the IP of the host it is running on (or remove it entirely if it only has one interface with an IP on).</p>
<h3 id="verification">Verification</h3>
<p>After you have configured the cluster, you should be able to see something like this on the master: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ consul members         
Node                        Address             Status  Type    Build  Protocol  DC           Segment
meshuggah                   192.168.0.7:8301    alive   server  1.6.1  <span style="color:#ae81ff">2</span>         noisepalace  &lt;all&gt;
config-01                   192.168.0.220:8301  alive   client  1.6.1  <span style="color:#ae81ff">2</span>         noisepalace  &lt;default&gt;
db-01                       192.168.0.229:8301  alive   client  1.6.1  <span style="color:#ae81ff">2</span>         noisepalace  &lt;default&gt;
exodus                      192.168.0.252:8301  alive   client  1.6.1  <span style="color:#ae81ff">2</span>         noisepalace  &lt;default&gt;
git-01                      192.168.0.223:8301  alive   client  1.6.1  <span style="color:#ae81ff">2</span>         noisepalace  &lt;default&gt;
ns-03                       192.168.0.221:8301  alive   client  1.6.1  <span style="color:#ae81ff">2</span>         noisepalace  &lt;default&gt;
teramaze                    192.168.0.236:8301  alive   client  1.6.1  <span style="color:#ae81ff">2</span>         noisepalace  &lt;default&gt;
testament                   192.168.0.253:8301  alive   client  1.6.1  <span style="color:#ae81ff">2</span>         noisepalace  &lt;default&gt;
vanhalen                    192.168.0.3:8301    alive   client  1.6.1  <span style="color:#ae81ff">2</span>         noisepalace  &lt;default&gt;
vektor                      192.168.0.251:8301  alive   client  1.6.1  <span style="color:#ae81ff">2</span>         noisepalace  &lt;default&gt;
vpn-01                      192.168.0.222:8301  alive   client  1.6.1  <span style="color:#ae81ff">2</span>         noisepalace  &lt;default&gt;
vps-shme                    192.168.100.1:8301  alive   client  1.6.1  <span style="color:#ae81ff">2</span>         noisepalace  &lt;default&gt;
vyos-01                     192.168.0.225:8301  alive   client  1.6.1  <span style="color:#ae81ff">2</span>         noisepalace  &lt;default&gt;
</code></pre></div><p>Excuse the mixed naming scheme, as I&rsquo;m halfway between everything being named after bands (physical machines) and purpose (virtual machines). I will standardize at some point&hellip;</p>
<p>The Consul UI should also be available at this point, which you&rsquo;ll be able to see at <code>https://${YOUR-SERVER-IP}:8500/ui/</code></p>
<p><img src="/img/prometheus/consul-ui.png" alt="Consul Interface"></p>
<h2 id="where-are-my-services">Where are my services?</h2>
<p>Once Consul is setup, it needs to know about services you want to expose on each client. As noted, these can be added via the Consul CLI (using the <code>consul service register</code> directive), via the API, or using files in the Consul configuration directory.</p>
<p>To add a service via a file, it needs to be formatted something like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;service&#34;</span>:
  {<span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;node_exporter&#34;</span>,
   <span style="color:#f92672">&#34;tags&#34;</span>: [<span style="color:#e6db74">&#34;node_exporter&#34;</span>, <span style="color:#e6db74">&#34;prometheus&#34;</span>],
   <span style="color:#f92672">&#34;port&#34;</span>: <span style="color:#ae81ff">9100</span>
  }
}
</code></pre></div><p>The tags are optional, but they are useful in identifying services. They can also be used to filter what services Prometheus will use (which I&rsquo;ll explain later in this post).</p>
<p>If the above is added in your <code>/etc/consul.d</code> directory on your agents, you can then run <code>consul reload</code> for the new service to be picked up. The agent will then inform the Server that this service exists on this node.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ consul catalog services                      
consul
node_exporter

$ consul catalog nodes -service<span style="color:#f92672">=</span>node_exporter     
Node                        ID        Address        DC
config-01                   6652b349  192.168.0.220  noisepalace
db-01                       b403eac7  192.168.0.229  noisepalace
exodus.noisepalace.home     e378b4ed  192.168.0.252  noisepalace
git-01                      9f30a62e  192.168.0.223  noisepalace
glap                                  192.168.0.234  noisepalace
meshuggah                   2d6e78b5  192.168.0.7    noisepalace
ns-03                       e300899f  192.168.0.221  noisepalace
teramaze.noisepalace.home   c10a3be9  192.168.0.236  noisepalace
testament.noisepalace.home  0b1c0103  192.168.0.253  noisepalace
tpap                                  192.168.0.245  noisepalace
vanhalen.noisepalace.home   b74d9bd5  192.168.0.3    noisepalace
vektor                      ad374347  192.168.0.251  noisepalace
vpn-01                      15c80eaa  192.168.0.222  noisepalace
vps-shme                    9a29d4ca  192.168.100.1  noisepalace
vyos-01                     <span style="color:#ae81ff">15659201</span>  192.168.0.225  noisepalace
</code></pre></div><p>As the Agent is informing the Server of what services it has, rather than the Server defining what services exist on the agents, this forms the basis of automatic service discovery. It is no longer dependent on what a Server has configured.</p>
<h3 id="services-without-an-agent">Services without an agent</h3>
<p>Not all services you will monitor can run an agent. For example, you can install a Node Exporter on OpenWRT (written in Lua rather than Go), but OpenWRT does not support running Consul.</p>
<p>To add external services, I have found adding them through the API is the easiest method.</p>
<p>Create a file that looks like the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;Node&#34;</span>: <span style="color:#e6db74">&#34;tpap&#34;</span>,
  <span style="color:#f92672">&#34;Address&#34;</span>: <span style="color:#e6db74">&#34;192.168.0.245&#34;</span>,
  <span style="color:#f92672">&#34;NodeMeta&#34;</span>: {
    <span style="color:#f92672">&#34;external-node&#34;</span>: <span style="color:#e6db74">&#34;true&#34;</span>,
    <span style="color:#f92672">&#34;external-probe&#34;</span>: <span style="color:#e6db74">&#34;true&#34;</span>
  },
  <span style="color:#f92672">&#34;Service&#34;</span>: {
    <span style="color:#f92672">&#34;ID&#34;</span>: <span style="color:#e6db74">&#34;node_exporter&#34;</span>,
    <span style="color:#f92672">&#34;Service&#34;</span>: <span style="color:#e6db74">&#34;node_exporter&#34;</span>,
    <span style="color:#f92672">&#34;Tags&#34;</span>: [<span style="color:#e6db74">&#34;node_exporter&#34;</span>, <span style="color:#e6db74">&#34;prometheus&#34;</span>],
    <span style="color:#f92672">&#34;Port&#34;</span>: <span style="color:#ae81ff">9100</span>
  },
  <span style="color:#f92672">&#34;Checks&#34;</span>: [
    {
      <span style="color:#f92672">&#34;Name&#34;</span>: <span style="color:#e6db74">&#34;http-check&#34;</span>,
      <span style="color:#f92672">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;passing&#34;</span>,
      <span style="color:#f92672">&#34;Definition&#34;</span>: {
        <span style="color:#f92672">&#34;http&#34;</span>: <span style="color:#e6db74">&#34;http://192.168.0.245:9100&#34;</span>,
        <span style="color:#f92672">&#34;interval&#34;</span>: <span style="color:#e6db74">&#34;30s&#34;</span>
      }
    }
  ]
}
</code></pre></div><p>The above defines the External Node (in this case, an OpenWRT router), the service running on it, and a basic health check.</p>
<p>To apply this to Consul, run <code>curl --request PUT --data @external.json localhost:8500/v1/catalog/register</code>. This needs to run on one of your Consul servers.</p>
<p>Afterwards, the service will appear in your Consul catalog.</p>
<h2 id="prometheus-integration">Prometheus Integration</h2>
<p>To start making use of Consul with Prometheus, the <code>prometheus.yml</code> file will need updating with the details of your Consul server(s). An example configuration is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">global</span>:
  <span style="color:#66d9ef">scrape_interval</span>:     15s <span style="color:#75715e"># Set the scrape interval to every 15 seconds. Default is every 1 minute.</span>
  <span style="color:#66d9ef">evaluation_interval</span>: 15s <span style="color:#75715e"># Evaluate rules every 15 seconds. The default is every 1 minute.</span>

<span style="color:#66d9ef">scrape_configs</span>:
  - <span style="color:#66d9ef">job_name</span>: <span style="color:#e6db74">&#39;prometheus&#39;</span>
    <span style="color:#66d9ef">static_configs</span>:
    - <span style="color:#66d9ef">targets</span>: [<span style="color:#e6db74">&#39;localhost:9090&#39;</span>]

  - <span style="color:#66d9ef">job_name</span>: <span style="color:#e6db74">&#39;consul&#39;</span>
    <span style="color:#66d9ef">consul_sd_configs</span>:
      - <span style="color:#66d9ef">server</span>: <span style="color:#e6db74">&#39;192.168.0.7:8500&#39;</span>
    <span style="color:#66d9ef">relabel_configs</span>:
      - <span style="color:#66d9ef">source_labels</span>: [__meta_consul_tags]
        <span style="color:#66d9ef">regex</span>: .<span style="color:#75715e">*,prometheus,.*</span>
        <span style="color:#66d9ef">action</span>: keep
      - <span style="color:#66d9ef">source_labels</span>: [__meta_consul_service]
        <span style="color:#66d9ef">target_label</span>: job
</code></pre></div><p>The first job is the standard Prometheus endpoint. The second job however talks to Consul and retrieves the services.</p>
<p>As noted earlier, you can use <strong>Tags</strong> to filter what services are used. By default Consul adds the <code>consul</code> service into its catalog of existing services, which does not expose a Prometheus-compatible endpoint natively (it can be enabled, but it requires some changes to the default endpoint).</p>
<p>By using tags, you can filter out the Consul service. You can also have different scrape configurations for different kinds of jobs, while still using Consul for discovery of the services.</p>
<h3 id="different-kinds-of-jobs">Different kinds of jobs?</h3>
<p>A good example of where you might want to have different scrape configurations, using the Consul cluster, is something like the Blackbox Exporter. The exporter itself runs on a server, but it is effectively a proxy for HTTP(S), ICMP, DNS, TCP and UDP requests to an arbitrary list of endpoints (e.g. Google DNS, your ISP-provided home router, a Roku smart TV device etc).</p>
<p>The Blackbox Exporter configuration in Prometheus requires relabelling the endpoint you&rsquo;re targeting to be proxied via the exporter itself, e.g.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  - <span style="color:#66d9ef">job_name</span>: <span style="color:#e6db74">&#39;blackbox&#39;</span>
    <span style="color:#66d9ef">metrics_path</span>: /probe
    <span style="color:#66d9ef">params</span>:
      <span style="color:#66d9ef">module</span>: [icmp_ipv4]
    <span style="color:#66d9ef">static_configs</span>:
      - <span style="color:#66d9ef">targets</span>:
        - <span style="color:#ae81ff">192.168.0.1</span>
        - <span style="color:#ae81ff">192.168.0.3</span>
        - <span style="color:#ae81ff">192.168.0.7</span>
        - <span style="color:#ae81ff">192.168.0.40</span>
        - <span style="color:#ae81ff">192.168.0.42</span>
        - <span style="color:#ae81ff">192.168.0.220</span>
    <span style="color:#66d9ef">relabel_configs</span>:
      - <span style="color:#66d9ef">source_labels</span>: [__address__]
        <span style="color:#66d9ef">target_label</span>: __param_target
      - <span style="color:#66d9ef">source_labels</span>: [__param_target]
        <span style="color:#66d9ef">target_label</span>: instance
      - <span style="color:#66d9ef">target_label</span>: __address__
        <span style="color:#66d9ef">replacement</span>: <span style="color:#ae81ff">127.0.0.1</span>:<span style="color:#ae81ff">9115</span>
</code></pre></div><p>This style of configuration is different from what you&rsquo;d require for the Node Exporter, hence you would use a different scrape job to get the endpoints from Consul.</p>
<h2 id="prometheus-targets">Prometheus Targets</h2>
<p>Looking at the targets and service discovery section in the Prometheus UI, you&rsquo;ll see the following when it scrapes from Consul</p>
<p><img src="/img/prometheus/prometheus-consul-sd.png" alt="Prometheus Consul Service Discovery"></p>
<p><img src="/img/prometheus/prometheus-consul-targets.png" alt="Prometheus Consul Targets"></p>
<p>Now, whenever a new machine is added in my network (running Node Exporter and the Consul Agent), Prometheus will pick it up on it&rsquo;s next scrape of Consul.</p>
<h2 id="services-discovered">Services Discovered!</h2>
<p>I&rsquo;m still in the process of moving my home Prometheus setup to use Consul, but already I&rsquo;m benefiting from it.</p>
<p>I&rsquo;m also deploying Consul and the services via Salt rather than Ansible (although I&rsquo;ll save that for another blog post&hellip;), meaning the moment a machine is added to the Salt master, it will soon be monitored by Prometheus. Perfect!</p>
]]></content>
        </item>
        
        <item>
            <title>Prometheus: Embracing the Exporter Life</title>
            <link>https://yetiops.net/posts/prometheus-the-exporter-life/</link>
            <pubDate>Sun, 06 Oct 2019 09:23:11 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/prometheus-the-exporter-life/</guid>
            <description>A couple of months ago I decided to start monitoring my home network. This was in part due to frequent dropouts of an Amazon Firestick. This usually happened in the middle of the kids watching Peppa Pig or Paw Patrol. Nothing like family-driven monitoring!
I originally setup Nagios and Zabbix. My home &amp;ldquo;network&amp;rdquo; mostly consists of a few Raspberry Pis, a couple of other random ARM boards, and a mish mash of cheap switches (see: unmanaged low end TP Link/Netgear, not old enterprise kit) and routers.</description>
            <content type="html"><![CDATA[<p>A couple of months ago I decided to start monitoring my home network. This was in part due to frequent dropouts of an Amazon Firestick. This usually happened in the middle of the kids watching Peppa Pig or Paw Patrol. Nothing like family-driven monitoring!</p>
<p>I originally setup Nagios and Zabbix. My home &ldquo;network&rdquo; mostly consists of a few Raspberry Pis, a couple of other random ARM boards, and a mish mash of cheap switches (see: unmanaged low end TP Link/Netgear, not old enterprise kit) and routers.</p>
<p>This is all in dire need of an update, but not really feasible currently.</p>
<h2 id="why-nagios-and-zabbix">Why Nagios and Zabbix?</h2>
<p>I used Nagios and Zabbix for three reasons.</p>
<h3 id="familiarity">Familiarity</h3>
<p>Nagios is the primary monitoring tool at my current workplace. It has a number of shortcomings, including not being very dynamic and the configuration takes a lot of getting used to. Also I had to use Debian Buster (well, Raspbian Buster) for my monitoring host as Nagios was pulled from the Debian archives in Debian Stretch, with Debian pushing people to use Icinga2 instead.</p>
<p>However I know it well enough to be able to get an instance up and running, and monitoring all elements of my home network in a couple of hours at most.</p>
<h3 id="learning-zabbix">Learning Zabbix</h3>
<p>We do use Zabbix at my workplace, but it isn&rsquo;t a primary monitoring tool. Other than adding a couple of graphs, I&rsquo;d had little to no involvement in using or configuring it. At least by doing this, I could get more of a handle on Zabbix itself.</p>
<h3 id="raspberry-pi-2">Raspberry Pi 2</h3>
<p>The only &ldquo;host&rdquo; I had spare was a Raspberry Pi 2. Using anything more demanding (which given I&rsquo;ve seen Observium bring a fairly decent spec&rsquo;d Dell server to its knees) was out of the question.</p>
<h2 id="not-enough-monitoring">Not enough monitoring</h2>
<p>I had monitored all my internal network, as well as my VPS. I was getting some useful statistics, and it helped me fix a few underlying issues I had either not noticed, and half-ignored to deal with another day. Having it flash up in a monitoring system made me put time into actually fixing the issues.</p>
<p>While I had more visibility of what was happening in my home network, the default check times on Nagios made it so I&rsquo;d actually not see the problems quick enough. The Firestick would drop out, but by the time Nagios was ready to do another check, the Firestick would have come back (or it would have been rebooted so the kids can see Grampy Rabbit or Chase again). Increasing the frequency of the checks would probably tax the little RPI too much.</p>
<p>I looked around to see if anything else could do the job, but I couldn&rsquo;t realistically just rip down a monitoring stack and plug in a new one, in the hope it was better.</p>
<p>I wondered about Icinga2, Check_MK and other Nagios-based tools, but they either added more resource requirements or would still have some of the same issues as Nagios.</p>
<p>I liked the idea of Sensu, but there was no way I could standup Redis and RabbitMQ just for this. Besides, the minimum requirements to run it were more than all the devices in my house combined.</p>
<p>Zabbix was good, but I found it did a much better job of networking gear than it did Linux or generic blackbox-style monitoring. Not to say it was bad at it, but it is the kind of monitoring tool I would have killed for in my previous career (i.e. a Network Engineer).</p>
<h2 id="prometheus">Prometheus?</h2>
<p>Anyone who&rsquo;s looked into monitoring for the past few years will likely have heard of Prometheus. Rather than doing checks based upon scripts returning status codes, it gathers metrics from endpoints and stores them in a Time Series Database (TSDB).</p>
<p>It is not the kind of monitoring tool that you&rsquo;d use to kick off a Bash/Perl/Python/other language script (i.e. the Nagios-style approach) to check on the status of a server/application. It is mainly used to gather metrics.</p>
<p>An example of what you would get back from a &ldquo;Metrics&rdquo; endpoint would be: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># HELP node_systemd_units Summary of systemd unit states</span>
<span style="color:#75715e"># TYPE node_systemd_units gauge</span>
node_systemd_units<span style="color:#f92672">{</span>state<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;activating&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">0</span>
node_systemd_units<span style="color:#f92672">{</span>state<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;active&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">260</span>
node_systemd_units<span style="color:#f92672">{</span>state<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;deactivating&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">0</span>
node_systemd_units<span style="color:#f92672">{</span>state<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;failed&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">0</span>
node_systemd_units<span style="color:#f92672">{</span>state<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;inactive&#34;</span><span style="color:#f92672">}</span> <span style="color:#ae81ff">76</span>
</code></pre></div><p>The above has something called a label (the <em>state</em> variable in the braces) and a number. This is then stored in the Prometheus TSDB</p>
<p>It says something like &ldquo;At time HH:MM:SS:ms, 260 SystemD units were active&rdquo;. A few seconds later Prometheus would check again, and store &ldquo;At time HH:MM:SS+10s:ms, 259 SystemD units were active&rdquo;. The actual way it is stored is not in plain English, this is more to give you an idea of how just pulling back a number is useful.</p>
<h3 id="how-to-do-it">How to do it?</h3>
<p>At my workplace, we are looking to implement Prometheus. I wanted to get a bit of a headstart on using it. However with the little Raspberry Pi 2 already running Nagios and Zabbix (and I also threw Smokeping onto it for fun), I thought it would be a bit much.</p>
<h3 id="thinkpad-to-the-rescue">Thinkpad to the rescue</h3>
<p>I had a Thinkpad X220 sitting around collecting dust. It is a great little laptop, but I don&rsquo;t use it enough. However looking online it does appear to not use a lot of power, and is still a fairly capable machine.</p>
<p>It also has it&rsquo;s own in-built UPS (most would call it a laptop battery), and KVM (or, screen and keyboard) for the times I break it!</p>
<p>I installed Debian Buster on it, no desktop, and was away.</p>
<h3 id="basic-setup">Basic setup</h3>
<p>A very basic setup of Prometheus is very straightforward. All you need to do is download the latest <a href="https://github.com/prometheus/prometheus/releases">release</a>, extract it and run it.</p>
<p>For example, to run it locally on my Mac: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ wget https://github.com/prometheus/prometheus/releases/download/v2.13.0/prometheus-2.13.0.darwin-amd64.tar.gz
$ tar -xvzf prometheus-2.13.0.darwin-amd64.tar.gz
$ cd prometheus-2.13.0.darwin-amd64
$ ./prometheus
level<span style="color:#f92672">=</span>info ts<span style="color:#f92672">=</span>2019-10-06T09:13:45.418Z caller<span style="color:#f92672">=</span>main.go:296 msg<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;no time or size retention was set so using the default time retention&#34;</span> duration<span style="color:#f92672">=</span>15d
level<span style="color:#f92672">=</span>info ts<span style="color:#f92672">=</span>2019-10-06T09:13:45.419Z caller<span style="color:#f92672">=</span>main.go:332 msg<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Starting Prometheus&#34;</span> version<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(version=2.13.0, branch=HEAD, revision=6ea4252299f542669aca11860abc2192bdc7bede)&#34;</span>
level<span style="color:#f92672">=</span>info ts<span style="color:#f92672">=</span>2019-10-06T09:13:45.419Z caller<span style="color:#f92672">=</span>main.go:333 build_context<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(go=go1.13.1, user=root@f30bdad2c3fd, date=20191004-11:33:07)&#34;</span>
level<span style="color:#f92672">=</span>info ts<span style="color:#f92672">=</span>2019-10-06T09:13:45.419Z caller<span style="color:#f92672">=</span>main.go:334 host_details<span style="color:#f92672">=(</span>darwin<span style="color:#f92672">)</span>
level<span style="color:#f92672">=</span>info ts<span style="color:#f92672">=</span>2019-10-06T09:13:45.420Z caller<span style="color:#f92672">=</span>main.go:335 fd_limits<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(soft=256, hard=unlimited)&#34;</span>
level<span style="color:#f92672">=</span>info ts<span style="color:#f92672">=</span>2019-10-06T09:13:45.420Z caller<span style="color:#f92672">=</span>main.go:336 vm_limits<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(soft=unlimited, hard=unlimited)&#34;</span>
level<span style="color:#f92672">=</span>info ts<span style="color:#f92672">=</span>2019-10-06T09:13:45.421Z caller<span style="color:#f92672">=</span>main.go:657 msg<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Starting TSDB ...&#34;</span>
level<span style="color:#f92672">=</span>info ts<span style="color:#f92672">=</span>2019-10-06T09:13:45.421Z caller<span style="color:#f92672">=</span>web.go:450 component<span style="color:#f92672">=</span>web msg<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Start listening for connections&#34;</span> address<span style="color:#f92672">=</span>0.0.0.0:9090
level<span style="color:#f92672">=</span>info ts<span style="color:#f92672">=</span>2019-10-06T09:13:45.424Z caller<span style="color:#f92672">=</span>head.go:512 component<span style="color:#f92672">=</span>tsdb msg<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;replaying WAL, this may take awhile&#34;</span>
level<span style="color:#f92672">=</span>info ts<span style="color:#f92672">=</span>2019-10-06T09:13:45.425Z caller<span style="color:#f92672">=</span>head.go:560 component<span style="color:#f92672">=</span>tsdb msg<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;WAL segment loaded&#34;</span> segment<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> maxSegment<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>
level<span style="color:#f92672">=</span>info ts<span style="color:#f92672">=</span>2019-10-06T09:13:45.426Z caller<span style="color:#f92672">=</span>main.go:672 fs_type<span style="color:#f92672">=</span><span style="color:#ae81ff">19</span>
level<span style="color:#f92672">=</span>info ts<span style="color:#f92672">=</span>2019-10-06T09:13:45.426Z caller<span style="color:#f92672">=</span>main.go:673 msg<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;TSDB started&#34;</span>
level<span style="color:#f92672">=</span>info ts<span style="color:#f92672">=</span>2019-10-06T09:13:45.426Z caller<span style="color:#f92672">=</span>main.go:743 msg<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Loading configuration file&#34;</span> filename<span style="color:#f92672">=</span>prometheus.yml
level<span style="color:#f92672">=</span>info ts<span style="color:#f92672">=</span>2019-10-06T09:13:45.440Z caller<span style="color:#f92672">=</span>main.go:771 msg<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Completed loading of configuration file&#34;</span> filename<span style="color:#f92672">=</span>prometheus.yml
level<span style="color:#f92672">=</span>info ts<span style="color:#f92672">=</span>2019-10-06T09:13:45.440Z caller<span style="color:#f92672">=</span>main.go:626 msg<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Server is ready to receive web requests.&#34;</span>
</code></pre></div><p>Go to <a href="http://localhost:9090">http://localhost:9090</a> in your browser, and you&rsquo;ll see the Prometheus interface</p>
<p><img src="/img/prometheus/prometheus-interface.png" alt="Prometheus Interface"></p>
<p>Initially, the only metrics it has to begin with are the metrics of the Prometheus daemon itself. You can see this by going to <code>Status &gt; Targets</code></p>
<p><img src="/img/prometheus/initial-target.png" alt="Prometheus First Target"></p>
<p>This first target still has a lot of metrics to begin with, which you can see by visiting <a href="http://localhost:9090/metrics">http://localhost:9090/metrics</a>.</p>
<h3 id="add-more-targets">Add more targets</h3>
<p>To add more targets to Prometheus, you add them into the <code>prometheus.yml</code> configuration. If you are just testing this, there is an example YAML file in the same directory as you extracted Prometheus to.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e">## my global config</span>
<span style="color:#66d9ef">global</span>:
  <span style="color:#66d9ef">scrape_interval</span>:     15s <span style="color:#75715e"># Set the scrape interval to every 15 seconds. Default is every 1 minute.</span>
  <span style="color:#66d9ef">evaluation_interval</span>: 15s <span style="color:#75715e"># Evaluate rules every 15 seconds. The default is every 1 minute.</span>
  <span style="color:#75715e"># scrape_timeout is set to the global default (10s).</span>

<span style="color:#75715e">## Alertmanager configuration</span>
<span style="color:#66d9ef">alerting</span>:
  <span style="color:#66d9ef">alertmanagers</span>:
  - <span style="color:#66d9ef">static_configs</span>:
    - <span style="color:#66d9ef">targets</span>:
      <span style="color:#75715e"># - alertmanager:9093</span>

<span style="color:#75715e">## Load rules once and periodically evaluate them according to the global &#39;evaluation_interval&#39;.</span>
<span style="color:#66d9ef">rule_files</span>:
  <span style="color:#75715e"># - &#34;first_rules.yml&#34;</span>
  <span style="color:#75715e"># - &#34;second_rules.yml&#34;</span>

<span style="color:#75715e">## A scrape configuration containing exactly one endpoint to scrape:</span>
<span style="color:#75715e">## Here it&#39;s Prometheus itself.</span>
<span style="color:#66d9ef">scrape_configs</span>:
  <span style="color:#75715e"># The job name is added as a label `job=&lt;job_name&gt;` to any timeseries scraped from this config.</span>
  - <span style="color:#66d9ef">job_name</span>: <span style="color:#e6db74">&#39;prometheus&#39;</span>

    <span style="color:#75715e"># metrics_path defaults to &#39;/metrics&#39;</span>
    <span style="color:#75715e"># scheme defaults to &#39;http&#39;.</span>

    <span style="color:#66d9ef">static_configs</span>:
    - <span style="color:#66d9ef">targets</span>: [<span style="color:#e6db74">&#39;localhost:9090&#39;</span>]
</code></pre></div><p>There are a number of other elements like Alertmanager (which can be used to send alerts from Prometheus to your system of choice, e.g. PagerDuty, OpsGenie, Slack etc), and rules files, but to add another target is very simple. You can add another line to the <strong>static_configs</strong> directive, or you can create a new job.</p>
<p>Jobs can be very basic (with nothing but a name and targets), or they can change labels (say, replace the target name e.g. <code>localhost:9090</code> with the instance hostname for example), or many other things.</p>
<p>However, not every application supports Prometheus metric output by default, so you will more than likely need to use <strong>exporters</strong></p>
<h3 id="add-exporters">Add exporters</h3>
<p>Exporters are used to pull data about an application, infrastructure, or from SNMP, or whatever you want so long as it can export some form of meaningful metric. It is possible to write metrics to a file and have Prometheus (via the Node Exporter) pull them.</p>
<p>The most likely exporter to start with is the Node Exporter, which exposes metrics about the host it is running on. This can be used to monitor CPU, memory, disk space, status of services, ongoing network activity, and far more.</p>
<p>To install the Node Exporter, again download the latest <a href="https://github.com/prometheus/node_exporter/releases">release</a> for your architecture, untar it, go into the directory, and run it. When you do, you&rsquo;ll see that it is exposing metrics on TCP port <strong>9100</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ cd node_exporter-0.18.1.darwin-amd64
$ ./node_exporter
INFO<span style="color:#f92672">[</span>0000<span style="color:#f92672">]</span> Starting node_exporter <span style="color:#f92672">(</span>version<span style="color:#f92672">=</span>0.18.1, branch<span style="color:#f92672">=</span>HEAD, revision<span style="color:#f92672">=</span>3db77732e925c08f675d7404a8c46466b2ece83e<span style="color:#f92672">)</span>  source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;node_exporter.go:156&#34;</span>
INFO<span style="color:#f92672">[</span>0000<span style="color:#f92672">]</span> Build context <span style="color:#f92672">(</span>go<span style="color:#f92672">=</span>go1.11.10, user<span style="color:#f92672">=</span>root@4a30727bb68c, date<span style="color:#f92672">=</span>20190604-16:47:36<span style="color:#f92672">)</span>  source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;node_exporter.go:157&#34;</span>
INFO<span style="color:#f92672">[</span>0000<span style="color:#f92672">]</span> Enabled collectors:                           source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;node_exporter.go:97&#34;</span>
INFO<span style="color:#f92672">[</span>0000<span style="color:#f92672">]</span>  - boottime                                   source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;node_exporter.go:104&#34;</span>
INFO<span style="color:#f92672">[</span>0000<span style="color:#f92672">]</span>  - cpu                                        source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;node_exporter.go:104&#34;</span>
INFO<span style="color:#f92672">[</span>0000<span style="color:#f92672">]</span>  - diskstats                                  source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;node_exporter.go:104&#34;</span>
INFO<span style="color:#f92672">[</span>0000<span style="color:#f92672">]</span>  - filesystem                                 source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;node_exporter.go:104&#34;</span>
INFO<span style="color:#f92672">[</span>0000<span style="color:#f92672">]</span>  - loadavg                                    source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;node_exporter.go:104&#34;</span>
INFO<span style="color:#f92672">[</span>0000<span style="color:#f92672">]</span>  - meminfo                                    source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;node_exporter.go:104&#34;</span>
INFO<span style="color:#f92672">[</span>0000<span style="color:#f92672">]</span>  - netdev                                     source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;node_exporter.go:104&#34;</span>
INFO<span style="color:#f92672">[</span>0000<span style="color:#f92672">]</span>  - textfile                                   source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;node_exporter.go:104&#34;</span>
INFO<span style="color:#f92672">[</span>0000<span style="color:#f92672">]</span>  - time                                       source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;node_exporter.go:104&#34;</span>
INFO<span style="color:#f92672">[</span>0000<span style="color:#f92672">]</span> Listening on :9100                            source<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;node_exporter.go:170&#34;</span>
</code></pre></div><p>You can then add this into your Prometheus YAML config with something like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">  - <span style="color:#66d9ef">job_name</span>: <span style="color:#e6db74">&#39;node_exporter&#39;</span>
    <span style="color:#66d9ef">static_configs</span>:
    - <span style="color:#66d9ef">targets</span>: [<span style="color:#e6db74">&#39;localhost:9100&#39;</span>]
</code></pre></div><p>Reload Prometheus (stopping and starting it will work if you&rsquo;re just testing) and you should now see something like this in your Targets</p>
<p><img src="/img/prometheus/node-target.png" alt="Prometheus Node Target"></p>
<p>Most exporters run on their own port (usually in the TCP 9000-9999) range. Other than a couple of cases, if you take the configuration above, change the name (to make it more meaningful, you could use the same job for everything if you wanted to), and put the correct IP and Port of the where the metric is running, you&rsquo;ll get metrics in Prometheus from them.</p>
<h3 id="see-the-metrics-in-prometheus">See the metrics in Prometheus</h3>
<p>To see what Metrics are being returned and stored in Prometheus, go to the Graphs tab and start typing the word <code>node</code> into the <strong>Expression</strong> input box.</p>
<p><img src="/img/prometheus/node-expression.png" alt="Prometheus Node Expression"></p>
<p>The above is a truncated list, far more metrics are available.</p>
<p>If we chose the Load Average of the Node for the past minute, we would see: -</p>
<p><img src="/img/prometheus/node-load-console.png" alt="Prometheus Node Load - Console"></p>
<p>You can take a quick look at the Graph of this too: -</p>
<p><img src="/img/prometheus/node-load-graph.png" alt="Prometheus Node Load - Graph"></p>
<h3 id="more-exporters">More exporters</h3>
<p>There a number of different exporters, everything from monitoring the status of NGINX, to running ping and DNS checks, and much more. Below is a list of all the ones I use</p>
<ul>
<li><a href="https://github.com/Lusitaniae/apache_exporter">apache_exporter</a> - Gets metrics from Apache</li>
<li><a href="https://github.com/digitalocean/bind_exporter">bind_exporter</a> - Exposes metrics from BIND</li>
<li><a href="https://github.com/prometheus/blackbox_exporter">blackbox_exporter</a> - For monitoring ICMP (ping), DNS, HTTP checks and more - useful for when you do not have access to the internals of the system (for example, a Firestick!)</li>
<li><a href="https://github.com/google/cadvisor">cadvisor</a> - Technically not an Exporter, but it does expose metrics natively. This exposes metrics of all running Docker containers on a host</li>
<li><a href="https://github.com/spagno/iscdhcpd_exporter">dhcp_exporter</a> - Exposes metrics for ISC DHCP pool usage</li>
<li><a href="https://github.com/retailnext/iptables_exporter">iptables_exporter</a> - Exposes metrics for IPTables</li>
<li><a href="https://github.com/kumina/libvirt_exporter">libvirt_exporter</a> - Exposes metrics for Libvirt on Linux (e.g. for KVM Virtualisation)</li>
<li><a href="https://github.com/nshttpd/mikrotik-exporter">mikrotik_exporter</a> - Exposes metrics from Mikrotik RouterBoard equipment</li>
<li><a href="https://github.com/prometheus/mysqld_exporter">mysqld_exporter</a> - Exposes metrics for MySQL</li>
<li><a href="https://github.com/nginxinc/nginx-prometheus-exporter">nginx_exporter</a> - Exposes metrics for NGINX, which I use for a Reverse Proxy</li>
<li><a href="https://github.com/RobustPerception/nrpe_exporter">nrpe_exporter</a> - Used to check the results of Nagios-based NRPE checks</li>
<li><a href="https://github.com/eko/pihole-exporter">pihole_exporter</a> - Exposes metrics for Pi-Hole (DNS-based ad blocker)</li>
<li><a href="https://github.com/MindFlavor/prometheus_wireguard_exporter">wireguard_exporter</a> - Exposes metrics for Wireguard VPNs - This requires installing Rust dependencies and compiling</li>
</ul>
<h2 id="how-do-i-make-it-look-pretty">How do I make it look pretty?</h2>
<p>For how brilliant Prometheus is, it isn&rsquo;t made to look pretty, merely to be functional and not an eyesore.</p>
<p>However, <a href="https://grafana.com/">Grafana</a> can use Prometheus as a data source. Grafana dashboards can be very, very pretty (if your thing is graphs, statistics and statuses) and a number of the exporters already have suggested dashboards. You can also go through the <a href="https://grafana.com/grafana/dashboards">Grafana Dashboards page</a> and find a number that are built by others for Prometheus (and other tools).</p>
<h3 id="install-grafana">Install Grafana</h3>
<p>To install Grafana, I use Docker. This is done like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">docker run <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  -d <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  -p 3000:3000 <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  --name<span style="color:#f92672">=</span>grafana <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  -v grafana-storage:/var/lib/grafana <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  -e <span style="color:#e6db74">&#34;GF_INSTALL_PLUGINS=alexanderzobnin-zabbix-app,grafana-piechart-panel,raintank-worldping-app,camptocamp-prometheus-alertmanager-datasource,farski-blendstat-panel&#34;</span> grafana/grafana
</code></pre></div><p>The above pull the Grafana docker container, names it <strong>Grafana</strong>, adds a persistent directory on your host (so that configuration isn&rsquo;t lost if you restart the container) and installs a number of other plugins. The extra plugins aren&rsquo;t necessary, but they add useful functionality if you would like to use them.</p>
<p>If you do not want any of the extra plugins, remove everything from <code>-e</code> to just before <code>grafana/grafana</code>.</p>
<h3 id="run-grafana">Run Grafana</h3>
<p>When you go to Grafana for the first time (<a href="http://your-host-ip:3000">http://your-host-ip:3000</a>) you will be given the option to setup authentication and a number of other settings. Go through all of this, and then when you get to the Add Data Sources section, you need to add something like the following for Prometheus: -</p>
<p><img src="/img/prometheus/grafana-data-source.png" alt="Grafana Data Source"></p>
<p>Do not put in <code>http://localhost:9090</code> for your URL, as Prometheus is not running in the same Docker container.</p>
<p>Click <strong>Save &amp; Test</strong>, and you should be connected to your Prometheus data source.</p>
<h3 id="import-dashboards">Import Dashboards</h3>
<p>Importing a dashboard into Grafana is a case of taking the ID of the dashboard (you&rsquo;ll see this on the side of the page on the Grafana Dashboards site</p>
<p><img src="/img/prometheus/grafana-dashboard-id.png" alt="Grafana Dashboard ID"></p>
<p>In Grafana itself, go to to the Four Boxes icon at the top of the main page, and you should see <strong>Import Dashboard</strong></p>
<p><img src="/img/prometheus/grafana-import-dashboard.png" alt="Grafana Import Dashboards"></p>
<p>This will ask you to either put in the <strong>Grafana.com Dashboard</strong> ID (that you retrieve before) or paste the full JSON. Some dashboards are not hosted on Grafana itself, so you can just copy and paste the contents of the actual Dashboard JSON file and paste them in here</p>
<h2 id="beautiful-dashboards">Beautiful Dashboards</h2>
<p>After you have done the above, you will be able to add dashboards and look through statistics. For example, the <a href="https://grafana.com/grafana/dashboards/9894">Node Exporter 0.16 for Prometheus Monitoring display board</a> looks like the below: -</p>
<p><img src="/img/prometheus/node-exporter-dashboard.png" alt="Grafana Node Exporter Dashboard"></p>
<p>I have also put my own Dashboard together with stats that I&rsquo;m interested in, which looks something like the below: -</p>
<p><img src="/img/prometheus/my-dashboard.png" alt="My Dashboard"></p>
<p><img src="/img/prometheus/my-dashboard-cont.png" alt="My Dashboard Continued"></p>
<p>Yes, all of my infrastructure (mostly) is named after metal bands. No, I will not apologise 🤘</p>
<h2 id="summary">Summary</h2>
<p>So after all of this, I went through the process of replacing everything I had in Nagios and Zabbix with Prometheus. Over a couple of days (and by days, I mean couple of hours here and there after the kids were in bed) I moved everything to exporters and now I get much more up to date statistics.</p>
<p>Being able to use the NRPE Exporter helped in removing some checks that do not really expose metrics correctly (or would require writing complex daemons for gathering simple checks). For a video on the NRPE Exporter by Brian Brazil (a core Prometheus developer), see below: -</p>

<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe src="https://www.youtube.com/embed/6VxIYZXFc6w" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allowfullscreen title="YouTube Video"></iframe>
</div>

<p>Unfortunately, it turns out that even with all of this, the problem with the Firestick actually appears to be the use of Powerline adapters in an early 1900s house with internal cabling from an unknown time. Still, it was fun to work that out!</p>
]]></content>
        </item>
        
        <item>
            <title>Triggering ArgoCD to deploy to Kubernetes with a Jenkins Pipeline</title>
            <link>https://yetiops.net/posts/argocd-jenkins-pipeline/</link>
            <pubDate>Tue, 17 Sep 2019 12:15:08 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/argocd-jenkins-pipeline/</guid>
            <description>In the world of DevOps, automation is one of the primary goals. This includes automating how you deploy your software. Rather than relying on someone to rsync/FTP/write their software on the machine it is being deployed upon, there is the concept of CI/CD.
CI, or Continuous Integration, is the step of creating an artifact from code commits. This could be a Docker image, deployed using commits in the Master branch of a Git repository.</description>
            <content type="html"><![CDATA[<p>In the world of DevOps, automation is one of the primary goals. This includes automating how you deploy your software. Rather than relying on someone to rsync/FTP/write their software on the machine it is being deployed upon, there is the concept of CI/CD.</p>
<p>CI, or Continuous Integration, is the step of creating an <strong>artifact</strong> from code commits. This could be a Docker image, deployed using commits in the Master branch of a Git repository.</p>
<p>CD, or Continuous Deployment/Delivery, is the step of deploying the <strong>artifact</strong>. This could be anything from the Deployment system running a <code>docker pull</code> and <code>docker run</code> on a machine, telling an AWS Lambda to update to the latest code in an S3 bucket, or it could be to tell a Kubernetes cluster to update an existing <strong>Deployment</strong> to use a new image.</p>
<p>In this post, I&rsquo;m going to quickly go through how to setup a Jenkins pipeline that will trigger ArgoCD. ArgoCD is a CD tool used to update Kubernetes clusters, based upon the manifests within a Git repository. It can deploy standard Kubernetes manifests, use Kustomize to update them or Helm charts.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>I won&rsquo;t go into the details of how to setup ArgoCD itself, as their <a href="https://argoproj.github.io/argo-cd/">documentation</a> covers this very well already.</p>
<p>What you will need to do to use Jenkins with ArgoCD is: -</p>
<ul>
<li><a href="https://github.com/argoproj/argo-cd/blob/master/docs/getting_started.md">argocd-cli</a> installed on the Jenkins worker/runner</li>
<li>A Jenkins Deployment role within ArgoCD, that has access to update Applications</li>
<li>One or more repositories that ArgoCD has access to in your Git-based version control system (e.g. GitHub, Gitlab, Gitea, Git with no frontend)</li>
<li>The Jenkins worker/runner needs to be able to access ArgoCD via the API, so ensure correct firewall rules are in place</li>
</ul>
<h3 id="argocd-cli">ArgoCD CLI</h3>
<p>We configure/prepare the images for our Jenkins workers using <a href="https://www.packer.io/">Packer</a>. Because of this, adding in additional tools is quite straightforward. If however you run static workers, then just ensure that the <strong>argocd</strong> binary is installed somewhere that is runnable by the Jenkins user. An example would be: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ pwd
/home/jenkins

<span style="color:#75715e">## Download the tool</span> 
$ curl -LO https://github.com/argoproj/argo-cd/releases/download/v1.2.0/argocd-linux-amd64 

<span style="color:#75715e">## Move it to the /usr/local/bin</span>
$ sudo mv argocd-linux-amd64 /usr/local/bin/argocd

<span style="color:#75715e">## Ensure it is exectutable</span>
$ sudo chmod <span style="color:#ae81ff">755</span> /usr/local/bin/argocd

<span style="color:#75715e">## Check it works</span>
$ argocd version
argocd: v1.2.0+674978c
  BuildDate: 2019-09-04T21:26:04Z
  GitCommit: 674978cd587701b39e81fce6d5c960b6d76d5882
  GitTreeState: clean
  GoVersion: go1.12.6
  Compiler: gc
  Platform: linux/amd64
argocd-server: v1.2.0+674978c
  BuildDate: 2019-09-04T21:27:17Z
  GitCommit: 674978cd587701b39e81fce6d5c960b6d76d5882
  GitTreeState: clean
  GoVersion: go1.12.6
  Compiler: gc
  Platform: linux/amd64
  Ksonnet Version: 0.13.1
</code></pre></div><h3 id="argocd-jenkins-deploy-role">ArgoCD Jenkins Deploy Role</h3>
<p>To create a Deployment role in ArgoCD, go to the ArgoCD dashboard, click on the Gears icon on the sidebar (to take you to settings) and go to <strong>Projects</strong>.</p>
<p><img src="/img/jenkins-argo/argocd-settings.png" alt="ArgoCD Settings"></p>
<p>In <strong>Projects</strong>, choose whatever project your application is running in. If you haven&rsquo;t created any yet, this is going to be <strong>Default</strong>.</p>
<p><img src="/img/jenkins-argo/argocd-projects.png" alt="ArgoCD Projects"></p>
<p>In the project, go to <strong>Roles</strong>, and click <strong>Add Role</strong></p>
<p><img src="/img/jenkins-argo/argocd-add-role.png" alt="ArgoCD Add Role"></p>
<p>In the role, you can give the Role the name you would like, and a description to make it easier to work out its purpose in future. You can apply fine-grained policy here to say what Jenkins can do (e.g. only create, only update, only sync existing applications). Alternatively you can give it full access to do anything, but obviously this is not recommended in a production environment</p>
<p><img src="/img/jenkins-argo/argocd-add-role-jenkins.png" alt="ArgoCD Add Role Jenkins"></p>
<p>When all of the above is done, you will then need to create a JWT (JSON Web Token). This is used to authenticate the user, ensuring only the client (in this case, Jenkins) can assume this role and use it&rsquo;s permissions. This can be done in the Web UI, or it can be done via the CLI.</p>
<p>To create the JWT from the CLI, you need to do <code>argocd proj role create-token {PROJECT-NAME} {PROJECT-ROLE}</code>, with an example below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ argocd proj role create-token default jenkins-deploy-role
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1Njg3MjEyMjEsImlzcyI6ImFyZ29jZCIsIm5iZiI6MTU2ODcyMTIyMSwic3ViIjoicHJvajpkZWZhdWx0OmplbmtpbnMtZGVwbG95LXJvbGUifQ.UyXNZtdbDyllzGl7PbLPhMgqNMFE1oJqONaLHV8RK-k
</code></pre></div><p>Keep this somewhere safe (i.e. some form of password manager)</p>
<h3 id="adding-the-token-to-jenkins">Adding the Token to Jenkins</h3>
<p>To add the token into Jenkins itself (ready to be used in a Pipeline), first go to your Jenkins instance and go to <strong>Credentials</strong> on the sidebar, then select <strong>System</strong>, then <strong>Global credentials</strong></p>
<p><img src="/img/jenkins-argo/jenkins-configure.png" alt="Jenkins Configure"></p>
<p>In here, you&rsquo;ll be given the option to <strong>Add Credentials</strong></p>
<p><img src="/img/jenkins-argo/jenkins-add-credentials.png" alt="Jenkins Add Credentials"></p>
<p>In here, fill in the fields as following: -</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Kind</td>
<td>Secret Text</td>
</tr>
<tr>
<td>Scope</td>
<td>Global</td>
</tr>
<tr>
<td>Secret</td>
<td>This is the JWT you created earlier</td>
</tr>
<tr>
<td>ID</td>
<td>Pick a name, e.g. argocd-deploy-role</td>
</tr>
<tr>
<td>Description</td>
<td>Choose a description that is relevant, e.g. Jenkins Deploy Token for ArgoCD</td>
</tr>
</tbody>
</table>
<p>You can now make use of this Token within your Jenkins pipelines</p>
<h3 id="allow-argocd-to-access-your-repositories">Allow ArgoCD to access your repositories</h3>
<p>The repositories that ArgoCD needs access to is those that host your Kubernetes manifests. You could keep the manifests in the same repository as your code, or you could have them in an entirely separate repository.</p>
<p>There are a number of options you can do to set this up. You can connect using SSH to your Version Control system, or HTTPS. I chose SSH.</p>
<p>Go to the <strong>Settings</strong> page in ArgoCD again, but rather than clicking on <strong>Projects</strong>, click on <strong>Repositories</strong>. In here, click on <strong>Connect repo using SSH</strong></p>
<p><img src="/img/jenkins-argo/argocd-repo.png" alt="ArgoCD Add Repository"></p>
<p>To generate an SSH key for this, choose your favourite method. I&rsquo;m running Linux, so I generate them as such: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ ssh-keygen -o -f argocd-deploy -C <span style="color:#e6db74">&#34;argocd@example.com&#34;</span>
Generating public/private rsa key pair.
Enter passphrase <span style="color:#f92672">(</span>empty <span style="color:#66d9ef">for</span> no passphrase<span style="color:#f92672">)</span>: 
Enter same passphrase again: 
Your identification has been saved in argocd-deploy.
Your public key has been saved in argocd-deploy.pub.
The key fingerprint is:
SHA256:kYa25wSePx/R1aLd/p8Z+b6MgPWDNMVsS+4Tp64JrHE argocd@example.com
The keys randomart image is:
+---<span style="color:#f92672">[</span>RSA 3072<span style="color:#f92672">]</span>----+
|                 |
|       . .  o  . |
|      + +    *o .|
|     o <span style="color:#f92672">=</span> . .<span style="color:#f92672">=</span>+.o |
|      + S .+o+...|
|       <span style="color:#f92672">=</span>. +.<span style="color:#f92672">=</span> +..|
|       .+E.o * o.|
|        +o..+ <span style="color:#f92672">=</span> *|
|       .  .o.o *B|
+----<span style="color:#f92672">[</span>SHA256<span style="color:#f92672">]</span>-----+
</code></pre></div><p>Take the contents of the <strong>Private Key</strong> (in my case, <code>argocd-deploy</code>) and paste them into the <strong>SSH private key data</strong> field in ArgoCD. Supply the URL of the repository that your Kubernetes manifests sit in (the HTTPS URL if you are connecting with HTTPS, SSH URL if via SSH).</p>
<p>You&rsquo;ll need the public key (<code>argocd-deploy.pub</code> in my case) for setting up a Deploy Key in your chosen Git server.</p>
<p>For more information on setting up deploy tokens in GitHub or Gitlab, see: -</p>
<ul>
<li><a href="https://docs.gitlab.com/ee/ssh/">GitLab and SSH keys</a></li>
<li><a href="https://developer.github.com/v3/guides/managing-deploy-keys/">GitHub - Managing Deploy Keys</a></li>
</ul>
<p>If you use another Git server for your version control, please refer to their documentation</p>
<h3 id="firewall-rules">Firewall Rules</h3>
<p>Your Jenkins worker/runner needs to be able to access the ArgoCD API. We expose it on port <code>TCP:443</code> (i.e. standard HTTPS) so you will need to ensure your firewall allows this through.</p>
<h2 id="kubernetes-manifests">Kubernetes Manifests</h2>
<p>As mentioned, you can use standard Kubernetes manifests, Kustomize and/or Helm charts to deploy your applications. I am using Kustomize to allow us to update the Docker image hash used in the Kubernetes Deployment. Without this, even if a new image is available in your Docker registry (private or public), the Deployments never update.</p>
<p>This is because the Kubernetes API doesn&rsquo;t think the Deployment has changed. The tag is the same as before, so it doesn&rsquo;t know that it needs to do anything.</p>
<p>As of Kubernetes version v1.15, they now support a <em>rolling restart</em>. Used in combination with <code>ImagePullPolicy: Always</code>, this will force the deployment to pick up the new image.</p>
<p>Pre-Kubernetes v1.15, the Deployment would never update. It is not seen as best practice to use <code>:latest</code> anyway, so referring to actual hashes is probably a good idea anyway.</p>
<h3 id="example-manifest">Example Manifest</h3>
<p>My very basic Kubernetes manifest is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e">## ------------------- Debian Deployment ------------------- #</span>

<span style="color:#66d9ef">kind</span>: Deployment
<span style="color:#66d9ef">apiVersion</span>: apps/v1
<span style="color:#66d9ef">metadata</span>:
  <span style="color:#66d9ef">labels</span>:
    <span style="color:#66d9ef">k8s-app</span>: debian-test
  <span style="color:#66d9ef">name</span>: debian-test
<span style="color:#66d9ef">spec</span>:
  <span style="color:#66d9ef">replicas</span>: <span style="color:#ae81ff">1</span>
  <span style="color:#66d9ef">revisionHistoryLimit</span>: <span style="color:#ae81ff">10</span>
  <span style="color:#66d9ef">selector</span>:
    <span style="color:#66d9ef">matchLabels</span>:
      <span style="color:#66d9ef">k8s-app</span>: debian-test
  <span style="color:#66d9ef">template</span>:
    <span style="color:#66d9ef">metadata</span>:
      <span style="color:#66d9ef">labels</span>:
        <span style="color:#66d9ef">k8s-app</span>: debian-test
    <span style="color:#66d9ef">spec</span>:
      <span style="color:#66d9ef">containers</span>:
      - <span style="color:#66d9ef">name</span>: debian-test
        <span style="color:#66d9ef">image</span>: {AWS-ACCOUNT}.dkr.ecr.{AWS-REGION}.amazonaws.com/k8s-debian-test:latest 
        <span style="color:#66d9ef">imagePullPolicy</span>: Always
        <span style="color:#66d9ef">resources</span>:
          <span style="color:#66d9ef">requests</span>:
            <span style="color:#66d9ef">memory</span>: <span style="color:#e6db74">&#34;64Mi&#34;</span>
            <span style="color:#66d9ef">cpu</span>: <span style="color:#e6db74">&#34;250m&#34;</span>
          <span style="color:#66d9ef">limits</span>:
            <span style="color:#66d9ef">memory</span>: <span style="color:#e6db74">&#34;128Mi&#34;</span>
            <span style="color:#66d9ef">cpu</span>: <span style="color:#e6db74">&#34;500m&#34;</span>
        <span style="color:#66d9ef">ports</span>:
        - <span style="color:#66d9ef">containerPort</span>: <span style="color:#ae81ff">80</span>
          <span style="color:#66d9ef">protocol</span>: TCP
        <span style="color:#66d9ef">livenessProbe</span>:
          <span style="color:#66d9ef">httpGet</span>:
            <span style="color:#66d9ef">scheme</span>: HTTP
            <span style="color:#66d9ef">path</span>: /
            <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">80</span>
          <span style="color:#66d9ef">initialDelaySeconds</span>: <span style="color:#ae81ff">30</span>
          <span style="color:#66d9ef">timeoutSeconds</span>: <span style="color:#ae81ff">30</span>

---
<span style="color:#75715e">## ------------------- Debian Service ------------------- #</span>

<span style="color:#66d9ef">kind</span>: Service
<span style="color:#66d9ef">apiVersion</span>: v1
<span style="color:#66d9ef">metadata</span>:
  <span style="color:#66d9ef">labels</span>:
    <span style="color:#66d9ef">k8s-app</span>: debian-test
  <span style="color:#66d9ef">name</span>: debian-test
<span style="color:#66d9ef">spec</span>:
  <span style="color:#66d9ef">ports</span>:
    - <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">80</span>
      <span style="color:#66d9ef">targetPort</span>: <span style="color:#ae81ff">80</span>
  <span style="color:#66d9ef">type</span>: NodePort
  <span style="color:#66d9ef">selector</span>:
    <span style="color:#66d9ef">k8s-app</span>: debian-test

</code></pre></div><p>We use Amazon&rsquo;s ECR private Docker registry for our images. As you can see, the above says <code>:latest</code> as the image tag. However, we also use <strong>Kustomize</strong> to change this per deployment. Our <code>kustomization.yaml</code> file is very simple: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">resources</span>:
- debian-test.yaml
</code></pre></div><p>As of v1.2.0, ArgoCD can leverage <strong>Kustomize</strong> natively (if you specify the correct arguments), so there is no need to add anything further in this YAML file.</p>
<h2 id="jenkins-pipeline">Jenkins Pipeline</h2>
<p>Now all the prerequisites are done, you have ArgoCD connected to your Repository with your manifests, and you have created your manifests, you can create a <code>Jenkinsfile</code> ready to deploy the application</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-groovy" data-lang="groovy">pipeline <span style="color:#f92672">{</span>
    agent <span style="color:#f92672">{</span>
        node <span style="color:#f92672">{</span>
            label <span style="color:#e6db74">&#39;testing&#39;</span>
        <span style="color:#f92672">}</span>
    <span style="color:#f92672">}</span>
    
    stages <span style="color:#f92672">{</span>       
        stage<span style="color:#f92672">(</span><span style="color:#e6db74">&#39;Prepare&#39;</span><span style="color:#f92672">)</span> <span style="color:#f92672">{</span>
            steps <span style="color:#f92672">{</span>
                checkout<span style="color:#f92672">([</span>$class<span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;GitSCM&#39;</span><span style="color:#f92672">,</span>
                branches: <span style="color:#f92672">[[</span>name: <span style="color:#e6db74">&#34;origin\master&#34;</span><span style="color:#f92672">]],</span>
                doGenerateSubmoduleConfigurations: <span style="color:#66d9ef">false</span><span style="color:#f92672">,</span>
                submoduleCfg: <span style="color:#f92672">[],</span>
                userRemoteConfigs: <span style="color:#f92672">[[</span>
                    url: <span style="color:#e6db74">&#39;ssh:\\git@git.example.com\argocd-test\argocd-test.git&#39;</span><span style="color:#f92672">]]</span>
                <span style="color:#f92672">])</span>
            <span style="color:#f92672">}</span>
        <span style="color:#f92672">}</span>
        stage <span style="color:#f92672">(</span><span style="color:#e6db74">&#39;Docker_Build&#39;</span><span style="color:#f92672">)</span> <span style="color:#f92672">{</span>
            steps <span style="color:#f92672">{</span>
                <span style="color:#960050;background-color:#1e0010">\\</span> Build the docker image
                sh<span style="color:#e6db74">&#39;&#39;&#39;
</span><span style="color:#e6db74">                    # Build the image
</span><span style="color:#e6db74">                    $(aws ecr get-login --region eu-west-1 --profile global --no-include-email)
</span><span style="color:#e6db74">                    docker build . -t k8s-debian-test
</span><span style="color:#e6db74">                &#39;&#39;&#39;</span>
            <span style="color:#f92672">}</span>
        <span style="color:#f92672">}</span>
        
        stage <span style="color:#f92672">(</span><span style="color:#e6db74">&#39;Deploy_K8S&#39;</span><span style="color:#f92672">)</span> <span style="color:#f92672">{</span>
             steps <span style="color:#f92672">{</span>
                     withCredentials<span style="color:#f92672">([</span>string<span style="color:#f92672">(</span>credentialsId: <span style="color:#e6db74">&#34;jenkins-argocd-deploy&#34;</span><span style="color:#f92672">,</span> variable: <span style="color:#e6db74">&#39;ARGOCD_AUTH_TOKEN&#39;</span><span style="color:#f92672">)])</span> <span style="color:#f92672">{</span>
                        sh <span style="color:#e6db74">&#39;&#39;&#39;
</span><span style="color:#e6db74">                        ARGOCD_SERVER=&#34;argocd-prod.example.com&#34;
</span><span style="color:#e6db74">                        APP_NAME=&#34;debian-test-k8s&#34;
</span><span style="color:#e6db74">                        CONTAINER=&#34;k8s-debian-test&#34;
</span><span style="color:#e6db74">                        REGION=&#34;eu-west-1&#34;
</span><span style="color:#e6db74">                        AWS_ACCOUNT=&#34;$ACCOUNT_NUMBER&#34;
</span><span style="color:#e6db74">                        AWS_ENVIRONMENT=&#34;staging&#34;
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">                        $(aws ecr get-login --region $REGION --profile $AWS_ENVIRONMENT --no-include-email)
</span><span style="color:#e6db74">                        
</span><span style="color:#e6db74">                        # Deploy image to ECR
</span><span style="color:#e6db74">                        docker tag $CONTAINER:latest $AWS_ACCOUNT.dkr.ecr.$REGION.amazonaws.com\$CONTAINER:latest
</span><span style="color:#e6db74">                        docker push $AWS_ACCOUNT.dkr.ecr.$REGION.amazonaws.com\$CONTAINER:latest
</span><span style="color:#e6db74">                        IMAGE_DIGEST=$(docker image inspect $AWS_ACCOUNT.dkr.ecr.$REGION.amazonaws.com\$CONTAINER:latest -f &#39;{{join .RepoDigests &#34;,&#34;}}&#39;)
</span><span style="color:#e6db74">                        # Customize image 
</span><span style="color:#e6db74">                        ARGOCD_SERVER=$ARGOCD_SERVER argocd --grpc-web app set $APP_NAME --kustomize-image $IMAGE_DIGEST
</span><span style="color:#e6db74">                        
</span><span style="color:#e6db74">                        # Deploy to ArgoCD
</span><span style="color:#e6db74">                        ARGOCD_SERVER=$ARGOCD_SERVER argocd --grpc-web app sync $APP_NAME --force
</span><span style="color:#e6db74">                        ARGOCD_SERVER=$ARGOCD_SERVER argocd --grpc-web app wait $APP_NAME --timeout 600
</span><span style="color:#e6db74">                        &#39;&#39;&#39;</span>
               <span style="color:#f92672">}</span>
            <span style="color:#f92672">}</span>
        <span style="color:#f92672">}</span>
    <span style="color:#f92672">}</span>
<span style="color:#f92672">}</span>
</code></pre></div><p>I won&rsquo;t cover the Jenkins specifics (e.g. the stages, steps, labels etc) as there are a number of resources out there that <a href="https://go.cloudbees.com/docs/pipelines/automating-with-jenkinsfile/creating-jenkinsfile/">cover</a> <a href="https://jenkins.io/doc/book/pipeline/jenkinsfile/">this</a>. Instead I&rsquo;ll go through the relevant parts that make it a little different from your standard <code>Jenkinsfile</code></p>
<h3 id="ecr-login">ECR Login</h3>
<p>This is used to login to AWS ECR, to retrieve Docker images (e.g. a base Debian image), and then to tag and push based upon our <code>Dockerfile</code>. If you are using standard images, or your own Private registry, you can ignore these parts.</p>
<h3 id="image-digest">Image Digest</h3>
<p>For whatever reason, the Docker image digests are not always seen in <code>docker images --digests</code>. Instead, we inspect the image from the latest Push to ECR, and retrieve the <code>.RepoDigests</code> tag instead. This gives the following: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ docker image inspect <span style="color:#e6db74">${</span>AWS_ACCOUNT<span style="color:#e6db74">}</span>.dkr.ecr.<span style="color:#e6db74">${</span>AWS_REGION<span style="color:#e6db74">}</span>.amazonaws.com/k8s-debian-test:latest -f <span style="color:#e6db74">&#39;{{join .RepoDigests &#34;,&#34;}}&#39;</span>
<span style="color:#e6db74">${</span>AWS_ACCOUNT<span style="color:#e6db74">}</span>.dkr.ecr.<span style="color:#e6db74">${</span>AWS_REGION<span style="color:#e6db74">}</span>.amazonaws.com/k8s-debian-test@sha256:###REALLY-LONG-SHA256-HASH-DIGEST###
</code></pre></div><p>We can then use this to pull the latest generated image</p>
<h3 id="argocd">ArgoCD</h3>
<p>Due to our ArgoCD API and dashboard being fronted by an AWS Application Load Balancer, we currently prefix all of ArgoCD commands with <code>--grpc-web</code>, e.g. <code>argocd --grpc-web app sync TEST --force</code>. This is because AWS ALBs do not support GRPC by default. If the load balancer/ingress you have in front of ArgoCD does support it, remove this from all the commands.</p>
<h4 id="customize-image">Customize Image</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ARGOCD_SERVER<span style="color:#f92672">=</span>$ARGOCD_SERVER argocd --grpc-web app set $APP_NAME --kustomize-image $IMAGE_DIGEST
</code></pre></div><p>The above command sets the <code>image</code> within the Kubernetes manifest to be that of the generated Image Digest variable. As ArgoCD supports Kustomize by default, it can manipulate the manifests itself. This means that we do not get into a situation where the Deployment never updates, due to the Image tag never changing.</p>
<p>This is a really nice feature, and means we do not need to run the Kustomize binary as well as ArgoCD on our Jenkins workers.</p>
<h4 id="sync-the-app">Sync the App</h4>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ARGOCD_SERVER<span style="color:#f92672">=</span>$ARGOCD_SERVER argocd --grpc-web app sync $APP_NAME --force
ARGOCD_SERVER<span style="color:#f92672">=</span>$ARGOCD_SERVER argocd --grpc-web app wait $APP_NAME --timeout <span style="color:#ae81ff">600</span>
</code></pre></div><p>The above simply asks ArgoCD to trigger Kubernetes to deploy the app. Within ArgoCD, this is using the Manifest from the Git repository, that has been updated with Kustomize to use the new image tag. This will then go out and deploy a new version of it, based upon the image generated by Jenkins.</p>
<h2 id="jenkins-pipeline-run">Jenkins Pipeline Run</h2>
<p>Below is all the output you get from the build</p>
<p><img src="/img/jenkins-argo/jenkins-run.png" alt="Jenkins Run"></p>
<h3 id="kubernetes-output">Kubernetes Output</h3>
<p><strong>Pre-ArgoCD Run</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">kubectl get pods
NAME                                     READY   STATUS      RESTARTS   AGE
debian-test-8648f969ff-hrsvp             1/1     Running     <span style="color:#ae81ff">0</span>          4d22h
</code></pre></div><p><strong>Post-ArgoCD Run</strong></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">kubectl get pods
NAME                                     READY   STATUS      RESTARTS   AGE
debian-test-7664c648bb-sq6h7             1/1     Running     <span style="color:#ae81ff">0</span>          22s
</code></pre></div><h3 id="jenkins-console-output---argocd">Jenkins Console Output - ArgoCD</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">
+ ARGOCD_SERVER<span style="color:#f92672">=</span>argocd-prod.example.com argocd <span style="color:#ae81ff">\ </span>
  --grpc-web app set debian-test-k8s <span style="color:#ae81ff">\ </span>
  --kustomize-image <span style="color:#e6db74">${</span>AWS-ACCOUNT<span style="color:#e6db74">}</span>.dkr.ecr.<span style="color:#e6db74">${</span>AWS-REGION<span style="color:#e6db74">}</span>.amazonaws.com/k8s-debian-test@sha256:###SHA256-IMAGE-HASH###

+ ARGOCD_SERVER<span style="color:#f92672">=</span>argocd-prod.example.com argocd --grpc-web app sync debian-test-k8s --force
TIMESTAMP                  GROUP        KIND   NAMESPACE                  NAME    STATUS   HEALTH        HOOK  MESSAGE
2019-09-17T13:05:27+00:00   apps  Deployment     default           debian-test    Synced  Healthy              
2019-09-17T13:05:27+00:00            Service     default           debian-test    Synced  Healthy              
2019-09-17T13:05:27+00:00   apps  Deployment     default           debian-test  OutOfSync  Healthy              

Name:               debian-test-k8s
Project:            default
Server:             https://kubernetes.default.svc
Namespace:          default
URL:                https://argocd-prod.example.com/applications/debian-test-k8s
Repo:               git@git.example.com:yeti/argocd-test.git
Target:             HEAD
Path:               yaml
Sync Policy:        &lt;none&gt;
Sync Status:        Synced to HEAD <span style="color:#f92672">(</span>f5f91ad<span style="color:#f92672">)</span>
Health Status:      Progressing

Operation:          Sync
Sync Revision:      f5f91ad16296ecab90f337e5dbf3f4f927b61799
Phase:              Succeeded
Start:              2019-09-17 13:05:27 +0000 UTC
Finished:           2019-09-17 13:05:29 +0000 UTC
Duration:           2s
Message:            successfully synced <span style="color:#f92672">(</span>all tasks run<span style="color:#f92672">)</span>

GROUP  KIND        NAMESPACE  NAME         STATUS  HEALTH       HOOK  MESSAGE
       Service     default    debian-test  Synced  Healthy            service/debian-test unchanged
apps   Deployment  default    debian-test  Synced  Progressing        deployment.apps/debian-test configured
+ ARGOCD_SERVER<span style="color:#f92672">=</span>argocd-prod.example.com argocd --grpc-web app wait debian-test-k8s --timeout <span style="color:#ae81ff">600</span>
TIMESTAMP                  GROUP        KIND   NAMESPACE                  NAME    STATUS   HEALTH            HOOK  MESSAGE
2019-09-17T13:05:29+00:00            Service     default           debian-test    Synced  Healthy                  service/debian-test unchanged
2019-09-17T13:05:29+00:00   apps  Deployment     default           debian-test    Synced  Progressing              deployment.apps/debian-test configured

Name:               debian-test-k8s
Project:            default
Server:             https://kubernetes.default.svc
Namespace:          default
URL:                https://argocd-prod.example.com/applications/debian-test-k8s
Repo:               git@git.example.com:yetiops/argocd-test.git
Target:             HEAD
Path:               yaml
Sync Policy:        &lt;none&gt;
Sync Status:        Synced to HEAD <span style="color:#f92672">(</span>f5f91ad<span style="color:#f92672">)</span>
Health Status:      Healthy

Operation:          Sync
Sync Revision:      f5f91ad16296ecab90f337e5dbf3f4f927b61799
Phase:              Succeeded
Start:              2019-09-17 13:05:27 +0000 UTC
Finished:           2019-09-17 13:05:29 +0000 UTC
Duration:           2s
Message:            successfully synced <span style="color:#f92672">(</span>all tasks run<span style="color:#f92672">)</span>

GROUP  KIND        NAMESPACE  NAME         STATUS  HEALTH   HOOK  MESSAGE
       Service     default    debian-test  Synced  Healthy        service/debian-test unchanged
apps   Deployment  default    debian-test  Synced  Healthy        deployment.apps/debian-test configured
</code></pre></div><h3 id="argocd-dashboard">ArgoCD Dashboard</h3>
<p><img src="/img/jenkins-argo/argocd-deployment.png" alt="ArgoCD Deployment"></p>
<h2 id="summary">Summary</h2>
<p>Why are we using Jenkins and ArgoCD? Jenkins is pretty good at building <strong>artifacts</strong>, bringing code commits together and accepting webhooks from something like GitHub or GitLab to kick off a job. However ArgoCD gives greater control on how we deploy the images, as everything can be described in native Kubernetes manifests.</p>
<p>This removes the need for exposing Kubernetes to Jenkins directly, as well as having to manipulate <code>kubectl</code> commands to make it deploy (or use extra plugins).</p>
<p>ArgoCD also gives us a live view of how the deployments went, and where we can roll back to if required. It also shows them in a way that is Kubernetes centric (i.e. how the Deployments and Services tie together) and so is much easier for developers and operations teams to see how everything ties together.</p>
<p>I hope this helps people looking for information on using Jenkins and ArgoCD together!</p>
]]></content>
        </item>
        
        <item>
            <title>AWS Cognito as an Oauth2 Provider for Kubernetes Apps - Part 1</title>
            <link>https://yetiops.net/posts/aws-cognito-oauth-k8s-part1/</link>
            <pubDate>Thu, 12 Sep 2019 14:44:44 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/aws-cognito-oauth-k8s-part1/</guid>
            <description>Recently I have been integrating a number of apps in Kubernetes to use AWS Cognito as an Oauth2 provider. For those unaware, Oauth2 is a protocol that can be used to authenticate users against a number of different services. Whenever you see &amp;ldquo;Login with Google&amp;rdquo; or &amp;ldquo;Login with Facebook&amp;rdquo;, this is using Oauth2 behind the scenes.
It&amp;rsquo;s worth pointing out that Oauth2 is a Framework for how to implement authorization. Open ID Connect (OIDC) extends Oauth2, but also simplifies it.</description>
            <content type="html"><![CDATA[<p>Recently I have been integrating a number of apps in Kubernetes to use AWS Cognito as an Oauth2 provider. For those unaware, Oauth2 is a protocol that can be used to authenticate users against a number of different services. Whenever you see &ldquo;Login with Google&rdquo; or &ldquo;Login with Facebook&rdquo;, this is using Oauth2 behind the scenes.</p>
<p>It&rsquo;s worth pointing out that Oauth2 is a Framework for how to implement authorization. Open ID Connect (OIDC) extends Oauth2, but also simplifies it. I am by no means an expert in either. Be prepared to read a blog post in 6 months where I correct everything I say in this one!</p>
<p>This post is going to cover a few apps I&rsquo;ve integrated so far with AWS Cognito. I&rsquo;ll also do more posts in future as I integrate more apps with it.</p>
<h2 id="oauth2-and-oidc">Oauth2 and OIDC</h2>
<p>For those new to Oauth2 and OIDC, I would suggest the following resources: -</p>
<ul>
<li><a href="https://milapneupane.com.np/2019/09/02/a-complete-guide-to-oauth2-protocol/">A Complete Guide to Oauth2 Protocol</a> - Very good blog post, breaks down the concepts clearly</li>
<li><a href="https://milapneupane.com.np/2019/09/02/a-complete-guide-to-oauth2-protocol/">Understanding Oauth2 and Open ID Connect</a> - Also very good, written by an employee of Okta (who provide a popular Oauth2/OIDC service of their own)</li>
</ul>
<h2 id="aws-cognito">AWS Cognito</h2>
<p><a href="https://aws.amazon.com/cognito/">Cognito</a> was chosen for a few reasons: -</p>
<ul>
<li>We are currently an AWS shop when it comes to the Cloud</li>
<li>All our code is hosted internally, rather than on something like GitHub or public Gitlab</li>
<li>We didn&rsquo;t want to also manage our own internal Oauth2 Authorization Server</li>
</ul>
<p>I&rsquo;m fairly inexperienced on Oauth2 and OIDC (as is our business in general), so I also didn&rsquo;t want to end up trying to set up our own Oauth2 server/provider and find out it&rsquo;s missing half the features or is horrendously insecure.</p>
<p>One issue with AWS Cognito though is because most of the world is on GitHub, a lot of documentation for Oauth2 integration in applications tends to assume you are too. Many projects have explicit instructions for how to authenticate against GitHub&rsquo;s Oauth2/OIDC provider. You may also see documentation for authentication against Google, maybe even Okta. Integration documentation on AWS Cognito is few and far between.</p>
<h3 id="setting-up-cognito">Setting up Cognito</h3>
<p>You need to follow a few specific steps to get Oauth2 working correctly with Cognito.</p>
<h4 id="create-a-user-pool">Create a user pool</h4>
<p>Go to Cognito in AWS, and you will be presented with this: -</p>
<p><img src="/img/cognito_welcome.png" alt="Cognito Welcome"></p>
<p>If you go to Manage User Pools, you can then begin to create your first Cognito User Pool. Follow the steps below to do so</p>
<p><strong>Create a User Pool</strong></p>
<p>Give it whatever name you would like</p>
<p><strong>Choose how you want End Users to sign in</strong></p>
<p>I would suggest Email, but it is down to your preference. You&rsquo;ll need to choose some attributes here as well. If you are using AWS Cognito itself to manage users (rather than an external entity, like Google or SAML), choose whatever attributes that you want a User to supply. I am using SAML to Active Directory, so I have chose Email, Family Name, Given Name and Name.</p>
<p><strong>Other Options</strong></p>
<p>After this, you will click through multiple options and set your preferences. This includes Password Complexity, Advanced Security (which adds some security features, but does cost more), Tags and a number of other attributes.</p>
<h4 id="app-client">App Client</h4>
<p>Once you have done the above, you can start adding your App Clients (so anything you want to authenticate and authorize against AWS Cognito). When you do this, you&rsquo;ll create a name for the client (for example, <strong>kubernetes-web-view</strong>). This will generate a <strong>Client ID</strong> and <strong>Client Secret</strong>.</p>
<h4 id="app-client-settings">App Client Settings</h4>
<p>In App Client Settings, you can set the specifics of how the apps will interact with Cognito. This includes your <strong>Callback URL(s)</strong>, <strong>Sign Out Urls</strong>, what Oauth Flows are used, allowed Scopes (i.e. can a client try and retrieve via Email or Phone?).</p>
<p>All of these settings are going to be app dependent. For example, Pomerium attempts to use the <strong>offline_access</strong> scope in its default OIDC provider. This isn&rsquo;t supported in AWS Cognito, so you&rsquo;ll have to customize your app config to match.</p>
<h4 id="important-domain-name">IMPORTANT: Domain Name</h4>
<p>There is an option to create a Domain Name as part of your User Pool. To have Oauth work on your apps, this is a must. You can choose your own domain, or you can use one that is provided by AWS, which will be in the format <code>https://{your-chosen-domain}.auth.{region}.amazoncognito.com</code>.</p>
<h3 id="endpoints">Endpoints</h3>
<p>After this, you&rsquo;ll end up with a number of useful endpoints that apps can make use of. Not all apps make use of each of them, so follow the instructions for the apps to work out which are required.</p>
<h4 id="authorize">Authorize</h4>
<p><code>https://{your-chosen-domain}.auth.{region}.amazoncognito.com/oauth2/authorize</code></p>
<h4 id="token">Token</h4>
<p><code>https://{your-chosen-domain}.auth.{region}.amazoncognito.com/oauth2/token</code></p>
<h4 id="login">Login</h4>
<p><code>https://{your-chosen-domain}.auth.{region}.amazoncognito.com/oauth2/login</code></p>
<h4 id="logout">Logout</h4>
<p><code>https://{your-chosen-domain}.auth.{region}.amazoncognito.com/oauth2/logout</code></p>
<h4 id="user-info">User Info</h4>
<p><code>https://{your-chosen-domain}.auth.{region}.amazoncognito.com/oauth2/userinfo</code></p>
<h4 id="idp-provider-url">IDP Provider URL</h4>
<p><code>https://cognito-idp.{region}.amazonaws.com/{user_pool_id}</code></p>
<p>You can find the User Pool ID in the AWS Console (select General Settings, and it should be seen as <strong>Pool ID</strong>. This URL is often used to discover the capabilities of the Provider.</p>
<p>If you navigated to <code>https://cognito-idp.{region}.amazonaws.com/{user_pool_id}/.well-known/openid-configuration</code> for example, you would receive the following JSON response: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;authorization_endpoint&#34;</span>: <span style="color:#e6db74">&#34;https://$COGNITO_DOMAIN.auth.eu-west-1.amazoncognito.com/oauth2/authorize&#34;</span>,
  <span style="color:#f92672">&#34;id_token_signing_alg_values_supported&#34;</span>: [
    <span style="color:#e6db74">&#34;RS256&#34;</span>
  ],
  <span style="color:#f92672">&#34;issuer&#34;</span>: <span style="color:#e6db74">&#34;https://cognito-idp.eu-west-1.amazonaws.com/$COGNITO_USERPOOL_ID&#34;</span>,
  <span style="color:#f92672">&#34;jwks_uri&#34;</span>: <span style="color:#e6db74">&#34;https://cognito-idp.eu-west-1.amazonaws.com/$COGNITO_USERPOOL_ID/.well-known/jwks.json&#34;</span>,
  <span style="color:#f92672">&#34;response_types_supported&#34;</span>: [
    <span style="color:#e6db74">&#34;code&#34;</span>,
    <span style="color:#e6db74">&#34;token&#34;</span>,
    <span style="color:#e6db74">&#34;token id_token&#34;</span>
  ],
  <span style="color:#f92672">&#34;scopes_supported&#34;</span>: [
    <span style="color:#e6db74">&#34;openid&#34;</span>,
    <span style="color:#e6db74">&#34;email&#34;</span>,
    <span style="color:#e6db74">&#34;phone&#34;</span>,
    <span style="color:#e6db74">&#34;profile&#34;</span>
  ],
  <span style="color:#f92672">&#34;subject_types_supported&#34;</span>: [
    <span style="color:#e6db74">&#34;public&#34;</span>
  ],
  <span style="color:#f92672">&#34;token_endpoint&#34;</span>: <span style="color:#e6db74">&#34;https://$COGNITO_DOMAIN.auth.eu-west-1.amazoncognito.com/oauth2/token&#34;</span>,
  <span style="color:#f92672">&#34;token_endpoint_auth_methods_supported&#34;</span>: [
    <span style="color:#e6db74">&#34;client_secret_basic&#34;</span>,
    <span style="color:#e6db74">&#34;client_secret_post&#34;</span>
  ],
  <span style="color:#f92672">&#34;userinfo_endpoint&#34;</span>: <span style="color:#e6db74">&#34;https://$COGNITO_DOMAIN.auth.eu-west-1.amazoncognito.com/oauth2/userInfo&#34;</span>
}
</code></pre></div><p>Some apps (for example, <strong>Pomerium</strong> and <strong>ArgoCD</strong>) will use the IDP Provider URL to discover the Oauth2 Token/Authorize/userInfo endpoints, rather than having to supply them yourself.</p>
<h4 id="terraform">Terraform</h4>
<p>I have been setting up most of this in AWS using Terraform. An example Terraform module is below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-hcl" data-lang="hcl"><span style="color:#75715e">## Create the User Pool
</span><span style="color:#75715e"></span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_cognito_user_pool&#34; &#34;kube-web-view&#34;</span> {
  name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;userpool-kube-web-view&#34;</span>
  alias_attributes <span style="color:#f92672">=</span> [
    <span style="color:#e6db74">&#34;email&#34;</span>,
    <span style="color:#e6db74">&#34;preferred_username&#34;</span>
  ]

  auto_verified_attributes <span style="color:#f92672">=</span> [
    <span style="color:#e6db74">&#34;email&#34;</span>
  ]

  <span style="color:#66d9ef">schema</span> {
    attribute_data_type      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;String&#34;</span>
    developer_only_attribute <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>
    mutable                  <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
    name                     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;name&#34;</span>
    required                 <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>

    <span style="color:#66d9ef">string_attribute_constraints</span> {
      min_length <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span>
      max_length <span style="color:#f92672">=</span> <span style="color:#ae81ff">70</span>
    }

  <span style="color:#66d9ef">schema</span> {
    attribute_data_type      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;String&#34;</span>
    developer_only_attribute <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>
    mutable                  <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
    name                     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;email&#34;</span>
    required                 <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>

    <span style="color:#66d9ef">string_attribute_constraints</span> {
      min_length <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span>
      max_length <span style="color:#f92672">=</span> <span style="color:#ae81ff">70</span>
    }
  }

  <span style="color:#66d9ef">admin_create_user_config</span> {
    allow_admin_create_user_only <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
  }

  tags <span style="color:#f92672">=</span> {
    &#34;Name&#34; <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;userpool-kube-web-view&#34;</span>
  }
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">## Create the oauth2 Domain
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_cognito_user_pool_domain&#34; &#34;kube-web-view&#34;</span> {
  domain <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;oauth-kube-web-view&#34;</span>
  user_pool_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_cognito_user_pool</span>.<span style="color:#66d9ef">kube</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">web</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">view</span>.<span style="color:#66d9ef">id</span>
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">## kube-web-view Client
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_cognito_user_pool_client&#34; &#34;kube-web-view&#34;</span> {
  name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;kube-web-view&#34;</span>
  user_pool_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_cognito_user_pool</span>.<span style="color:#66d9ef">kube</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">web</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">view</span>.<span style="color:#66d9ef">id</span>

  allowed_oauth_flows <span style="color:#f92672">=</span> [
    <span style="color:#e6db74">&#34;code&#34;</span>,
    <span style="color:#e6db74">&#34;implicit&#34;</span>
  ]

  allowed_oauth_scopes <span style="color:#f92672">=</span> [
    <span style="color:#e6db74">&#34;email&#34;</span>,
    <span style="color:#e6db74">&#34;openid&#34;</span>,
    <span style="color:#e6db74">&#34;profile&#34;</span>,
  ]

  supported_identity_providers <span style="color:#f92672">=</span> [
    <span style="color:#e6db74">&#34;COGNITO&#34;</span>
  ]

  generate_secret <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>

  allowed_oauth_flows_user_pool_client <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>

  callback_urls <span style="color:#f92672">=</span> [
    <span style="color:#e6db74">&#34;https://{my-kube-web-view-host}/oauth2/callback&#34;</span>
  ]
}<span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">
</span><span style="color:#75715e">## Outputs
</span><span style="color:#75715e"></span>
<span style="color:#66d9ef">output</span> <span style="color:#e6db74">&#34;kube-web-view-id&#34;</span> {
  description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Kube Web View App ID&#34;</span>
  value <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_cognito_user_pool_client</span>.<span style="color:#66d9ef">kube</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">web</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">view</span>.<span style="color:#66d9ef">id</span>
}

<span style="color:#66d9ef">output</span> <span style="color:#e6db74">&#34;kube-web-view-secret&#34;</span> {
  description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Kube Web View App Secret&#34;</span>
  value <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_cognito_user_pool_client</span>.<span style="color:#66d9ef">kube</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">web</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">view</span>.<span style="color:#66d9ef">client_secret</span>
}
</code></pre></div><p>You will likely need to customise this to match your environment, but this should at least get you started.</p>
<h2 id="apps">Apps</h2>
<p>So far I have integrated (or at least attempted to integrate) the following apps on Kubernetes with AWS Cognito, with some limitations: -</p>
<ul>
<li><a href="https://codeberg.org/hjacobs/kube-web-view">Kubernetes Web View</a> - Read-only lightweight dashboard of Kubernetes with permalinks
<ul>
<li>This project now has AWS Cognito documentation, which I contributed</li>
</ul>
</li>
<li><a href="https://argoproj.github.io/argo-cd/">ArgoCD</a> - GitOps-style Continuous Deployment for Kubernetes
<ul>
<li>Yet to submit documentation, but will likely do so in the near future</li>
</ul>
</li>
<li><a href="https://www.spinnaker.io/">Spinnaker</a> - Comprehensive Continuous Deployment tool, covering a number of providers (including AWS, Kubernetes, GCE, Azure etc)
<ul>
<li>Failed to get this working, will work more on this one in the future</li>
</ul>
</li>
<li><a href="https://www.pomerium.io/">Pomerium</a> - Oauth2-enabled Reverse Proxy - allows Oauth2 authentication in front of resources that do not support it natively</li>
</ul>
<h3 id="kubernetes-web-view">Kubernetes Web View</h3>
<p><img src="/img/kube-web-view.png" alt="Kubernetes Web View"></p>
<p>Kubernetes Web View is an application developed by <a href="https://srcco.de/">Henning Jacobs</a> from Zalando. Zalando have provided a number of useful applications for Kubernetes.</p>
<p>Kubernetes Web View provides a simple read-only dashboard, with predictable URLs for resources rather than uniquely generated per user. This helps for copying-and-pasting links in chats to people so that they can see the same view as you.</p>
<p>Henning also runs the <a href="https://github.com/hjacobs/kubernetes-failure-stories">Kubernetes Failure Stores</a> repository. If you want to learn from others who have used Kubernetes, and what not to do, go here. I have made a number of updates to how we run our clusters and apps on Kubernetes due to the stories in this repository.</p>
<p>A lot of the instructions above were expanded from the documentation I contributed to the Kubernetes Web View project, that can be seen <a href="https://kube-web-view.readthedocs.io/en/latest/oauth2.html#aws-cognito-provider">here</a>.</p>
<h4 id="kubernetes-deployment">Kubernetes Deployment</h4>
<p>The Deployment YAML that you use in Kubernetes would look something like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">apiVersion</span>: apps/v1
<span style="color:#66d9ef">kind</span>: Deployment
<span style="color:#66d9ef">metadata</span>:
  <span style="color:#66d9ef">labels</span>:
    <span style="color:#66d9ef">application</span>: kube-web-view
  <span style="color:#66d9ef">name</span>: kube-web-view
<span style="color:#66d9ef">spec</span>:
  <span style="color:#66d9ef">replicas</span>: <span style="color:#ae81ff">1</span>
  <span style="color:#66d9ef">selector</span>:
    <span style="color:#66d9ef">matchLabels</span>:
      <span style="color:#66d9ef">application</span>: kube-web-view
  <span style="color:#66d9ef">template</span>:
    <span style="color:#66d9ef">metadata</span>:
      <span style="color:#66d9ef">labels</span>:
        <span style="color:#66d9ef">application</span>: kube-web-view
    <span style="color:#66d9ef">spec</span>:
      <span style="color:#66d9ef">serviceAccountName</span>: kube-web-view
      <span style="color:#66d9ef">containers</span>:
      - <span style="color:#66d9ef">name</span>: kube-web-view
        <span style="color:#75715e"># see https://codeberg.org/hjacobs/kube-web-view/releases</span>
        <span style="color:#66d9ef">image</span>: hjacobs/kube-web-view:latest
        <span style="color:#66d9ef">args</span>:
        - --port=<span style="color:#ae81ff">8080</span>
        <span style="color:#75715e"># uncomment the following line to enable pod logs</span>
        <span style="color:#75715e"># (disabled by default as they might consider sensitive information)</span>
        <span style="color:#75715e"># - &#34;--show-container-logs&#34;</span>
        <span style="color:#75715e"># uncomment the following line to unhide secret data</span>
        <span style="color:#75715e"># see also https://kube-web-view.readthedocs.io/en/latest/security.html</span>
        <span style="color:#75715e"># - &#34;--show-secrets&#34;</span>
        <span style="color:#66d9ef">ports</span>:
        - <span style="color:#66d9ef">containerPort</span>: <span style="color:#ae81ff">8080</span>
        <span style="color:#66d9ef">env</span>:
        - <span style="color:#66d9ef">name</span>: OAUTH2_AUTHORIZE_URL
          <span style="color:#66d9ef">value</span>: <span style="color:#e6db74">&#34;https://{AWS_COGNITO_DOMAIN_PREFIX}.auth.eu-west-1.amazoncognito.com/oauth2/authorize&#34;</span>
        - <span style="color:#66d9ef">name</span>: OAUTH2_ACCESS_TOKEN_URL
          <span style="color:#66d9ef">value</span>: <span style="color:#e6db74">&#34;https://{AWS_COGNITO_DOMAIN_PREFIX}.auth.eu-west-1.amazoncognito.com/oauth2/token&#34;</span>
        - <span style="color:#66d9ef">name</span>: OAUTH2_CLIENT_ID
          <span style="color:#66d9ef">value</span>: <span style="color:#e6db74">&#34;{AWS_COGNITO_APP_CLIENT_ID}&#34;</span>
        - <span style="color:#66d9ef">name</span>: OAUTH2_CLIENT_SECRET
          <span style="color:#66d9ef">value</span>: <span style="color:#e6db74">&#34;{AWS_COGNITO_APP_CLIENT_SECRET}&#34;</span>
        <span style="color:#66d9ef">readinessProbe</span>:
          <span style="color:#66d9ef">httpGet</span>:
            <span style="color:#66d9ef">path</span>: /health
            <span style="color:#66d9ef">port</span>: <span style="color:#ae81ff">8080</span>
        <span style="color:#66d9ef">resources</span>:
          <span style="color:#66d9ef">limits</span>:
            <span style="color:#66d9ef">memory</span>: 100Mi
          <span style="color:#66d9ef">requests</span>:
            <span style="color:#66d9ef">cpu</span>: 5m
            <span style="color:#66d9ef">memory</span>: 100Mi
        <span style="color:#66d9ef">securityContext</span>:
          <span style="color:#66d9ef">readOnlyRootFilesystem</span>: <span style="color:#66d9ef">true</span>
          <span style="color:#66d9ef">runAsNonRoot</span>: <span style="color:#66d9ef">true</span>
          <span style="color:#66d9ef">runAsUser</span>: <span style="color:#ae81ff">1000</span>
</code></pre></div><ul>
<li><strong>{AWS_COGNITO_DOMAIN_PREFIX}</strong> - Replace this with what you set in <strong>Domain Name</strong> Section</li>
<li><strong>{AWS_COGNITO_APP_CLIENT_ID}</strong> - Got to your App Client, and get the Client ID</li>
<li><strong>{AWS_COGNITO_APP_CLIENT_SECRET}</strong> - Got to your App Client, and get the Client Secret</li>
</ul>
<h3 id="argocd">ArgoCD</h3>
<p><img src="/img/argocd.png" alt="ArgoCD UI"></p>
<p>ArgoCD is a very lightweight Continuous Deployment solution, using &ldquo;GitOps&rdquo;, defining your repositories as the source of truth for deployment, configuration and versioning, rather than your CI (continuous integration) application.</p>
<p>ArgoCD uses the IDP Provider URL. You supply the Oauth details using a ConfigMap, with an example below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">apiVersion</span>: v1
<span style="color:#66d9ef">kind</span>: ConfigMap
<span style="color:#66d9ef">metadata</span>:
  <span style="color:#66d9ef">name</span>: argocd-cm
  <span style="color:#66d9ef">namespace</span>: argocd
  <span style="color:#66d9ef">labels</span>:
    <span style="color:#66d9ef">app.kubernetes.io/name</span>: argocd-cm
    <span style="color:#66d9ef">app.kubernetes.io/part-of</span>: argocd
<span style="color:#66d9ef">data</span>:
  <span style="color:#75715e"># Argo CD&#39;s externally facing base URL (optional). Required when configuring SSO</span>
  <span style="color:#66d9ef">url</span>: https://argocd.example.com

  <span style="color:#75715e"># OIDC configuration as an alternative to dex (optional).</span>
  <span style="color:#66d9ef">oidc.config</span>: <span style="color:#e6db74">|
</span><span style="color:#e6db74">    name: CloudCall</span>
    <span style="color:#66d9ef">issuer</span>: https://cognito-idp.eu-west<span style="color:#ae81ff">-1.</span>amazonaws.com/{USER_POOL_ID}
    <span style="color:#66d9ef">clientID</span>: {AWS_COGNITO_APP_CLIENT_ID}
    <span style="color:#66d9ef">clientSecret</span>: {AWS_COGNITO_APP_CLIENT_SECRET} 
    <span style="color:#75715e"># Optional set of OIDC scopes to request. If omitted, defaults to: [&#34;openid&#34;, &#34;profile&#34;, &#34;email&#34;, &#34;groups&#34;]</span>
    <span style="color:#66d9ef">requestedScopes</span>: [<span style="color:#e6db74">&#34;openid&#34;</span>, <span style="color:#e6db74">&#34;profile&#34;</span>, <span style="color:#e6db74">&#34;email&#34;</span>]
    <span style="color:#75715e"># Optional set of OIDC claims to request on the ID token.</span>
    <span style="color:#66d9ef">requestedIDTokenClaims</span>: {<span style="color:#66d9ef">&#34;groups&#34;: {&#34;essential&#34;: </span><span style="color:#66d9ef">true</span>}}
</code></pre></div><h3 id="spinnaker">Spinnaker</h3>
<p>Spinnaker does look very promising. It is a Continuous Deployment solution that was developed inside of Netflix. It is very heavy on resources though, so I would avoid trying to run it on smaller clusters.</p>
<p>I am yet to get Spinnaker working correctly with AWS Cognito though, so I will revisit this in a later blog post.</p>
<h3 id="pomerium">Pomerium</h3>
<p>Pomerium provides a reverse proxy feature, in a similar way to NGINX Reverse Proxying. Instead of relying on the applications to provide their own Oauth2-based authentication and authorization, Pomerium can provide it for them.</p>
<p>The slight disadvantage is that if you are already using some form of Ingress controller, you will be forwarding through the ingress, then Pomerium, then to your application for every request.</p>
<p>The extra hop and processing time may make a difference to what you are protecting. Whether the benefits of having your applications fronted by Oauth2-based authentication/authorization outweigh the extra overhead is up to you to decide.</p>
<p>The ConfigMap used looks like the below: -</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#66d9ef">apiVersion</span>: v1
<span style="color:#66d9ef">data</span>:
  <span style="color:#66d9ef">config.yaml</span>: <span style="color:#e6db74">|
</span><span style="color:#e6db74">     # Main configuration flags : https://www.pomerium.io/reference/</span>
     <span style="color:#66d9ef">authenticate_service_url</span>: https://k8s-auth-prod.example.com
     <span style="color:#66d9ef">authenticate_internal_url</span>: https://pomerium-authenticate-service.default.svc.cluster.local
     <span style="color:#66d9ef">authorize_service_url</span>: https://pomerium-authorize-service.default.svc.cluster.local
     
     
     <span style="color:#66d9ef">idp_provider</span>: oidc
     <span style="color:#66d9ef">idp_provider_url</span>: https://cognito-idp.eu-west<span style="color:#ae81ff">-1.</span>amazonaws.com/{USER_POOL_ID}
     <span style="color:#66d9ef">idp_client_id</span>: {AWS_COGNITO_APP_CLIENT_ID}
     <span style="color:#66d9ef">idp_client_secret</span>: <span style="color:#e6db74">&#34;{AWS_COGNITO_APP_CLIENT_SECRET}&#34;</span>
     <span style="color:#66d9ef">idp_scopes</span>: [<span style="color:#e6db74">&#34;openid&#34;</span>, <span style="color:#e6db74">&#34;email&#34;</span>, <span style="color:#e6db74">&#34;profile&#34;</span>] 
     
     <span style="color:#66d9ef">policy</span>:
       - <span style="color:#66d9ef">from</span>: https://tekton-prod.example.com
         <span style="color:#66d9ef">to</span>: http://tekton-dashboard.tekton-pipelines.svc.cluster.local:<span style="color:#ae81ff">9097</span>
         <span style="color:#66d9ef">allowed_domains</span>: 
           - example.com
       
       - <span style="color:#66d9ef">from</span>: https://k8s-prod-prometheus.example.com
         <span style="color:#66d9ef">to</span>: http://prometheus-k8s.monitoring.svc.cluster.local:<span style="color:#ae81ff">9090</span>
         <span style="color:#66d9ef">allowed_domains</span>: 
           - example.com
<span style="color:#66d9ef">kind</span>: ConfigMap
<span style="color:#66d9ef">metadata</span>:
  <span style="color:#66d9ef">name</span>: pomerium-config
</code></pre></div><p>When using Pomerium with AWS Cognito, you have to set the <strong>idp_scopes</strong>. By default, the Pomerium OIDC provider attempts the following scopes: -</p>
<ul>
<li><em>profile</em> - Supported by Cognito</li>
<li><em>email</em> - Supported by Cognito</li>
<li><em>offline_access</em> - Not supported by Cognito</li>
</ul>
<p>If you do not specify this, your requests will fail with <strong>Invalid Scopes</strong>.</p>
<h2 id="to-be-continued">To be continued</h2>
<p>I&rsquo;m working with putting more applications behind Oauth2, and I&rsquo;m sure I&rsquo;m also going to learn more about Oauth2 and OIDC along the way. In future posts I&rsquo;ll cover other applications that integrate with Oauth2, as well as ones which benefit from using <strong>Pomerium</strong> (or similar Oauth2 proxies).</p>
]]></content>
        </item>
        
        <item>
            <title>Network Engineer to DevOps Engineer - My experience of career shifts</title>
            <link>https://yetiops.net/posts/network-to-devops/</link>
            <pubDate>Sat, 17 Aug 2019 20:57:50 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/network-to-devops/</guid>
            <description>Can I Wireshark this? As mentioned in my previous posts, I spent many years working primarily in the networking industry. Even my first full time role was at a broadband company (admittedly in the call centre, but it still counts!).
Now, I work as a DevOps Engineer. How did that happen?
The network world The network industry itself has been around almost as long as computing expanded beyond gears and levers.</description>
            <content type="html"><![CDATA[<h2 id="can-i-wireshark-this">Can I Wireshark this?</h2>
<p>As mentioned in my previous posts, I spent many years working primarily in the networking industry. Even my first full time role was at a broadband company (admittedly in the call centre, but it still counts!).</p>
<p>Now, I work as a DevOps Engineer. How did that happen?</p>
<h2 id="the-network-world">The network world</h2>
<p>The network industry itself has been around almost as long as computing expanded beyond gears and levers. From the early days of ARPANet, to the advent of TCP/IP, IP addressing, to later on IPv6, there have always people working in the terminal, plugging away at a CLI interface making it work.</p>
<p>For over a decade, I was on one of them. I took great joy in configuring a Cisco 6500/ASR9000/887/Nexus 7k, or a Juniper MX/SRX, or a HP Procurve, or HP/H3C 5800 or 5900, or Mikrotiks, Brocade, and any of the Linux-based tools to talk routing protocols to them (Quagga, BIRD, OpenBGPD etc).</p>
<p>I spent my days working with BGP, MPLS, MTU issues (it&rsquo;s ALWAYS MTU), Spanning Tree (running it across a 200-mile distance worked&hellip;somehow!), PPP, IPSec, VxLAN, EVPN, and loving every second of it.</p>
<p>Firewalling goes hand in hand with networking (in terms of knowledge and blame), so I also tried my hand with Cisco ASAs, Checkpoint, learning IPTables, pining for Palo Alto and a lot more.</p>
<p>Forming and forging core networks, routing packets all over the world, it felt almost magical at times. I was enamoured, to the point of being addicted to studying for certifications in my free time (to learn, and to prove what I knew).</p>
<p>I loved networking. It was my thing. I felt like I was in in the networking industry for the long haul. So what changed?</p>
<h2 id="automation">Automation?</h2>
<p>Automation has been a bad word in the networking industry to some. Many believe it replace them. Others want to avoid being &ldquo;programmers&rdquo;, happy to plug away at the CLI. Compared to the Systems Administration world, and especially the cloud offerings, the networking industry (and the vendors) have lagged behind.</p>
<p>Most networking equipment and vendors still put the CLI first. Automation often involves complex Expect-style scripts, effectively simulating someone typing in commands and reacting to the output.</p>
<p>Libraries and modules have come along to make this job easier (e.g. Netmiko for Python, NAPALM, and Ansible support). Often though, it isn&rsquo;t much of an abstraction. You are still expected to know the commands and expected output. Also, there&rsquo;s little support for agent-based configuration management (e.g. Puppet, Salt) in anything but the latest equipment.</p>
<p>Some vendors are starting to include APIs, YANG and/or gRPC endpoints. The problem is many providers still maintain legacy environments. I have worked at places still using Cisco 6500s (first released over 20 years ago!) as the backbone of their network more recently than you would hope (i.e. last year!).</p>
<h2 id="winds-of-change">Winds of change</h2>
<p>In early 2015, I took a role at one of the largest telecoms companies in the UK (and in the world), working on their burgeoning cloud/data centre offering. They were heavily invested in HP/H3C networking. My previous company&rsquo;s core network was nearly all HP/H3C, so I was a good fit.</p>
<p>I worked on improving their core network (including merging legacy networks together, decommissioning old suppliers, re-merging their autonomous system back together). During this, I noticed that changes could be made to the core network (usually by me&hellip;) and nobody had any idea. No alerting, no version control, nothing.</p>
<p>I investigated. It turned out Solarwinds NPM was installed as a plugin for our Monitoring platform (Solarwinds, oddly enough). It would periodically retrieve the configuration from the network kit, and it was trying to send out configuration changes every day to the NOC team.</p>
<p>This was great, except that when it sent emails with the configuration diffs, it sent the full configuration of every device (before and after changes) AND what differed (if anything). Fine with a couple of devices, but we had around 40 or 50 in total. Every email was 80mb. Our email quota was 2mb per email. Spot the problem.</p>
<p>A later version of Solarwinds fixed this issue (and only sent JUST the differences, rather than the full configuration), but we had a lot of custom alerts that would need re-writing when upgrading.</p>
<p>So I decided to build a tool to do it myself. I could have implemented something like Rancid or similar, but this would require opening up network access to it. People there were happy with the current tooling, and didn&rsquo;t see the problem with the status quo.</p>
<h2 id="fun-with-python">Fun with Python</h2>
<p>I had dabbled with Python before, but I hadn&rsquo;t done anything of note in it. Basic user input, search and replace, nothing complex. I used this as a project to learn it.</p>
<p>The tool I built would log on to each device, get its current configuration, and save a copy locally. Once all the devices in the list had their configurations retrieved, they were committed to Git. As those who use Git know, it is very easy to show differences between the current commit, and the last commit (i.e. the one which ran the day before).</p>
<p>Using Netmiko (which supported most of the vendors we used), and Paramiko (for vendors that Netmiko didn&rsquo;t support), I felt happy with what I had put together. It involved classes, native Python Git modules, and functions.</p>
<p>Seasoned Python developers will think this is quite basic. Now when I look back on it, I do too. I have since rewritten/refactored similar scripts for other purposes (to include parallelism, reporting etc).</p>
<p>It&rsquo;s worth knowing that about 2 years prior (in my previous company), I nearly gave up on a project that required writing basic Bash scripts. To quote myself at the time, &ldquo;I am not a programmer and I never will be&rdquo;. That mindset was changing.</p>
<h2 id="not-fun-with-networking">Not fun with networking</h2>
<p>I had scratched an itch, I had built a tool that filled a gap in our processes. People came to rely on it. This was good, and gave me confidence to do more automation and scripting. I also gained familiarity with Netmiko, a Python module which automated basic shell interaction with a number of networking kit. It would automatically recognise prompts, you could tell it that certain commands were for configuration and some for just verification. It became my go-to library for Network interaction from then on.</p>
<p>Unfortunately I had also seen first hand how difficult it was to automate equipment (i.e. most network kit) that was never intended to be automated.</p>
<p>A lot of the kit in our legacy network was not natively supported by Netmiko. We also had some interesting HP firewalling that nobody has heard of before or since that role. For these, I would need to use Paramiko instead (which did basic SSH interaction).  I had to tell it the exact prompt to expect, the time to wait on commands, the exact responses to expect from commands, and sometimes the terminal type (not everything worked well with VT100 for example) so it wouldn&rsquo;t screw up the output.</p>
<p>Compared with my colleagues, who were dabbling with Ansible to manage multiple machines, Jenkins to deploy changes to server and code configuration, something didn&rsquo;t feel right.</p>
<p>I had also started to reach a point in networking where I hadn&rsquo;t learnt anything groundbreaking in a long time. I could learn the finer points of what was already there. The new developments in the industry seemed like a rehash of what had gone before (how many different ways have we tried stretched Layer 2 now?).</p>
<p>It is still an industry that is moving slowly, and even the &ldquo;big&rdquo; changes are not the entire paradigm shifts you find in the Cloud and Systems world. Facebook may be implementing something like Open/R (a distributed system for routing information), but some poor helpless souls are still using RIP unironically.</p>
<h2 id="kuberwhatnow">Kuberwhatnow?</h2>
<p>The Systems guys on my team were very much Windows-first guys, along with a lot of VMWare knowledge. The company (or at least the tech lead) pushed more towards open source, meaning that anyone who had Linux skills in the team became immediately useful. I had been using Linux for longer that I had been in networking (although not as much professional experience), so I started getting more involved on the systems side.</p>
<p>The developers were rolling out Docker containers, but they only had a single machine to do it with. Every now and then (whether due to power cuts/maintenance or whatever) the Docker box would fail. This needed to change.</p>
<p>I&rsquo;d heard about this Kubernetes thing (through the wonders of Twitter) and decided to give it a go. It was a new world of automation, orchestration, being able to interact with APIs, and watch the cluster just do its thing. Throw in some Ansible, and I could spin up a virtual machine, and watch it join the cluster 5 minutes later. A world away from the artisanal core network configs required to make everything run just about right.</p>
<p>I had a slight eureka moment, seeing where the industry was heading towards. At the time, Docker Swarm was still an option, and Mesosphere was in use in many companies. Little more than 3 years later and Kubernetes has dominated the container world.</p>
<p>Strangely, one of the first things that was deployed on the cluster was a Graphite-based tool to gather stats from CheckPoint firewalls and push them to a server which ran Graphite and a Grafana dashboard. Why did we need to create this tool? Because Solarwinds couldn&rsquo;t have a device be polled differently, but using the same management IP, and Solarwinds were unwilling to implement the latest SNMP MIB that would get around this issue.</p>
<p>I can probably thank Solarwinds (begrudgingly) for a lot of my current career.</p>
<h2 id="moving-on">Moving on</h2>
<p>I moved on to a different role, a Solutions Architect kind of job. It seemed like a good fit for my family and I, in terms of the location and the pay increase. Also a role that was working at a higher level (i.e. overall design and architecture rather than configuration) would help me see the bigger picture.</p>
<p>Unfortunately the role was very hands off. I spent 99% of my time in Excel and Word. That did not work for me. I did have more of an appreciation of budgeting, purchase orders, high level design, but I&rsquo;m at my happiest in front of a black-background terminal with white/green text.</p>
<p>So I applied for a hands-on role again, working for a small ISP. This seemed perfect. Back in my earlier days in the industry, I always dreamed of working for ISPs. Working with large core networks, making changes at a large scale, it all seemed exciting.</p>
<h2 id="not-living-the-dream">Not living the dream</h2>
<p>I started at the role. And it hit home. This wasn&rsquo;t for me any longer. I grew more and more dissatisfied with networking.</p>
<p>While doing customer rollouts, I would build dashboards (using Python, Flask, Bootstrap and Postgres) that would automatically updated when engineers uploaded their job spreadsheets. I built configuration management tools for our network infrastructure. Whenever I actually had to log on to a device directly, it made me feel a bit sad.</p>
<p>The love for networking was gone. It didn&rsquo;t help that the past few jobs had issues with management, workload, and the work/life balance being a mystery to them. In those times before though, I used to fall back on how much I loved networking. Instead I found increasingly the more I had to work on the network, the more disenchanted with the networking industry I became.</p>
<h2 id="time-for-a-change">Time for a change</h2>
<p>I decided the right path for me should be in the burgeoning DevOps world. My operational experience of managing Networks, Linux and Virtualisation would help me. My newfound love for development would help. I started looking into the tooling and infrastructure.</p>
<p>I brushed up on the AWS basics, made myself familiar with Azure, I started playing around with Terraform. I had worked with Ansible a little before, so I brushed up on that. And then I applied for DevOps jobs. Every day. For months.</p>
<p>I struggled to get much feedback. There were no shortage of jobs that wanted someone to come in and put some basic Python scripts together to do network rollouts, but this really wasn&rsquo;t what I wanted. I&rsquo;d still be managing the network, and still end up logging in manually to see why automation hadn&rsquo;t worked.</p>
<h2 id="use-this-one-trick">&ldquo;Use this one trick!&rdquo;</h2>
<p>I decided to try something. My resume had my current role as Network Engineer. While this was technically my job title, I was also managing all the Virtualisation, the MySQL database, and also managing all the additional tooling I had created. So I changed my job title to be Network And Systems Engineer.</p>
<p>Next thing I knew, I had three or four phone interviews within the week. A couple didn&rsquo;t work out, primarily as they were very Microsoft focussed. I can do some of the basics within Windows. I had even migrated over from two separate Active Directory domains to a single one in the role I was in. Really though, Windows is not where my skillset is.</p>
<p>One of the phone interviews I had was with a company I&rsquo;d already applied to twice before, for the same role (unbeknownst to me when I applied the third time!). It required someone with Linux skills, knowledge of the cloud and virtualisation, some development experience, and any knowledge of carrier networking would be beneficial too. This seemed like a perfect fit.</p>
<p>3 months later I started there as a DevOps Engineer.</p>
<h2 id="whoami">Whoami</h2>
<p>I still feel odd about having left the networking industry. The excitement I used to feel for it, the comfort I had in my knowledge. I still have a hand in the network infrastructure at my current role, but to a much lesser degree than I used to.</p>
<p>Maybe when the industry has moved to being truly automated, and not just at the megascale companies (Amazon, Facebook, Google), it may be more interesting to me again.</p>
<p>For now though I&rsquo;m excited about my job again, learning every day, and finding a lot of room to grow.</p>
]]></content>
        </item>
        
        <item>
            <title>Can I do this? - Doubt and Imposter Syndrome</title>
            <link>https://yetiops.net/posts/2-imposter/</link>
            <pubDate>Thu, 01 Aug 2019 21:37:44 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/2-imposter/</guid>
            <description>Imposter Syndrome Imposter syndrome is the feeling that you are a fraud in your job role. Somehow you got your role through sheer luck and at some point it will all going come crashing down.
Do I feel this? Not as much as I used to. Earlier in my career? Definitely. I still feel doubts whenever I encounter a new technology that I have little to no experience with, but these go away quickly (usually after working with said technology for a little while)</description>
            <content type="html"><![CDATA[<h2 id="imposter-syndrome">Imposter Syndrome</h2>
<p>Imposter syndrome is the feeling that you are a <em>fraud</em> in your job role. Somehow you got your role through sheer luck and at some point it will all going come crashing down.</p>
<p>Do I feel this? Not as much as I used to. Earlier in my career? Definitely. I still feel doubts whenever I encounter a new technology that I have little to no experience with, but these go away quickly (usually after working with said technology for a little while)</p>
<p>I have found that doubt and imposter syndrome are not necessarily equal, as often there can be one without the other. One frequently fuels the other though, making it feel like there is no way out of the situation.</p>
<p>Here&rsquo;s my long and rambling story.</p>
<h2 id="my-background">My background</h2>
<p>I do not have a traditional CompSci background. Computers were always around from a very young age, but I wasn&rsquo;t a teenage programming prodigy or a network expert. The first time I had a mini LAN party with a friend, my dad had to come round and put in the IP address details. Given where I was a few years later, this seems alien to me now!</p>
<h3 id="my-teens">My teens</h3>
<p>Up until my mid-teens, I was convinced that I&rsquo;d become a programmer. I would go to university, study Computer Science, and go and build <strong>things</strong>. At this time, my father was a SysAdmin, in the days of Novell Netware, Windows NT4.0. I had been around computers from being a toddler. Being in something to do with computing just made sense.</p>
<p>Then I got into music, and I mean REALLY got into it. Plans changed. Out went the Computer Science degree path. Out went the (admittedly terrible!) web design I did in my free time, in came practicing guitar for hours a day. Much to the chagrin of my parents.</p>
<p>When it came to choosing my degree, I chose Music Technology (recording music, and everything related to it)</p>
<h3 id="my-degree">My degree</h3>
<p>During my degree years, I had my first encounter with imposter syndrome. In my secondary school years, I achieved consistently good grades. I was a good student. I thought the step up to a degree wouldn&rsquo;t be too different.</p>
<p>However I was now surrounded by people who had been playing and recording music for years, in some cases over a decade. Before going to university, I could barely record myself and I had only played guitar for about 2 years.</p>
<p>After 3 years of this degree (with a lot of doubts) I still somehow managed to come away with a decent grade. To this day I cannot produce the same level of polish and professional-sounding recordings as some I heard from my fellow students, so I thought the grade was pure luck. This was my first experience of imposter syndrome and doubt.</p>
<p>I left university feeling utterly unsatisfied with what I had done, and only hoped I could improve.</p>
<h3 id="first-job">First job</h3>
<p>After university, I spent months trying to find a job. I first tried to find jobs in recording studios, but they were all overburdened with candidates, or usually someone running a studio in their bedroom!</p>
<p>I gave up, and took a job in a call centre working for Orange Broadband. This is where I entered the real world, so to speak. The doubts about my future crept in.</p>
<p>Working in a call centre is what a lot of people now go through as their first job. For me though, I&rsquo;m naturally an introvert who tries to avoid confrontation. These two traits are the antithesis of working in a call centre. Even worse, it was for (at the time) the lowest rated ISP in the UK. The dreams of a life in music dwindled.</p>
<p>I spent most of my hours outside work looking for new jobs, playing guitar and trying to get Linux working on a fairly decent (at the time) HP laptop. Bad memories of <code>ndiswrapper</code> come to mind&hellip;</p>
<p>I would regularly apply to 100-200 jobs a month (sometimes significantly more), and would hear nothing back. Was I stuck in this job?</p>
<p>While there, I also applied to be on the Team Leader development programme. Looking back now, I find this weirdly funny. I&rsquo;m not a natural manager, and have never worked towards a management role since. At the time though, it was the only way I could see making a step up.</p>
<h3 id="a-chance">A chance?</h3>
<p>After months of searching, a NOC Analyst role popped up. The company specialised in transaction processing and connectivity for retailers, primarily using Linux as a platform and Cisco for networking, which seemed very interesting. However, due to my self-doubt at the time, I did not present myself as a very capable individual, I downplayed all of my skills to make sure I wouldn&rsquo;t get &ldquo;found out&rdquo;.</p>
<p>Despite having some hands on knowledge with Linux (around 18 months to 2 years in my personal time), I portrayed myself as being barely above a beginner. The only thing I could make any genuine reference to were the systems I used at my current job (for managing broadband customers). I came away assuming I didn&rsquo;t get the job. Why would I?</p>
<p>Technically I was right.  I didn&rsquo;t get the NOC Analyst role. Instead, they offered me a role on their ADSL/Broadband support desk.</p>
<h3 id="beginnings-of-my-career">Beginnings of my career</h3>
<p>Within weeks of starting this role, I realised that my Linux skill level would have been good enough for the role. In fact the one who had gotten the job had barely used Linux at all! Still, I was sure in my head that I couldn&rsquo;t have been ready for it.</p>
<p>In hindsight, I would have been ready, and I did myself out of a higher salary and more interesting role, simply due to not wanting to be &ldquo;found out&rdquo;. Doubt and the fear of being the imposter.</p>
<p>I did what I could to learn off people there, but I never felt ready to be part of the NOC. I always believed if I got the job, and made even the tiniest of mistakes, that was it. I&rsquo;d be sacked.</p>
<h3 id="working-my-way-up">Working my way up</h3>
<p>Despite my doubts, the company took another chance on me, and made me a NOC Analyst. I learned a lot, but it was primarily a monitoring role. It had a lot of downtime, due to long nightshifts, or long weekend shifts, waiting for alerts to come through.</p>
<p>As the company was heavily into Linux and Networking, I felt that my networking knowledge was lacking. I started to look into the Cisco CCNA to help.</p>
<p>I read up and made notes on my night and weekend shifts, and started to try commands out on Cisco simulators. After more than a year of studying, I felt confident enough to take the exams. That I failed. Badly. I was an imposter again, I wasn&rsquo;t good enough.</p>
<h3 id="try-again">Try again!</h3>
<p>I wanted to give up with the CCNA. I felt like maybe I had reached the limit of my abilities. But I convinced myself to keep going, because the job role was starting to affect my health.</p>
<p>The erratic shift patterns (12 hour night shifts, followed a few days later by 12 hour day shifts, some weekends etc) were taking their toll on my health. I had no sleeping pattern. Often I would be in work at 7am, having been unable to sleep until 5:45am, before getting up at 6:30am to go in.</p>
<p>I needed a new strategy. In searching around the internet, I realised I had not done enough labbing so the fundamental networking concepts were not sinking in. I had also relied entirely on what was in the certification guides. I didn&rsquo;t check the Cisco exam topic reference to see if I had covered everything. Also, the sheer breadth of what the CCNA covered was a lot for anyone.</p>
<p>Cisco had (and still have I believe) two ways of taking the exams. You could do two exams, ICND1 and ICND2, with ICND2 have more advanced concepts than ICND1, and ICND1 driving home the fundamentals. You can also do a combined exam which covered both. Originally, I had done the combined exam. This was too much for someone who was still getting to grips with some of the concepts.</p>
<p>Strangely, I was actually better at the ICND2 topics anyway. I found them more interesting (basics of routing protocols, spanning tree etc), whereas the fundamentals were (while important) difficult to get excited about.</p>
<p>I split the exam, focussed on the fundamentals (ARP, IPv4 and IPv6 basics, OSI Layers, Subnetting), and passed ICND1. I then went to ICND2 feeling better, especially as I felt more confident in the topics anyway. I passed, and became a CCNA.</p>
<h3 id="certifications">Certifications?</h3>
<p>Certifications are not always important. They definitely do not make up for a lack of experience. What they are useful for though is showing a base theoretical knowledge in a subject. This is helpful for recruiters obviously, but it is also helpful in troubleshooting too.</p>
<p>Knowing how something <strong>should</strong> work means that you aren&rsquo;t relying just on what you&rsquo;ve done before, and you can work on systems or protocols that have you not used before.</p>
<p>Also, more pertinent to this post, they can help give you confidence. I felt like at minimum, my base knowledge must be better now. I may still be &ldquo;found out&rdquo;, but I would be able to get higher up in my career before I did.</p>
<h3 id="trying-to-see-daylight-again">Trying to see daylight again</h3>
<p>Now with my experience and my CCNA in tow, I thought I should be able to get out of the current role. No more long shifts, I might get a sleeping pattern again. So I applied to lots of jobs. And more. And more again. Nothing. I at least got a couple of interviews, but my trait of downplaying myself was still there. I did myself out of jobs again.</p>
<p>Instead, I found a role within the same company, an implementations engineer role.</p>
<h3 id="the-new-role">The new role</h3>
<p>This was a kind of production line/BAU-style role. Cisco router comes in, find and replace some terms in a template, apply to the router. Test it, send it out. It also involved making some changes on our Linux estate (which were less cookie cutter and template driven thankfully).</p>
<p>I quickly reached the limit of this role. I had also gotten bitten by the certification bug, and was working on my Cisco CCNP during this role (as well as Juniper, partly because of their free study guides)</p>
<h3 id="ccnp">CCNP</h3>
<p>I didn&rsquo;t learn from my previous mistakes. I still only used a single certification guide, I didn&rsquo;t check the exam topics, and then failed the exams when I took them. One of them I didn&rsquo;t pass until my third attempt. Doubts increased, and I felt maybe I had reached my limit again. Then I realised.</p>
<p>I remembered the issues I had with the CCNA, and yet somehow I was trying to do the same thing again? What a buffoon!</p>
<p>I paid more attention to the exam topics, labbed everything heavily, and used multiple study guides (and online resources were a lot better by now too). I passed the routing exam (BSCI at the time), and while it took me two attempts to pass the switching exam (known as BCMSN at the time), I just got back up, worked on it and tried again. I took the TSHOOT exam, and passed first time. I was a CCNP!</p>
<p>I started to feel more confident in my abilities, and began looking for a more challenging role. I found it as a Second Line Support engineer in the same company.</p>
<h3 id="not-the-challenge-i-was-thinking-of">Not the challenge I was thinking of</h3>
<p>I moved to the new role. The teams remit was &ldquo;If you can fix it, do it. You are not siloed&rdquo;. After working only on BAU implementations, or only monitoring part of the network, this role opened up my view on how the core network and supporting services actually worked, not just what I was allowed to see previously.</p>
<p>However, the team also came with a manager who others had found challenging or difficult. I had hoped that this was just a clash of personalities with people, or simply misunderstandings with people. It was not.</p>
<p>Needless to say, even the most minute of mistakes were on parade to the entire company, and often you felt like the worst engineer in existence. Even bugs in vendor equipment were somehow your fault. I had never felt like this much of an imposter. I could not shift the feeling. Any time I built up any confidence, it was wiped out at the next morning call.</p>
<p>Admittedly I also made some big mistakes while in this role too. My level of monitoring wasn&rsquo;t adequate during changes, which was something I needed to improve. I would rely too much on people&rsquo;s view of their technology I was supporting, rather than knowing it myself. I also still had a fear of confrontation, so wouldn&rsquo;t challenge people when I needed to.</p>
<p>I have never entirely gotten over this role. In some ways it has helped me, because my monitoring now goes overboard. I also have to know every system I work on inside out. However, I still do get nervous whenever a manager wants to speak to me, as I assume I&rsquo;m about to get roasted (which thankfully has never really happened since).</p>
<h3 id="hope">Hope</h3>
<p>During this role, I got involved in the networking community online. By chance, someone who frequented the same forum I posted on happened to work in the same city. The company he worked for had a vacancy for a Network Engineer, and he invited me for an interview.</p>
<p>I went to the interview and made the best account of myself as I could (despite my low view of myself with my current role). To my surprise, I was offered the job during my second stage interview with the company.</p>
<p>This did not go down well with my current manager, but I felt a sense of relief that things might get better. I still had my doubts, and wondering if I was good enough, but I hoped.</p>
<h3 id="new-company">New company</h3>
<p>I started at the new place, and was greeted with my new Linux workstation, running Fedora. I could get used to this, I just hoped they would keep me on. I shadowed the Senior Network Engineer (the guy who found me on the forums) and got a feel for what the place was like.</p>
<p>Towards the end of the first day the company had an outage, while I was shadowing.</p>
<h3 id="a-good-outage">A good outage?</h3>
<p>We discussed the problem, with me looking at the configuration of the network over his shoulder. I had a theory that the outage was due to Equal Cost Multipathing (i.e. trying to forward to an IP address over two or more routed links rather than just one) across two core interlinks. This isn&rsquo;t normally an issue, but one link had MPLS enabled, one didn&rsquo;t.</p>
<p>When MPLS traffic tries to cross a link that doesn&rsquo;t have MPLS enabled, it just drops the traffic. It was initially assumed that the second link wouldn&rsquo;t be used, due to it being in a non-backbone area in OSPF (OSPF being the core routing protocol in the company). However MPLS doesn&rsquo;t care about OSPF areas, just what routes are in the routing table.</p>
<p>My theory turned out to be correct, at which point MPLS was enabled on this link, and the outage was fixed, and then avoided in future.</p>
<p>This was a massive turning point for me, because I had managed to point out a problem, that someone who (in my head) was vastly more knowledgeable than me hadn&rsquo;t realised. Suddenly, I started thinking &ldquo;Wait, am I not a fraud? Am I capable?&rdquo;.</p>
<h3 id="am-i-good-enough">Am I good enough?</h3>
<p>Through the years of working at this company, my confidence in my abilities grew. Some days I still had my doubts. As more time passed though, and the more I dealt with the people at this company, I realised that everyone has the same doubts, Almost everyone has some form of imposter syndrome, a lot just hide it well or ignore it.</p>
<p>I <strong>finally</strong> realised that not knowing every single last thing about a technology was not the end of the world, and saying &ldquo;I don&rsquo;t know&rdquo; is actually an acceptable answer to a question.</p>
<p>I credit this role with restoring my confidence in myself and my abilities. The realisations I had during this time made me more able to deal with any future doubts I had. I never really felt like an &ldquo;imposter&rdquo; again.</p>
<h3 id="aftermath">Aftermath</h3>
<p>Since then, I have worked in other jobs. Some had their issues (big and small), but it was more down to company culture. I never experienced the level of doubt that I had previously.</p>
<p>I always viewed myself as on an equal footing with my teammates. I no longer questioned whether I was good enough, or whether I was going to be &ldquo;found out&rdquo;. I finally stopped feeling like the cause of all problems. I stopped believing that a single minor mistake could cost me my job.</p>
<h3 id="career-change">Career change?</h3>
<p>After a decade in the networking industry, I decided to move into the DevOps world instead. The networking industry is fairly stoic, and has a lot of companies and people who still trying to get beyond managing equipment manually.</p>
<p>In one of my roles, I had the chance to work with things like Ansible, Kubernetes, Jenkins, and started to build tools of my own in Python. While I still stayed in the networking industry for a few years after, I eventually decided that I&rsquo;d done all that I wanted to in the networking industry. Automation, development, the cloud and Linux as a primary focus, this is what I wanted to do.</p>
<p>I&rsquo;ll expand on the career change in a future post. In making the change, I did briefly feel some of the familiar feelings of doubt.</p>
<p>Was my confidence all about my knowledge in the networking industry, or could I cut it here too? The first couple of months were a bit of a whirlwind, but I got up to speed quite quickly. The feelings of doubt dissipated quickly. I am happy.</p>
<h3 id="summary">Summary</h3>
<p>I have rambled on in this post, so I thought it would be best to summarise a couple of the main points.</p>
<p>You may not think you are good enough for your role. You may feel like you&rsquo;re seconds way from being discovered, and everyone working out you&rsquo;re a fraud. You are not. Everyone has doubts, some people just hide it better.</p>
<p>You may not think you are capable of learning new skills, at least to the level you believe is required. You&rsquo;ll be surprised how many people think the same of you. Also, when a company introduces a new technology, is everyone an expert instantly? No, it takes time.</p>
<p>You are not alone in feeling this, don&rsquo;t feel like you are the odd one out. Imposter syndrome is everywhere, it&rsquo;s not just you!</p>
<p>Life is short. There will be setbacks, and some days you&rsquo;ll feel like you have no idea what you&rsquo;re doing. That&rsquo;s normal. Embrace it, use it as an opportunity to learn.</p>
<p>The mantra of &ldquo;better the devil you know&rdquo; only works if you enjoy your job. If you change to somewhere and it doesn&rsquo;t work out? Great, that means you just move on, but with more experience and knowledge of the industry. If it does work out? Perfect!</p>
]]></content>
        </item>
        
        <item>
            <title>YetiOps Introduction</title>
            <link>https://yetiops.net/posts/my-first-post/</link>
            <pubDate>Thu, 01 Aug 2019 20:45:36 +0100</pubDate>
            
            <guid>https://yetiops.net/posts/my-first-post/</guid>
            <description>Welcome to YetiOps This is the first post, at which point I should probably give a bit of an introduction to who I am, what I do, what I have done before, and why I am blogging at all.
Who am I? I am a very hairy human who lives in the East Midlands area of the UK. I work as a DevOps Engineer for a firm specialising in VoIP integration into CRM systems, and I love my job.</description>
            <content type="html"><![CDATA[<h2 id="welcome-to-yetiops">Welcome to YetiOps</h2>
<p>This is the first post, at which point I should probably give a bit of an introduction to who I am, what I do, what I have done before, and why I am blogging at all.</p>
<h3 id="who-am-i">Who am I?</h3>
<p>I am a very hairy human who lives in the East Midlands area of the UK. I work as a DevOps Engineer for a firm specialising in VoIP integration into CRM systems, and I love my job.</p>
<p>I am also a father to two wonderful children, and husband to a wonderful wife. I also play guitar, listen to a dizzying amount of obnoxiously over the top music, and have an obsession with motorsport.</p>
<h3 id="what-do-i-do">What do I do?</h3>
<p>The company I work for encourage new technology, staying up to date with interesting technology, and have a very fun environment to work in.</p>
<p>I touch everything from Linux, Docker, Kubernetes, AWS, Terraform, Packer, Salt, Ansible, Golang, Python, and a number of other technologies.</p>
<h3 id="what-have-i-done-before">What have I done before?</h3>
<p>Previous to my current role, I have spent over a decade in the networking industry, in service providers and enterprise. I still hold the Cisco CCNA and CCNP, and have previously held Juniper certifications, Brocade certifications, and made an attempt at the Cisco CCIE written exam twice (failed both times unfortunately, mostly due to a lack of time and preparation).</p>
<p>During this time, I also worked with a lot of Linux and Virtualisation (for monitoring systems, TACACS, configuration management etc).</p>
<h3 id="networking-to-devops">Networking to DevOps?</h3>
<p>How did I change from being primarily a Network guy to DevOps? This is something I&rsquo;m going to cover in a future post. The summary is a seed was planted in one of my previous roles, and my interest grew increasingly towards automation and development, and less towards the fairly stoic networking industry.</p>
<h3 id="why-am-i-blogging">Why am I blogging?</h3>
<p>A number of years ago, I ran a networking site called &ldquo;Working From My Shed&rdquo;, which covered a number of networking-related topics, including certification, vendor interoperability and just general findings in the industry. I enjoyed it, but my priorities changed over the years, firstly towards spending more time with music, and then later on to my family.</p>
<p>I missed blogging, and have now decided it is time to give it a go again.</p>
<h3 id="what-will-the-blog-be-about">What will the blog be about?</h3>
<p>I have a couple of posts I am going to do that are more towards my career path, and dealing with the dreaded imposter syndrome, but I will also be blogging on technology that I deal with day to day, as well as a lot that I am discovering.</p>
<h3 id="thank-you">Thank you</h3>
<p>I promise the future content will be a lot more interesting, but for now, here I am!</p>
]]></content>
        </item>
        
    </channel>
</rss>
