<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Joseph Heyburn</title>
    <description>The latest articles on DEV Community by Joseph Heyburn (@jdheyburn).</description>
    <link>https://dev.to/jdheyburn</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F37155%2Ffde86635-1d4c-44a5-b891-d673832ab3f1.jpg</url>
      <title>DEV Community: Joseph Heyburn</title>
      <link>https://dev.to/jdheyburn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jdheyburn"/>
    <language>en</language>
    <item>
      <title>Alerting on NHS Coronavirus Vaccine Updates With Huginn</title>
      <dc:creator>Joseph Heyburn</dc:creator>
      <pubDate>Thu, 03 Jun 2021 00:00:00 +0000</pubDate>
      <link>https://dev.to/jdheyburn/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn-cdf</link>
      <guid>https://dev.to/jdheyburn/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn-cdf</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;UPDATE 2021-06-06&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A few days after publishing this I came across a bug where agent jobs would be stuck in pending state. I've since fixed this and documented some additional changes I've made at the end of the post.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The UK’s coronavirus vaccine strategy has been to target those most vulnerable first, and then trickle down towards the healthier population. Since that age is creeping down toward my age group, I wanted to see if I could alert myself when I would be eligible for the vax.&lt;/p&gt;

&lt;p&gt;My local GP would send out an SMS text message informing me when I’m eligible, however I’ve heard that this text can come days after you’re eligible. Knowing that the latest guidance is maintained on the &lt;a href="https://www.nhs.uk/conditions/coronavirus-covid-19/coronavirus-vaccination/coronavirus-vaccine/"&gt;NHS Coronavirus Vaccine site&lt;/a&gt;, I can use &lt;a href="https://github.com/huginn/huginn#what-is-huginn"&gt;Huginn&lt;/a&gt; to alert me when the page updates with the latest eligibility.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/vaccine-eligibility.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A2KkPH-8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/vaccine-eligibility.png" alt="A list of bullet points of who is eligible to receive the vaccine, includes people aged 30 years and older, vulnerable people, etc"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  tl;dr
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Huginn is an automation tool with a number of different agents&lt;/li&gt;
&lt;li&gt;It can be configured to monitor a property (or properties) on a web page and trigger an action&lt;/li&gt;
&lt;li&gt;That action can take the form of an email alert&lt;/li&gt;
&lt;li&gt;This can be used to monitor the latest age group eligible for a vaccine&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Deploying Huggin
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/huginn/huginn"&gt;Huginn&lt;/a&gt; is a self-hosted automation kit that allows you to create agents and workflows in response to events, sort of like your own &lt;a href="https://ifttt.com/"&gt;IFTTT&lt;/a&gt; (If This Then That).&lt;/p&gt;

&lt;p&gt;Being self-hosted it can be deployed out in a number of ways. I already have &lt;a href="https://www.portainer.io/"&gt;Portainer&lt;/a&gt; (a GUI for &lt;a href="https://www.docker.com/"&gt;Docker&lt;/a&gt;) running in a virtual machine - so to deploy it out I can follow the instructions for &lt;a href="https://github.com/huginn/huginn/blob/master/doc/docker/install.md"&gt;Docker container deployment&lt;/a&gt;. I created a &lt;a href="https://docs.docker.com/compose/"&gt;docker-compose&lt;/a&gt; file so that it can be easily replicated for yourselves in Docker, or even as a &lt;a href="https://documentation.portainer.io/v2.0/stacks/create/"&gt;Portainer Stack&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You’ll notice there are some environment variables for SMTP here; the values for these will differ for your SMTP setup. I talk more about how I set this up with my Gmail account later in the post.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Also big thanks to zblesk for their &lt;a href="https://zblesk.net/blog/running-huginn-with-docker/"&gt;blog post on Huginn&lt;/a&gt; which helped me iron out some of the environment variables!&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3"&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;huginn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/scripts/init&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;huginn&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SMTP_PORT=587&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SMTP_SERVER=&amp;lt;SMTP_SERVER&amp;gt;&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SMTP_PASSWORD=&amp;lt;SMTP_PASSWORD&amp;gt;&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SMTP_USER_NAME=&amp;lt;SMTP_USER_NAME&amp;gt;&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SMTP_ENABLE_STARTTLS_AUTO=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SMTP_AUTHENTICATION=plain&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SMTP_DOMAIN=&amp;lt;SMTP_DOMAIN&amp;gt;&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DATABASE_POOL=30&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TIMEZONE=London&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;IMPORT_DEFAULT_SCENARIO_FOR_ALL_USERS=false&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;huginn/huginn:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;3000:3000/tcp&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1000"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mysql:/var/lib/mysql&lt;/span&gt;
    &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mysql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be sure to change &lt;code&gt;TIMEZONE&lt;/code&gt; to your timezone, I found that having an incorrectly set timezone caused Huginn jobs to be backed up in a pending state. I tried &lt;code&gt;Europe/London&lt;/code&gt; first but that caused the process to crash on boot; so I ultimately got it working with just &lt;code&gt;London&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The config above will expose Huginn on the Docker host at port 3000. I like to give my services a nice domain name to access them at using Caddy, which I’ve &lt;a href="https://dev.to/jdheyburn/reverse-proxy-multiple-domains-using-caddy-2-3497"&gt;written about before&lt;/a&gt;. Here’s a condensed version of what my Caddy config file looks like for Huginn.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"apps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"http"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"servers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"srv0"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"listen"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;":443"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"routes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"handle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                  &lt;/span&gt;&lt;span class="nl"&gt;"handler"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"subroute"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                  &lt;/span&gt;&lt;span class="nl"&gt;"routes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                      &lt;/span&gt;&lt;span class="nl"&gt;"handle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                          &lt;/span&gt;&lt;span class="nl"&gt;"handler"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reverse_proxy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                          &lt;/span&gt;&lt;span class="nl"&gt;"upstreams"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                              &lt;/span&gt;&lt;span class="nl"&gt;"dial"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"192.168.2.15:3000"&lt;/span&gt;&lt;span class="w"&gt;
                            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
                          &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
                        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
                      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
                  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"match"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                  &lt;/span&gt;&lt;span class="nl"&gt;"host"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="s2"&gt;"huginn.joannet.casa"&lt;/span&gt;&lt;span class="w"&gt;
                  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"terminal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="c1"&gt;// ... others removed for brevity&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"tls_connection_policies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"match"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"sni"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                  &lt;/span&gt;&lt;span class="s2"&gt;"huginn.joannet.casa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                  &lt;/span&gt;&lt;span class="c1"&gt;// ... others removed for brevity&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"automation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"policies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"challenges"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"dns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                  &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"api_token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CLOUDFLARE_API_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cloudflare"&lt;/span&gt;&lt;span class="w"&gt;
                  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"acme"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"subjects"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="s2"&gt;"huginn.joannet.casa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="c1"&gt;// ... others removed for brevity&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I’ll need to also add in a DNS entry in PiHole to route HTTP calls on my network for &lt;code&gt;https://huginn.joannet.casa&lt;/code&gt; to the IP address of my Caddy server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/huginn-deployed.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fWzO5GfV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/huginn-deployed.png" alt="Huginn login page at the domain name for it specified earlier"&gt; &lt;/a&gt;Deployed and ready for set up!&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up agents
&lt;/h2&gt;

&lt;p&gt;The workflow we need to set up here is pretty simple whereby we only need two agents; a Website Agent and an Email Agent. The website agent will perform the scraping of the NHS website on a regular basis, and if the component on the web page has changed, then it will invoke it’s downstream notifier - the Email Agent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Website Agent ------ invokes ------&amp;gt; Email Agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before we do that we’ll need to create an account for it - this is all local to your deployment and is not external. The invitation code you’ll need to enter is &lt;code&gt;try-huginn&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/huginn-account-setup.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KfArU5hh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/huginn-account-setup.png" alt="Huginn account setup page with the form filled in with email address and password. The invitation code is populated with try-huginn"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you get through to the main page, I suggest disabling or removing the agents that are there already to prevent some problems later on. If you set &lt;code&gt;IMPORT_DEFAULT_SCENARIO_FOR_ALL_USERS=false&lt;/code&gt; then you should not see any there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Website agent
&lt;/h3&gt;

&lt;p&gt;From the Huginn home page, create a new agent, where the type will be Website Agent.&lt;/p&gt;

&lt;p&gt;There will be a bunch of fields that appear, the only ones you need to fill in are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name

&lt;ul&gt;
&lt;li&gt;e.g. &lt;code&gt;NHSScrape&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Schedule

&lt;ul&gt;
&lt;li&gt;for me, checking once an hour is good enough&lt;/li&gt;
&lt;/ul&gt;


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

&lt;p&gt;The other fields serve a purpose that’s beyond the scope of this post.&lt;/p&gt;

&lt;p&gt;Within Options comes the configuration used to define the website agent. The documentation for the agent config appears on the right hand side, so you can read through that for reference.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/website-agent-form-populated.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--cvUAMS9G--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/website-agent-form-populated.png" alt="The completed form to create a new website agent will the fields populated as specified previously"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In order for us to determine how to configure this we first need to decide what part of the web page we want to be alerted on in event of a update. Refer to the screenshot below of the &lt;a href="https://www.nhs.uk/conditions/coronavirus-covid-19/coronavirus-vaccination/coronavirus-vaccine/"&gt;NHS Coronavirus Vaccine page&lt;/a&gt; and you’ll see a number of bullet points listed out of the vaccine criteria. The text we want to be alerted on is “people aged 30 and over”.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/vaccine-eligibility.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A2KkPH-8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/vaccine-eligibility.png" alt="A list of bullet points of who is eligible to receive the vaccine, includes people aged 30 years and older, vulnerable people, etc"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The website agent supports an &lt;a href="https://www.w3schools.com/xml/xpath_syntax.asp"&gt;xpath syntax&lt;/a&gt; as a config option, which is an expression syntax used to retrieve objects from XML documents - including HTML. But how do we find out what the xpath for this text field is?&lt;/p&gt;

&lt;p&gt;Enter &lt;a href="https://selectorgadget.com/"&gt;SelectorGadget&lt;/a&gt;. It’s a &lt;a href="https://en.wikipedia.org/wiki/Bookmarklet"&gt;bookmarklet&lt;/a&gt; tool that helps with just that - I recommend taking a look at the &lt;a href="https://vimeo.com/52055686"&gt;short tutorial&lt;/a&gt; on how to use it.&lt;/p&gt;

&lt;p&gt;Since I know I only want to select the first bullet point in this particular list, I start off by first clicking that property which turns that node green.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/selector-gadget-1.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--I81Ao5B1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/selector-gadget-1.png" alt="Selector gadget highlighted in green the bullet point we want to monitor on, the remaining bullet points are highlighted yellow. There are 76 items in scope for this current selection"&gt; &lt;/a&gt;&lt;br&gt;
Clicking on the property I want to target highlights it green&lt;/p&gt;

&lt;p&gt;You can see the xpath for this is a &lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt; node, which returns 76 results on the page we’re scraping. These are represented by the boxes that are highlighted yellow.&lt;/p&gt;

&lt;p&gt;What I want to do is whittle it down by filtering out all the other &lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt; nodes I’m not interested in, so that I only have 1 result left. As I’ve already made my first selection, any subsequent clicks will now filter those out. So now it’s just a case of playing whack-a-mole until all yellow highlighted fields are gone.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/selector-gadget-2.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4AAkraKd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/selector-gadget-2.png" alt="Selector gadget highlighted in green the bullet point we want to monitor on, the second bullet point we want to ignore is highlighted red - indicating we do not want it in scope. There are 17 items in scope for this current selection"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clicking the second bullet point indicated that I don’t care about any other &lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt; elements in this range.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/selector-gadget-3.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--WtFPWgDy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/selector-gadget-3.png" alt="Scrolling to the top of the page there is another element that is highlighted yellow, the 'Home' button"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt; properties also make up headings at the top of the page - so these appear highlighted in yellow too.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/selector-gadget-4.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Jvi2WsxA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/selector-gadget-4.png" alt="Clicking the home element filters it out and now is highlighted red - there are 13 items in scope for this current selection"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clicking the Home element filters that out.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/selector-gadget-5.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--OPMAYqol--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/selector-gadget-5.png" alt="Scrolling further down the page we see another bullet point in a separate list that is highlighted yellow"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Scrolling down further on the page, there’s another &lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt; range which needs to be filtered.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/selector-gadget-6.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--M9hhuOcD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/selector-gadget-6.png" alt="Filtering out the bullet point highlights it red and now there is only 1 item in scope for this selection"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Boom - filtering that means we only have 1 element being targeted.&lt;/p&gt;

&lt;p&gt;We now only have 1 selection, so now we can click the XPath button in the tool to retrieve the config we need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//ul[(((count(preceding-sibling::*) + 1) = 6) and parent::*)]//li[(((count(preceding-sibling::*) + 1) = 1) and parent::*)]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/selector-gadget-7.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--wvNMj7IJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/selector-gadget-7.png" alt="Clicking on the XPath button in SelectorGadget shows the final xpath syntax that we need"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;N.B. since writing this it looks like the xpath changed - I’ve updated the config below with the same. I retrieved it using the same method as described above.&lt;/p&gt;

&lt;p&gt;As a future task it’d be great to see if we could be alerted on when the working status of a job fails - but it seems that &lt;a href="https://github.com/huginn/huginn/issues/1333"&gt;feature is missing&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Going back to the agent config, this xpath syntax then goes into the xpath key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expected_update_period_in_days"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.nhs.uk/conditions/coronavirus-covid-19/coronavirus-vaccination/coronavirus-vaccine/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"on_change"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extract"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"xpath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"//ul[(((count(preceding-sibling::*) + 1) = 4) and parent::*)]//li[(((count(preceding-sibling::*) + 1) = 1) and parent::*)]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"normalize-space(.)"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once it’s configured then you can click on &lt;strong&gt;Dry Run&lt;/strong&gt; at the bottom to see the text it extracts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/website-agent-dry-run.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--B5lJUzYS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/website-agent-dry-run.png" alt="Huginn correctly extracts the text for 'people aged 30 and over' from the NHS website"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Email agent
&lt;/h3&gt;

&lt;p&gt;Now that the website agent is set up we need to set up our alert destination; this takes the form of an email agent. At minimum, we only need to configure these fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name

&lt;ul&gt;
&lt;li&gt;e.g. NHSEmail&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Sources

&lt;ul&gt;
&lt;li&gt;select the website agent you created beforehand&lt;/li&gt;
&lt;/ul&gt;


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

&lt;p&gt;Note that an agent such as email can have multiple sources - so if you wanted to be alerted on multiple web pages then you only need one email agent.&lt;/p&gt;

&lt;p&gt;The options field is not as complex as the website agent - mine is configured with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NHS Coronavirus Page update"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"headline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Vaccine age updated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expected_receive_period_in_days"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/email-agent-form-populated.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vPOqpmCd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/email-agent-form-populated.png" alt="The completed form to create a new email agent will the fields populated as specified previously"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The real complexity lies in configuring Huginn to send email…&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure Huginn for sending email over Gmail SMTP
&lt;/h2&gt;

&lt;p&gt;I have a Gmail account which opens up SMTP access to allows applications to send email programatically, where Huginn has support for this. You’ll notice earlier when we deployed Huginn that I had a number of environment variables configured for SMTP. It took a bit of trial-and-error and searching through GitHub issues to get it right, but that config works for me.&lt;/p&gt;

&lt;p&gt;Since my Gmail account is set up with 2FA, I cannot use my actual Gmail password in the SMTP_PASSWORD field. Instead what I have to do is set up an application-specific password that only Huginn is configured for. This restriction known in Google as &lt;a href="https://support.google.com/accounts/answer/6010255"&gt;less secure app access&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/google-less-secure-app-access.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--wcC7E2wf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/google-less-secure-app-access.png" alt="Google accounts less secure app access page - this is disabled because 2-step verification is set up on my account"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As mentioned, we’ll need to set up an app password. For this I navigated to the &lt;a href="https://myaccount.google.com/apppasswords"&gt;app password page&lt;/a&gt; for my account and selected &lt;em&gt;Other (Custom name)&lt;/em&gt; from the app dropdown, to then enter the name of the app. The name doesn’t matter here - it’s for your reference.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/google-app-password-setup.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jxQImALw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/google-app-password-setup.png" alt="Google accounts app password creation page - an entry for Huginn is being created"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you click on &lt;strong&gt;Generate&lt;/strong&gt; it will display the password assigned. It is this value that goes into the SMTP_PASSWORD env var for Huginn to pick up.&lt;/p&gt;

&lt;p&gt;As a recap, the environment variables that I needed to set in order to get emails to be sent were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SMTP_DOMAIN=gmail.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SMTP_SERVER=smtp.gmail.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SMTP_PORT=587&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SMTP_AUTHENTICATION=plain&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SMTP_USER_NAME=$EMAIL_ADDRESS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SMTP_PASSWORD=$APP_PASSWORD&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SMTP_ENABLE_STARTTLS_AUTO=true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DATABASE_POOL=30&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;While not directly related to SMTP, it helps ensure there are enough threads to process database requests&lt;/li&gt;
&lt;/ul&gt;


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

&lt;p&gt;Note if you did not start up Huginn with these environment variables already set, then you’ll need to restart the service so that it can pick them up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the flow
&lt;/h2&gt;

&lt;p&gt;We can test the whole flow by making a modification to the website agent we created. Currently the mode we have it set to is &lt;code&gt;on_change&lt;/code&gt; which is the desired end mode, and will only trigger its receivers if there has been a change in the property being selected. If we change the mode to be &lt;code&gt;all&lt;/code&gt; then it will always invoke the receivers.&lt;/p&gt;

&lt;p&gt;Couple this with setting a frequent schedule (i.e. every 5m) then we should be receiving an email with the current value every 5 minutes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/example-email.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--anTHLkJK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/alerting-on-nhs-coronavirus-vaccine-updates-with-huginn/example-email.png" alt="An example email sent out by the agent - it contains the text extracted from the NHS page; 'people aged 30 and over'"&gt; &lt;/a&gt;Once the flow is tested, we can set the mode on the website agent back to &lt;code&gt;on_change&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Now we just play the waiting time until getting vaxx’ed up as Marc Rebillet says… 💉&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/qeCwwYjf8gw"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing agent jobs stuck in pending state
&lt;/h2&gt;

&lt;p&gt;Using the docker compose file above caused an issue for me where Huginn would get jobs stuck in a pending state - where rebooting the container was the only way to unblock them... no good for an alerting app!&lt;/p&gt;

&lt;p&gt;Huginn by default includes a mysql daemon as the datastore if none is provided in the environment variables. I decided to have mysql running in a separate container to see if that fixed it... and it did!&lt;/p&gt;

&lt;p&gt;My new docker compose file looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;huginn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/scripts/init&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;huginn_huginn&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SMTP_PORT=587&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SMTP_SERVER=&amp;lt;SMTP_SERVER&amp;gt;&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SMTP_PASSWORD=&amp;lt;SMTP_PASSWORD&amp;gt;&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SMTP_USER_NAME=&amp;lt;SMTP_USER_NAME&amp;gt;&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SMTP_ENABLE_STARTTLS_AUTO=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SMTP_AUTHENTICATION=plain&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SMTP_DOMAIN=&amp;lt;SMTP_DOMAIN&amp;gt;&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TIMEZONE=London&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DATABASE_POOL=30&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DATABASE_NAME=huginn&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DATABASE_USERNAME=huginn&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DATABASE_PASSWORD=&amp;lt;MYSQL_PASSWORD&amp;gt;&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DATABASE_HOST=huginn_mysql&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DATABASE_PORT=3306&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;START_MYSQL=false&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DATABASE_ENCODING=utf8mb4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;IMPORT_DEFAULT_SCENARIO_FOR_ALL_USERS=false&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DOMAIN=huginn.joannet.casa&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;INVITATION_CODE=&amp;lt;INVITATION_CODE&amp;gt;&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;huginn/huginn:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;3000:3000/tcp&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1000"&lt;/span&gt;
    &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mysql&lt;/span&gt;
  &lt;span class="na"&gt;mysql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;huginn_mysql&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3306:3306"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MYSQL_ROOT_PASSWORD=&amp;lt;MYSQL_ROOT_PASSWORD&amp;gt;&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MYSQL_DATABASE=huginn&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MYSQL_USER=huginn&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MYSQL_PASSWORD=&amp;lt;MYSQL_PASSWORD&amp;gt;&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mysql:/var/lib/mysql&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mysql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;mysql&lt;/code&gt; container is pretty standard so I won't cover that here. There are some env vars I had to add to the &lt;code&gt;huginn&lt;/code&gt; container:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DATABASE_HOST&lt;/code&gt; - the database hostname to connect to, we can use the container name here&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DATABASE_PORT&lt;/code&gt; - the port to which to connect to the database&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DATABASE_NAME&lt;/code&gt; - the name of the database to use&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DATABASE_USERNAME&lt;/code&gt; - who we should connect to the database as&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DATABASE_PASSWORD&lt;/code&gt; - authentication for the user&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DATABASE_ENCODING&lt;/code&gt; - a requirement when using a newer version of MySQL as defined in the &lt;a href="https://github.com/huginn/huginn/blob/master/.env.example#L33"&gt;documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;START_MYSQL&lt;/code&gt; - whether to use a local mysql daemon or not&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some additional env vars I added unrelated to the new database:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;IMPORT_DEFAULT_SCENARIO_FOR_ALL_USERS&lt;/code&gt; - I don't care able the agents that are added by default&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;INVITATION_CODE&lt;/code&gt; - Lock down Huginn by requiring this code for new user sign ups&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DOMAIN&lt;/code&gt; - the endpoint that Huginn is available at&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I've also added in a &lt;code&gt;depends_on&lt;/code&gt; on the database container to assist with orchestration. On first boot however it takes some time for mysql to initialise the database, so Huginn may fail as the database is not yet ready to be connected to. Once the initialisation is done then reboot the Huginn container and it should be able to bootstrap the database fine.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>coronavirus</category>
      <category>huginn</category>
    </item>
    <item>
      <title>Backup to Backblaze B2 using restic and rclone</title>
      <dc:creator>Joseph Heyburn</dc:creator>
      <pubDate>Tue, 04 May 2021 00:00:00 +0000</pubDate>
      <link>https://dev.to/jdheyburn/backup-to-backblaze-b2-using-restic-and-rclone-7ld</link>
      <guid>https://dev.to/jdheyburn/backup-to-backblaze-b2-using-restic-and-rclone-7ld</guid>
      <description>&lt;p&gt;Over the last UK lockdown I spent some time making sure I had backups of my music collection after I had organised them using &lt;a href="https://beets.readthedocs.io/en/stable/"&gt;beets&lt;/a&gt;. I used this as a good opportunity to ensure I had other backups in place for some other critical files at home too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backup tools
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://restic.net/"&gt;Restic&lt;/a&gt; is a backup snapshot tool that given a set of directories, it will split the data into chunks and de-duplicate these chunks to a specified backup repository. You can specify your backup policy to a repository, where it will manage what daily, weekly, or monthly snapshots to keep.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://rclone.org/"&gt;Rclone&lt;/a&gt; is a cloud file transfer tool that allows you to synchronise, copy, etc., any data across a huge library of cloud backends.&lt;/p&gt;

&lt;p&gt;Both of these are open source tools that are free to use. Getting set up on them is beyond the scope of this post.&lt;/p&gt;

&lt;p&gt;Backups should be automated without us having to think of them (at least until you need to restore!), so for this I’ll be using systemd unit services with timers.&lt;/p&gt;

&lt;p&gt;Restic is storing the snapshots in repositories on my local NFS server. I’m then using &lt;a href="https://www.backblaze.com/b2/cloud-storage.html"&gt;Backblaze B2&lt;/a&gt; to store these in the cloud. At the time of writing they charge $0.005 GB/month for storage, and $0.01 per GB downloaded. So it costs more to download from B2, but since this is a disaster recovery backup I don’t anticipate downloading all that much. Being able to store it cheap over a long term is most important.&lt;/p&gt;

&lt;p&gt;There are a load of other storage services too; here’s a list below taken from &lt;a href="https://www.backblaze.com/b2/cloud-storage.html"&gt;Backblaze’s website&lt;/a&gt;. The other one I was contending with was &lt;a href="https://wasabi.com/cloud-storage-pricing/#three-info"&gt;Wasabi&lt;/a&gt;; they’re only marginally more expensive than B2 for storage ($0.0059 GB/month), but they don’t charge for downloads.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/backup-to-backblaze-b2-using-restic-and-rclone/cloud-storage-price-comparison.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--WhYxLHjQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/backup-to-backblaze-b2-using-restic-and-rclone/cloud-storage-price-comparison.png" alt="A table showing the cost comparison of B2 versus S3, Azure, and Google Cloud Platform - B2 is the cheapest."&gt; &lt;/a&gt;Cloud storage cost comparisons&lt;/p&gt;

&lt;h2&gt;
  
  
  Backup contents and policy
&lt;/h2&gt;

&lt;p&gt;I have restic set up to backup to two respositories:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;small-files

&lt;ul&gt;
&lt;li&gt;PiHole configuration&lt;/li&gt;
&lt;li&gt;UniFi backups&lt;/li&gt;
&lt;li&gt;Logitech Media Server (LMS) backups&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;media

&lt;ul&gt;
&lt;li&gt;all music files&lt;/li&gt;
&lt;li&gt;beets databases&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Reason being why these are split into two repositories is so that I can define a separate backup policy for each of them.&lt;/p&gt;

&lt;p&gt;For example, small-files contains… small files. You can see from the list above that these are just configuration files or even backups of files themselves. Since config files are important, I’d like the option to go back to a particular setting from weeks or maybe months ago. So the snapshots I want to keep for this repository is 30 daily snapshots, 5 weekly snapshots, 12 monthly snapshots, and 3 yearly snapshots. It might be a bit overkill, but I can always reduce that number and restic will remove them.&lt;/p&gt;

&lt;p&gt;Whereas for media, these files tend to be much larger which will be reflected in the repository size. Especially since music files don’t tend to change over time - all I’m looking for is a way of reverting back to a state I had recently in case I did some reorganising with beets. Based on this I only need to keep 30 daily snapshots.&lt;/p&gt;

&lt;p&gt;Also in the media repository are the beets databases. I’ll expand on how I have this set up in a future post, but for now think of it as a database containing the metadata for your music collection. It’s useful to include this with my music files so that I can reflect on the state of my collection at a particular point in time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backup procedure
&lt;/h2&gt;

&lt;p&gt;I have a 2TB USB hard drive connected to a Pi (dee) which is configured to be the NFS server for my network, along with PiHole for DNS, and a UniFi controller too. The drive holds my music collection and the restic local repositories.&lt;/p&gt;

&lt;p&gt;LMS sits on a different machine to dee so we’ll need to retrieve the files first before we perform a snapshot.&lt;/p&gt;

&lt;p&gt;Once restic has taken snapshots and stored them on the USB drive, we’ll need to upload the backups to B2 with rclone.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;N.B. the configuration to automate restic with systemd was largely inspired from a post on &lt;a href="https://fedoramagazine.org/automate-backups-with-restic-and-systemd/"&gt;Fedora Magazine&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can view the code for all these scripts and systemd unit files at my &lt;a href="https://github.com/jdheyburn/dotfiles/tree/master/restic"&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retrieving backups from remote servers
&lt;/h3&gt;

&lt;p&gt;I’ll have the restic backup script call another script, &lt;code&gt;pull-backups.sh&lt;/code&gt; to retrieve remote files that I want to back up.&lt;/p&gt;

&lt;p&gt;For now I only need to retrieve LMS backups from &lt;a href="https://www.picoreplayer.org/"&gt;PiCorePlayer&lt;/a&gt; - for which there is a handy command to help with that: &lt;code&gt;pcp bu&lt;/code&gt;. Don’t get too invested in the file path locations, these are specific to PiCorePlayer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# pull-backups.sh&lt;/span&gt;

&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class="c"&gt;# Invoke and pull backups from remote servers and place them on USB&lt;/span&gt;

&lt;span class="k"&gt;function &lt;/span&gt;pcp&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;fpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/mnt/mmcblk0p2/tce"&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;fname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"mydata"&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;ext&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"tgz"&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;now&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +&lt;span class="s2"&gt;"%Y-%m-%dT%H%M%S"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;dstPath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/mnt/usb/Backup/lms/*.&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ext&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"removing previous backups..."&lt;/span&gt;
    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; &lt;span class="nv"&gt;$dstPath&lt;/span&gt;

    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;sourceFile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;fpath&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;fname&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ext&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;dstFile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/mnt/usb/Backup/lms/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;fname&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;now&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ext&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"starting pcp backup"&lt;/span&gt;
    ssh &lt;span class="nt"&gt;-i&lt;/span&gt; /home/jdheyburn/.ssh/pcp tc@pcp.joannet.casa &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s1"&gt;'pcp bu'&lt;/span&gt;

    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"copying &lt;/span&gt;&lt;span class="nv"&gt;$sourceFile&lt;/span&gt;&lt;span class="s2"&gt; on remote to &lt;/span&gt;&lt;span class="nv"&gt;$dstFile&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    scp &lt;span class="nt"&gt;-i&lt;/span&gt; /home/jdheyburn/.ssh/pcp &lt;span class="s2"&gt;"tc@pcp.joannet.casa:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;sourceFile&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;$dstFile&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;function &lt;/span&gt;main&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"entered &lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    pcp
&lt;span class="o"&gt;}&lt;/span&gt;

main &lt;span class="nv"&gt;$@&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This takes the backup created on the remote at &lt;code&gt;/mnt/mmcblk0p2/tce/mydata.tgz&lt;/code&gt; and places it locally at &lt;code&gt;/mnt/usb/Backup/lms/mydata_DATETIME.tgz&lt;/code&gt;. This &lt;code&gt;lms&lt;/code&gt; directory can then be used as a backup path for restic.&lt;/p&gt;

&lt;p&gt;Should I need to retrieve backups from any other servers, I’ll write a new function here and append it to &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Restic backup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# restic-all.sh&lt;/span&gt;

&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class="c"&gt;# Master script for backing up anything to do with restic&lt;/span&gt;

&lt;span class="k"&gt;function &lt;/span&gt;do_restic&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$mode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"backup"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Backing up &lt;/span&gt;&lt;span class="nv"&gt;$target&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        restic backup &lt;span class="nt"&gt;--verbose&lt;/span&gt; &lt;span class="nt"&gt;--tag&lt;/span&gt; systemd.timer &lt;span class="nv"&gt;$BACKUP_EXCLUDES&lt;/span&gt; &lt;span class="nv"&gt;$BACKUP_PATHS&lt;/span&gt;
        &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Forgetting old &lt;/span&gt;&lt;span class="nv"&gt;$target&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        restic forget &lt;span class="nt"&gt;--verbose&lt;/span&gt; &lt;span class="nt"&gt;--tag&lt;/span&gt; systemd.timer &lt;span class="nt"&gt;--group-by&lt;/span&gt; &lt;span class="s2"&gt;"paths,tags"&lt;/span&gt; &lt;span class="nt"&gt;--keep-daily&lt;/span&gt; &lt;span class="nv"&gt;$RETENTION_DAYS&lt;/span&gt; &lt;span class="nt"&gt;--keep-weekly&lt;/span&gt; &lt;span class="nv"&gt;$RETENTION_WEEKS&lt;/span&gt; &lt;span class="nt"&gt;--keep-monthly&lt;/span&gt; &lt;span class="nv"&gt;$RETENTION_MONTHS&lt;/span&gt; &lt;span class="nt"&gt;--keep-yearly&lt;/span&gt; &lt;span class="nv"&gt;$RETENTION_YEARS&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$mode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"prune"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"pruning &lt;/span&gt;&lt;span class="nv"&gt;$target&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        restic &lt;span class="nt"&gt;--verbose&lt;/span&gt; prune
    &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;function &lt;/span&gt;small_files&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;BACKUP_PATHS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/var/lib/unifi/backup/autobackup /etc/pihole /mnt/usb/Backup/lms"&lt;/span&gt;
    &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;BACKUP_EXCLUDES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
    &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RETENTION_DAYS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"7"&lt;/span&gt;
    &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RETENTION_WEEKS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"5"&lt;/span&gt;
    &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RETENTION_MONTHS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"12"&lt;/span&gt;
    &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RETENTION_YEARS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"3"&lt;/span&gt;
    &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RESTIC_REPOSITORY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/mnt/usb/Backup/restic/small-files"&lt;/span&gt;
    &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RESTIC_PASSWORD_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/home/restic/.resticpw"&lt;/span&gt;

    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;

    do_restic &lt;span class="nv"&gt;$mode&lt;/span&gt; &lt;span class="s2"&gt;"small files"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;function &lt;/span&gt;media&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;BACKUP_PATHS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/mnt/usb/Backup/media/beets-db /mnt/usb/Backup/media/lossless /mnt/usb/Backup/media/music /mnt/usb/Backup/media/vinyl"&lt;/span&gt;
    &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;BACKUP_EXCLUDES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
    &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RETENTION_DAYS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"30"&lt;/span&gt;
    &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RETENTION_WEEKS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"0"&lt;/span&gt;
    &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RETENTION_MONTHS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"0"&lt;/span&gt;
    &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RETENTION_YEARS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"0"&lt;/span&gt;
    &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RESTIC_REPOSITORY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/mnt/usb/Backup/restic/media"&lt;/span&gt;
    &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RESTIC_PASSWORD_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/home/restic/.resticmediapw"&lt;/span&gt;

    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;

    do_restic &lt;span class="nv"&gt;$mode&lt;/span&gt; &lt;span class="s2"&gt;"media"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;function &lt;/span&gt;main&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nv"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$mode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"backup"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
        /home/jdheyburn/dotfiles/restic/pull-backups.sh
    &lt;span class="k"&gt;fi

    &lt;/span&gt;small_files &lt;span class="nv"&gt;$mode&lt;/span&gt;
    media &lt;span class="nv"&gt;$mode&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

main &lt;span class="nv"&gt;$@&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script is a bit messy, but it gets the job done. It takes in an argument which can be either &lt;code&gt;backup&lt;/code&gt; or &lt;code&gt;prune&lt;/code&gt;, depending on what task needs to run. If the mode is &lt;code&gt;backup&lt;/code&gt; then it will invoke the &lt;code&gt;pull-backups.sh&lt;/code&gt; prior to snapshotting so that it has the latest files to backup. I cover the &lt;code&gt;prune&lt;/code&gt; function later.&lt;/p&gt;

&lt;p&gt;Both restic repositories will be kept under &lt;code&gt;/mnt/usb/Backup/restic&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Once the script is defined we can have systemd invoke it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/restic-all.service&lt;/span&gt;

&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="nt"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;Restic backup everything service
&lt;span class="nt"&gt;OnFailure&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;unit-status-mail@%n.service

&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="nt"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;oneshot
&lt;span class="nt"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;/home/jdheyburn/dotfiles/restic/restic-all.sh backup

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="nt"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;multi-user.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can see that we’re passing in the mode as an argument at &lt;code&gt;ExecStart&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We can perform a test to make sure everything is defined correctly by running &lt;code&gt;systemctl start restic-all.service&lt;/code&gt;, and viewing the logs back at &lt;code&gt;journalctl -u restic-all.service -f&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In order to then have this systemd unit invoked on a regular occurrence, we need to define a &lt;code&gt;timer&lt;/code&gt; for this unit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/restic-all.timer&lt;/span&gt;

&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="nt"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;Backup with restic daily

&lt;span class="k"&gt;[Timer]&lt;/span&gt;
&lt;span class="nt"&gt;OnCalendar&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;*-*-* 2:00:00
&lt;span class="nt"&gt;Persistent&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;true

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="nt"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;timers.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will invoke the service at 2am every day once we enable it with &lt;code&gt;systemctl enable restic-all.timer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We can have a look at the resulting snapshots with the below command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;restic &lt;span class="nt"&gt;-r&lt;/span&gt; /mnt/usb/Backup/restic/small-files/ snapshots
&lt;span class="go"&gt;repository 79dbc9b6 opened successfully, password is correct
found 1 old cache directories in /root/.cache/restic, run `restic cache --cleanup` to remove them
ID Time Host Tags Paths
-----------------------------------------------------------------------------------------------
d93573f4 2020-06-30 02:00:01 dee systemd.timer /etc/pihole
                                                          /var/lib/unifi/backup/autobackup

96ecf97e 2020-07-31 02:00:35 dee systemd.timer /etc/pihole
                                                          /var/lib/unifi/backup/autobackup

d2b0a78e 2020-08-31 02:00:21 dee systemd.timer /etc/pihole
                                                          /var/lib/unifi/backup/autobackup

&lt;/span&gt;&lt;span class="gp"&gt;... #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;removed &lt;span class="k"&gt;for &lt;/span&gt;brevity
&lt;span class="go"&gt;
ec6fd77e 2021-05-03 02:00:34 dee systemd.timer /etc/pihole
                                                          /mnt/usb/Backup/lms
                                                          /var/lib/unifi/backup/autobackup

722fcbbf 2021-05-04 02:00:34 dee systemd.timer /etc/pihole
                                                          /mnt/usb/Backup/lms
                                                          /var/lib/unifi/backup/autobackup
-----------------------------------------------------------------------------------------------
39 snapshots
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Syncing to B2 with rclone
&lt;/h2&gt;

&lt;p&gt;Restic has now created snapshots and stored them in their respective repositories on the USB drive - but this isn’t really a safe place to keep backups since the USB drive could crap out at any moment. We can use rclone to offload the repositories to B2.&lt;/p&gt;

&lt;p&gt;For this I’m going to use the same pattern as before for restic; a shell script invoked by systemd.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# rclone-all.sh&lt;/span&gt;

&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class="c"&gt;# Master script for backing up all rclone stuff to various clouds&lt;/span&gt;

&lt;span class="nv"&gt;RCLONE_CONFIG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/home/jdheyburn/.config/rclone/rclone.conf

&lt;span class="k"&gt;function &lt;/span&gt;main&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"rcloning beets-db -&amp;gt; gdrive:media/beets-db"&lt;/span&gt;
    rclone &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nb"&gt;sync&lt;/span&gt; /mnt/usb/Backup/media/beets-db gdrive:media/beets-db &lt;span class="nt"&gt;--config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RCLONE_CONFIG&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"rcloning music -&amp;gt; gdrive:media/music"&lt;/span&gt;
    rclone &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nb"&gt;sync&lt;/span&gt; /mnt/usb/Backup/media/music gdrive:media/music &lt;span class="nt"&gt;--config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RCLONE_CONFIG&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"rcloning lossless -&amp;gt; gdrive:media/lossless"&lt;/span&gt;
    rclone &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nb"&gt;sync&lt;/span&gt; /mnt/usb/Backup/media/lossless gdrive:media/lossless &lt;span class="nt"&gt;--config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RCLONE_CONFIG&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"rcloning vinyl -&amp;gt; gdrive:media/vinyl"&lt;/span&gt;
    rclone &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nb"&gt;sync&lt;/span&gt; /mnt/usb/Backup/media/vinyl gdrive:media/vinyl &lt;span class="nt"&gt;--config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RCLONE_CONFIG&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"rcloning restic -&amp;gt; b2:restic"&lt;/span&gt;
    rclone &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nb"&gt;sync&lt;/span&gt; /mnt/usb/Backup/restic b2:iifu8Noi-backups/restic/ &lt;span class="nt"&gt;--config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RCLONE_CONFIG&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Done rcloning"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

main &lt;span class="nv"&gt;$@&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In addition to the restic repository directory, I’m also backing up the beets databases and music files. These are going to my Google Drive storage in case I want to hook it up to some other apps that can pull from there.&lt;/p&gt;

&lt;p&gt;Like &lt;code&gt;restic-all.sh&lt;/code&gt; above, this script is also invoked by systemd.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/rclone-all.service&lt;/span&gt;

&lt;span class="c"&gt;# This should be invoked after restic has done doing the daily backup&lt;/span&gt;

&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="nt"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;Rclone backup everything service
&lt;span class="nt"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;restic-all.service
&lt;span class="nt"&gt;OnFailure&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;unit-status-mail@%n.service

&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="nt"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;oneshot
&lt;span class="nt"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;/home/jdheyburn/dotfiles/restic/rclone-all.sh

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="nt"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;restic-all.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can test it with &lt;code&gt;systemctl start rclone-all.service&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Since we want to have rclone invoked after restic has done its thing, we need to specify &lt;code&gt;After=restic-all.service&lt;/code&gt; and &lt;code&gt;WantedBy=restic-all.service&lt;/code&gt;. Note that this’ll run even if &lt;code&gt;restic-all.service&lt;/code&gt; failed - but that’s not really an issue for me since rclone is uploading more than just restic respositories.&lt;/p&gt;

&lt;p&gt;We don’t need to specify a systemd &lt;code&gt;.timer&lt;/code&gt; file for this unit file since we’re using the completion of &lt;code&gt;restic-all.service&lt;/code&gt; as our invocation point, so we can start that service in order to test it’ll run afterward.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl start restic-all.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Handling service failures
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;N.B. big thanks to &lt;a href="https://northernlightlabs.se/"&gt;Laeffe&lt;/a&gt; for their &lt;a href="https://northernlightlabs.se/2014-07-05/systemd-status-mail-on-unit-failure.html"&gt;excellent guide&lt;/a&gt; on this.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Automated backups are very useful for setting and forgetting, but when something goes wrong and backups haven’t occurred, we need to be made aware of it.&lt;/p&gt;

&lt;p&gt;We can configure each of the systemd files to invoke a script on failure which can then send us an email when this happens.&lt;/p&gt;

&lt;p&gt;You’ll notice that each of the systemd unit files have &lt;code&gt;OnFailure=unit-status-mail@%n.service&lt;/code&gt; defined in them. This is an additional unit file where if the scripts fail, this service will be invoked.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/unit-status-mail@.service&lt;/span&gt;

&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="nt"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;Unit Status Mailer Service
&lt;span class="nt"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;network.target

&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="nt"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;simple
&lt;span class="nt"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;/home/jdheyburn/dotfiles/restic/systemd/unit-status-mail.sh %I "Hostname: %H" "Machine ID: %m" "Boot ID: %b"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;@&lt;/code&gt; symbol in the filename is a special case within systemd that &lt;a href="https://superuser.com/a/393429"&gt;enables special functionality&lt;/a&gt;. In our case when we invoked it at &lt;code&gt;OnFailure=unit-status-mail@%n.service&lt;/code&gt; the calling unit file will pass its name via &lt;code&gt;%n&lt;/code&gt; to allow the receiving unit file to access it at &lt;code&gt;%I&lt;/code&gt;. So when the &lt;code&gt;restic-all&lt;/code&gt; service fails, this value ends up in &lt;code&gt;unit-status-mail@.service&lt;/code&gt; at &lt;code&gt;%I&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This &lt;code&gt;%I&lt;/code&gt; variable is then being passed as an argument to the script &lt;code&gt;unit-status-mail.sh&lt;/code&gt;. The contents of the script are below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# unit-status-mail.sh&lt;/span&gt;

&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c"&gt;# From https://northernlightlabs.se/2014-07-05/systemd-status-mail-on-unit-failure.html&lt;/span&gt;

&lt;span class="nv"&gt;MAILTO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ADD_EMAIL_HERE"&lt;/span&gt;
&lt;span class="nv"&gt;MAILFROM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"unit-status-mailer"&lt;/span&gt;
&lt;span class="nv"&gt;UNIT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;

&lt;span class="nv"&gt;EXTRA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;e &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;:2&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;EXTRA+&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;done

&lt;/span&gt;&lt;span class="nv"&gt;UNITSTATUS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;systemctl status &lt;span class="nv"&gt;$UNIT&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

sendmail &lt;span class="nv"&gt;$MAILTO&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
From:&lt;/span&gt;&lt;span class="nv"&gt;$MAILFROM&lt;/span&gt;&lt;span class="sh"&gt;
To:&lt;/span&gt;&lt;span class="nv"&gt;$MAILTO&lt;/span&gt;&lt;span class="sh"&gt;
Subject:Status mail for unit: &lt;/span&gt;&lt;span class="nv"&gt;$UNIT&lt;/span&gt;&lt;span class="sh"&gt;

Status report for unit: &lt;/span&gt;&lt;span class="nv"&gt;$UNIT&lt;/span&gt;&lt;span class="sh"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$EXTRA&lt;/span&gt;&lt;span class="sh"&gt;

&lt;/span&gt;&lt;span class="nv"&gt;$UNITSTATUS&lt;/span&gt;&lt;span class="sh"&gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"Status mail sent to: &lt;/span&gt;&lt;span class="nv"&gt;$MAILTO&lt;/span&gt;&lt;span class="s2"&gt; for unit: &lt;/span&gt;&lt;span class="nv"&gt;$UNIT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The name of the calling unit file is passed to &lt;code&gt;UNIT&lt;/code&gt; where the status of it is received, and the output is sent in an email to &lt;code&gt;MAILTO&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You’ll need to make sure you have &lt;code&gt;sendmail&lt;/code&gt; installed on the machine to do this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;sendmail &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For me, the emails landed in the spam folder - but once you configure a rule on your email client to always forward them to your inbox, you’ll always be notified if there’s an error.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/backup-to-backblaze-b2-using-restic-and-rclone/status-email.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NK8LiHtc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/backup-to-backblaze-b2-using-restic-and-rclone/status-email.png" alt="A screenshot showing an example email highlighting there has been a failure in the restic-all service."&gt; &lt;/a&gt;The resulting email&lt;/p&gt;

&lt;h2&gt;
  
  
  Removing aged restic snapshots
&lt;/h2&gt;

&lt;p&gt;One last area to look at with restic is &lt;a href="https://restic.readthedocs.io/en/latest/060_forget.html"&gt;pruning&lt;/a&gt;, this is where restic will remove old data that has been “forgotten”.&lt;/p&gt;

&lt;p&gt;Back in &lt;code&gt;restic-all.sh&lt;/code&gt; we are calling &lt;code&gt;restic forget&lt;/code&gt; after each backup - this tells restic to clean up any old snapshots not required by our defined backup policy. The script has been written to accommodate for both &lt;code&gt;backup&lt;/code&gt; and &lt;code&gt;prune&lt;/code&gt; functionality, so in order to execute that portion of the script we need to set up another systemd unit file to invoke it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/restic-prune.service&lt;/span&gt;

&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="nt"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;Restic backup service (data pruning)
&lt;span class="nt"&gt;OnFailure&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;unit-status-mail@%n.service

&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="nt"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;oneshot
&lt;span class="nt"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;/home/jdheyburn/dotfiles/restic/restic-all.sh prune
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since it is resource intensive to perform &lt;code&gt;prune&lt;/code&gt; against your repository, its best to run this at a different interval to your backups. From the below unit file &lt;code&gt;OnCalendar=*-*-1 10:00:00&lt;/code&gt; corresponds to the first day of the month at 10am.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="nt"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;Prune data from the restic repository monthly

&lt;span class="k"&gt;[Timer]&lt;/span&gt;
&lt;span class="nt"&gt;OnCalendar&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;*-*-1 10:00:00
&lt;span class="nt"&gt;Persistent&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;true

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="nt"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;timers.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Restoring a backup
&lt;/h2&gt;

&lt;p&gt;Now the most important part - how to restore a restic snapshot. Let’s start with how to restore from a local repository.&lt;/p&gt;

&lt;h3&gt;
  
  
  Restore from local restic repository
&lt;/h3&gt;

&lt;p&gt;Firstly we need to find the snapshot ID that we want to restore to - in this example I want the latest snapshot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;restic &lt;span class="nt"&gt;-r&lt;/span&gt; /mnt/usb/Backup/restic/small-files/ snapshots &lt;span class="nt"&gt;--last&lt;/span&gt;
&lt;span class="go"&gt;repository 79dbc9b6 opened successfully, password is correct
found 1 old cache directories in /root/.cache/restic, run `restic cache --cleanup` to remove them
ID Time Host Tags Paths
-----------------------------------------------------------------------------------------------
f7ef9c33 2021-04-17 02:00:51 dee systemd.timer /etc/pihole
                                                          /mnt/usb/Backup/media/beets-db
                                                          /var/lib/unifi/backup/autobackup

e2a73c00 2021-04-21 02:00:21 dee systemd.timer /etc/pihole
                                                          /var/lib/unifi/backup/autobackup

722fcbbf 2021-05-04 02:00:34 dee systemd.timer /etc/pihole
                                                          /mnt/usb/Backup/lms
                                                          /var/lib/unifi/backup/autobackup
-----------------------------------------------------------------------------------------------
3 snapshots
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the ID is &lt;code&gt;722fcbbf&lt;/code&gt;, let’s browse the contents to see if the file we want is in there.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;restic &lt;span class="nt"&gt;-r&lt;/span&gt; /mnt/usb/Backup/restic/small-files/ &lt;span class="nb"&gt;ls &lt;/span&gt;722fcbbf
&lt;span class="go"&gt;repository 79dbc9b6 opened successfully, password is correct
found 1 old cache directories in /root/.cache/restic, run `restic cache --cleanup` to remove them
snapshot 722fcbbf of [/var/lib/unifi/backup/autobackup /etc/pihole /mnt/usb/Backup/lms] filtered by [] at 2021-05-04 02:00:34.383403601 +0100 BST):
/etc
/etc/pihole
/etc/pihole/GitHubVersions
/etc/pihole/adlists.list
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;... removed &lt;span class="k"&gt;for &lt;/span&gt;brevity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s say we want to restore &lt;code&gt;/etc/pihole/adlists.list&lt;/code&gt;, we can use the &lt;code&gt;--include&lt;/code&gt; argument to specify just that. If we didn’t use a filter argument then restic would default to restoring the entire contents of the snapshot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;restic &lt;span class="nt"&gt;-r&lt;/span&gt; /mnt/usb/Backup/restic/small-files/ restore 722fcbbf &lt;span class="nt"&gt;--target&lt;/span&gt; /tmp/restic-restore &lt;span class="nt"&gt;--include&lt;/span&gt; /etc/pihole/adlists.list
&lt;span class="go"&gt;repository 79dbc9b6 opened successfully, password is correct
found 1 old cache directories in /root/.cache/restic, run `restic cache --cleanup` to remove them
&lt;/span&gt;&lt;span class="gp"&gt;restoring &amp;lt;Snapshot 722fcbbf of [/var/lib/unifi/backup/autobackup /etc/pihole /mnt/usb/Backup/lms] at 2021-05-04 02:00:34.383403601 +0100 BST by root@dee&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;to /tmp/restic-restore
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tree /tmp/restic-restore/
&lt;span class="go"&gt;/tmp/restic-restore/
└── etc
    └── pihole
        └── adlists.list
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Restore from B2 restic repository
&lt;/h3&gt;

&lt;p&gt;We’re storing the repositories on B2 too, and restic has B2 integration built into it. So in the scenario where the NFS server had died and we needed to restore a snapshot stored on B2, we can hit it directly without having to use rclone to pull down the entire repository for us to restore from. This’ll be a cheaper approach as restic is only pulling down the files it needs to restore from, lowering B2 download costs.&lt;/p&gt;

&lt;p&gt;We just need to configure some variables to permit restic to hit B2. If you’re using rclone you can use the same ID and key here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;B2_ACCOUNT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ACCCOUNT_ID"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;B2_ACCOUNT_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ACCCOUNT_KEY"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RESTIC_PASSWORD_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/path/to/passwordfile
restic -r b2:BUCKET_NAME:restic/small-files snapshots --last
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From here you can then use the same commands as in the local repository to traverse the snapshots and restore.&lt;/p&gt;

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

&lt;p&gt;The process above is probably more complex than what it needs to be; having a separate process for backing up and restoring. Since I want to back up to the NFS locally first followed by B2, this approach made the most sense as it allows me to minimise download costs from B2 through targeting the local repository first.&lt;/p&gt;

&lt;p&gt;What’s most important is that backups are being made, and that I &lt;em&gt;can&lt;/em&gt; restore from them.&lt;/p&gt;

</description>
      <category>restic</category>
      <category>rclone</category>
      <category>backup</category>
    </item>
    <item>
      <title>How to automate zero downtime maintenance with AWS SSM &amp; ALBs</title>
      <dc:creator>Joseph Heyburn</dc:creator>
      <pubDate>Tue, 19 Jan 2021 00:00:00 +0000</pubDate>
      <link>https://dev.to/jdheyburn/how-to-automate-zero-downtime-maintenance-with-aws-ssm-albs-4kn0</link>
      <guid>https://dev.to/jdheyburn/how-to-automate-zero-downtime-maintenance-with-aws-ssm-albs-4kn0</guid>
      <description>&lt;p&gt;Welcome to the last post in this &lt;a href="https://dev.to/jdheyburn/series/9477"&gt;series&lt;/a&gt; where we've been exploring SSM Documents, so far we've covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How &lt;a href="https://dev.to/jdheyburn/automate-instance-hygiene-with-aws-ssm-command-documents-c10"&gt;Command Documents&lt;/a&gt; can help to execute commands on EC2 Instances&lt;/li&gt;
&lt;li&gt;Automating these Command Documents through &lt;a href="https://dev.to/jdheyburn/automate-instance-hygiene-with-aws-ssm-maintenance-windows-5ck7"&gt;Maintenance Windows&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Safely chaining Command Documents through &lt;a href="https://dev.to/jdheyburn/automate-instance-hygiene-with-aws-ssm-automation-documents-4g6f"&gt;Automation Documents&lt;/a&gt;, and aborting for any failures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post will now look into how we can use Automation Documents to perform maintenance on EC2 instances without impacting user experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  tl;dr
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;With the introduction of load balancers to front your services, you can control which instances should be receiving traffic&lt;/li&gt;
&lt;li&gt;This enables you to proactively remove instances from rotation so that you can perform maintenance on the backends to minimalise user disruption&lt;/li&gt;
&lt;li&gt;SSM automation documents can enable us to execute pre-maintenance steps such as removing an instance from a load balancer, as well as adding them back after

&lt;ul&gt;
&lt;li&gt;See the &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-3/documents/graceful_patch_instance.yml"&gt;document&lt;/a&gt; produced in this post highlighting this, and where I explain how it works, and how to deploy it using Terraform&lt;/li&gt;
&lt;/ul&gt;


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

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;If you're just joining in from this post then I recommend reading through the previous posts to gain an understanding of how we got here; or if you know what you're looking for the tl;dr provides a summary.&lt;/p&gt;

&lt;p&gt;As always, the code for this post can be found on &lt;a href="https://github.com/jdheyburn/terraform-examples/tree/main/aws-ssm-automation-3"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A basic understanding and knowledge of &lt;a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html"&gt;Application Load Balancers&lt;/a&gt; (ALB), and its components (e.g. &lt;a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html"&gt;target groups&lt;/a&gt;) is required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing load balancers
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Load_balancing_(computing)"&gt;Load balancers&lt;/a&gt; are a key component in software architecture that distribute traffic and requests across backend services in a variety of algorithms, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Round-robin

&lt;ul&gt;
&lt;li&gt;every backend serves the same number of requests&lt;/li&gt;
&lt;li&gt;the most commonly used algorithm&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Weighted round-robin

&lt;ul&gt;
&lt;li&gt;backends receive a fixed percentage of incoming requests&lt;/li&gt;
&lt;li&gt;useful if some backends are more beefy than others&lt;/li&gt;
&lt;li&gt;also used in &lt;a href="https://martinfowler.com/bliki/CanaryRelease.html"&gt;canary deployments&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Least outstanding requests

&lt;ul&gt;
&lt;li&gt;the backend which is currently processing the least number of requests is forwarded the request&lt;/li&gt;
&lt;/ul&gt;


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

&lt;p&gt;There are multiple benefits to having a load balancer sit in front of your services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Protecting your infrastructure; user requests are proxied via the load balancer&lt;/li&gt;
&lt;li&gt;Distribute traffic and requests however you like&lt;/li&gt;
&lt;li&gt;Perform healthchecks on backends and don't forward traffic to unhealthy nodes&lt;/li&gt;
&lt;li&gt;Drain and remove backends to permit for rolling upgrades&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While there are several different software-based load balancers out there such as &lt;a href="https://www.nginx.com/"&gt;nginx&lt;/a&gt; and &lt;a href="https://www.haproxy.org/"&gt;HAProxy&lt;/a&gt;, AWS has its own managed load balancer service known as an &lt;a href="https://aws.amazon.com/elasticloadbalancing/"&gt;Elastic Load Balancer&lt;/a&gt; (ELB).&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding web services to our demo environment
&lt;/h3&gt;

&lt;p&gt;In order for us to get the benefit of load balancers to front the EC2 instances in the architecture this series left off from &lt;a href="https://dev.to/jdheyburn/automate-instance-hygiene-with-aws-ssm-automation-documents-4g6f#prerequisites"&gt;last time&lt;/a&gt;, we will need to have a service running on our instances.&lt;/p&gt;

&lt;p&gt;Let's simulate a real web service by running a simple Hello World application across each of the instances. We can utilise EC2s &lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html"&gt;user data&lt;/a&gt; to start a basic service up for us by giving it a script to run on instance provision.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-3/alb-arch.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hvN_I9QK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-3/alb-arch.png" alt="An architecture diagram showing a user with an arrow pointing to an application load balancer on port 80. The load balancer then points to 3 EC2 instances on port 8080."&gt; &lt;/a&gt;This is the architecture we'll be building out in this section, with an ALB fronting our EC2 instances&lt;/p&gt;

&lt;p&gt;Let's use Go to create a web service for us since it is easy to get set up quickly - we'll have the server return a simple &lt;code&gt;Hello, World!&lt;/code&gt; message when a request hits it. Let's also have it return the name of the instance that was hit - this will be used later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;' &amp;gt; /home/ec2-user/main.go
package main

import (
        "fmt"
        "net/http"
        "os"
)

func main() {
        http.HandleFunc("/", HelloServer)
        http.ListenAndServe(":8080", nil)
}

func HelloServer(w http.ResponseWriter, r *http.Request) {
        hostname, _ := os.Hostname()
        fmt.Fprintf(w, "Hello, World! From %v&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;", hostname)
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;yum &lt;span class="nb"&gt;install &lt;/span&gt;golang &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt;crontab &lt;span class="nt"&gt;-l&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"@reboot nohup go run /home/ec2-user/main.go"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; | crontab -
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GOCACHE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/go-cache
&lt;span class="nb"&gt;nohup &lt;/span&gt;go run /home/ec2-user/main.go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So on instance creation this will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new file called &lt;code&gt;main.go&lt;/code&gt; and populate it with the lines between the &lt;code&gt;EOF&lt;/code&gt; delimiters&lt;/li&gt;
&lt;li&gt;Install Go&lt;/li&gt;
&lt;li&gt;Create a &lt;a href="https://en.wikipedia.org/wiki/Cron"&gt;crontab&lt;/a&gt; entry to run the service on subsequent boots&lt;/li&gt;
&lt;li&gt;Run the Go application in the background immediately&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;While user data is for useful provisioning services, I don't advise storing the source code of your application in there like I've done - this is just a hacky way to get something up and running.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We've been using Terraform to provision our nodes. So we need to save this script in a &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-3/scripts/hello_world_user_data.sh"&gt;file&lt;/a&gt; (&lt;code&gt;scripts/hello_world_user_data.sh&lt;/code&gt;), and then pass it into the &lt;code&gt;user_data&lt;/code&gt; attribute of our &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-3/ec2.tf#L27"&gt;EC2 module&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"hello_world_ec2"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-aws-modules/ec2-instance/aws"&lt;/span&gt;
  &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 2.0"&lt;/span&gt;

  &lt;span class="c1"&gt;# ... removed for brevity&lt;/span&gt;

  &lt;span class="nx"&gt;user_data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"scripts/hello_world_user_data.sh"&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform will recreate any EC2 nodes with a change in &lt;code&gt;user_data&lt;/code&gt; contents, so when you invoke &lt;code&gt;terraform apply&lt;/code&gt; all instances will be recreated.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;N.B. the AMI I picked up for my EC2 instances has an issue with SSM Agent. Ensure you execute the &lt;code&gt;AWS-UpdateSSMAgent&lt;/code&gt; across your instances after they have provisioned, or you can use an &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-state-about.html"&gt;SSM Association&lt;/a&gt; document to do that for you as &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-3/update_ssm_agent_association.tf"&gt;shown here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After they have all successfully deployed, you should be able to &lt;code&gt;curl&lt;/code&gt; the public IP address of each instance from your machine to verify your setup is correct. If you are getting timeouts then make sure your instances have a security group rule permitting traffic from your IP address through port 8080.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl http://54.229.209.60:8080
Hello, World! From ip-172-31-39-169.eu-west-1.compute.internal

&lt;span class="nv"&gt;$ &lt;/span&gt;curl http://3.250.160.209:8080
Hello, World! From ip-172-31-21-197.eu-west-1.compute.internal

&lt;span class="nv"&gt;$ &lt;/span&gt;curl http://34.254.238.146:8080
Hello, World! From ip-172-31-8-52.eu-west-1.compute.internal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Fronting instances with a load balancer
&lt;/h3&gt;

&lt;p&gt;Now that we have a web service hosted on our instances, let's now add a load balancer in front of it. This load balancer will now become the point of entry for our application instead of hitting the EC2 instances directly.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For this I am using a Terraform &lt;a href="https://registry.terraform.io/modules/terraform-aws-modules/alb/aws/latest"&gt;ALB module&lt;/a&gt; for provisioning all the components in the load balancer, and expanding on them is beyond the scope of this post.&lt;/p&gt;

&lt;p&gt;You can navigate to the AWS ALB &lt;a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html"&gt;documentation&lt;/a&gt; to find out about the underlying components the module creates for us.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Security groups
&lt;/h4&gt;

&lt;p&gt;Before we can provision the load balancer, we need to specify the security group (SG) and the rules that should be applied to it. You can view this on &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-3/alb.tf#L45"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Since our application is written to serve request on port 8080, we need to permit both the new &lt;code&gt;aws_security_group.hello_world_alb&lt;/code&gt; SG and the existing &lt;code&gt;aws_security_group.vm_base&lt;/code&gt; SG to communicate between each other.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"hello_world_alb"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HelloWorldALB"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;default&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_security_group_rule"&lt;/span&gt; &lt;span class="s2"&gt;"alb_egress_ec2"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;security_group_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hello_world_alb&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"egress"&lt;/span&gt;
  &lt;span class="nx"&gt;from_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;
  &lt;span class="nx"&gt;to_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;
  &lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
  &lt;span class="nx"&gt;source_security_group_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vm_base&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_security_group_rule"&lt;/span&gt; &lt;span class="s2"&gt;"ec2_ingress_alb"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;security_group_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vm_base&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ingress"&lt;/span&gt;
  &lt;span class="nx"&gt;from_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;
  &lt;span class="nx"&gt;to_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;
  &lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
  &lt;span class="nx"&gt;source_security_group_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hello_world_alb&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we need to open up the ALB to allow traffic to hit it. In our case it will just be us hitting it, but this will change depending on who the consumer of the service is. If it is to serve traffic from the Internet then &lt;code&gt;cidr_blocks&lt;/code&gt; would be &lt;code&gt;["0.0.0.0/0"]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The ALB will be hosting the traffic on insecure HTTP (port 80).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_security_group_rule"&lt;/span&gt; &lt;span class="s2"&gt;"alb_ingress_user"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;security_group_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hello_world_alb&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ingress"&lt;/span&gt;
  &lt;span class="nx"&gt;from_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
  &lt;span class="nx"&gt;to_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
  &lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_blocks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ip_address&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  ALB module
&lt;/h4&gt;

&lt;p&gt;The &lt;a href="https://registry.terraform.io/modules/terraform-aws-modules/alb/aws/latest"&gt;module documentation&lt;/a&gt; will tell us how we need to structure it. Our requirements dictate we need the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Receive traffic on port 80&lt;/li&gt;
&lt;li&gt;Forward traffic to backend targets on port 8080&lt;/li&gt;
&lt;li&gt;Include health checks to ensure we do not forward requests to unhealthy instances&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Translate these requirements into the context of the module and we have &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-3/alb.tf#L1"&gt;something like this&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"hello_world_alb"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-aws-modules/alb/aws"&lt;/span&gt;
  &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 5.0"&lt;/span&gt;

  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HelloWorldALB"&lt;/span&gt;

  &lt;span class="nx"&gt;load_balancer_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"application"&lt;/span&gt;

  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;default&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;subnets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tolist&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_subnet_ids&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;security_groups&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hello_world_alb&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;target_groups&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;name_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"pref-"&lt;/span&gt;
      &lt;span class="nx"&gt;backend_protocol&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HTTP"&lt;/span&gt;
      &lt;span class="nx"&gt;backend_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;
      &lt;span class="nx"&gt;target_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"instance"&lt;/span&gt;
      &lt;span class="nx"&gt;health_check&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="nx"&gt;healthy_threshold&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
        &lt;span class="nx"&gt;unhealthy_threshold&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
        &lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;http_tcp_listeners&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
      &lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HTTP"&lt;/span&gt;
      &lt;span class="nx"&gt;target_group_index&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;In order to keep this post simple I am not fronting services over HTTPS (secure HTTP) - I would strongly advise against doing this for non-test scenarios.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Another step we will need is to hook up our EC2 instances with the target group created.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lb_target_group_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"hello_world_tg_att"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hello_world_ec2&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;target_group_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hello_world_alb&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target_group_arns&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;target_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hello_world_ec2&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's some clever Terraform going on here. All we're doing is looping over each of the created EC2 instances in the module and adding it to the target group, to receive traffic on port 8080.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hitting the load balancer
&lt;/h3&gt;

&lt;p&gt;Whereas earlier when we were testing the services by hitting the EC2 instances directly, we'll now be hitting the ALB instead. You can grab the ALB DNS name from the &lt;a href="https://console.aws.amazon.com/ec2/v2/home#LoadBalancers:sort=loadBalancerName"&gt;console&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl http://HelloWorldALB-128172928.eu-west-1.elb.amazonaws.com:80
Hello, World! From ip-172-31-31-223.eu-west-1.compute.internal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nice - and we can see from here what instance we've hit in the backend, since we included the hostname in the response.&lt;/p&gt;

&lt;p&gt;If we now hit the ALB one more time, we will get a different instance respond.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl http://HelloWorldALB-128172928.eu-west-1.elb.amazonaws.com:80
Hello, World! From ip-172-31-0-10.eu-west-1.compute.internal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the load balancer rotating between the backends available to it. We can see the rotation by repeatedly hitting the endpoint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;curl http://HelloWorldALB-128172928.eu-west-1.elb.amazonaws.com:80 &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;0.5&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
&lt;/span&gt;Hello, World! From ip-172-31-31-223.eu-west-1.compute.internal
Hello, World! From ip-172-31-43-11.eu-west-1.compute.internal
Hello, World! From ip-172-31-0-10.eu-west-1.compute.internal
Hello, World! From ip-172-31-31-223.eu-west-1.compute.internal
Hello, World! From ip-172-31-0-10.eu-west-1.compute.internal
Hello, World! From ip-172-31-43-11.eu-west-1.compute.internal
Hello, World! From ip-172-31-43-11.eu-west-1.compute.internal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  So what does all this have to do with our maintenance document?
&lt;/h2&gt;

&lt;p&gt;Now that we have a load balancer fronting our services, let's review executing our automation document from the &lt;a href="https://dev.to/jdheyburn/automate-instance-hygiene-with-aws-ssm-automation-documents-4g6f#combining-command-docs-into-automation"&gt;last post&lt;/a&gt; and the problem it brings.&lt;/p&gt;

&lt;p&gt;If an instance were to be rebooted during the &lt;strong&gt;AWS-RunPatchBaseline&lt;/strong&gt; stage of the automation document, then there is a chance that a request would have been forwarded to that instance before the health checks against it have failed.&lt;/p&gt;

&lt;p&gt;To simulate this, let's create a new automation document which simulates a reboot. We'll just take our existing &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-3/documents/patch_with_healthcheck_template.yml"&gt;patching document&lt;/a&gt; and replace the patch step with a reboot command, following the &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/send-commands-reboot.html"&gt;AWS guidelines&lt;/a&gt; to do this. I've also modified the health check to check to see if the new service came up okay - this may differ for your environment.&lt;/p&gt;

&lt;p&gt;You can view the document on &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-3/documents/reboot_with_healthcheck_template.yml"&gt;GitHub&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;--------&lt;/span&gt;
&lt;span class="na"&gt;schemaVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.3"&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Executes a reboot on the instance followed by a healthcheck&lt;/span&gt;
&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;InstanceIds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;StringList&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;The instance to target&lt;/span&gt;
&lt;span class="na"&gt;mainSteps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;InvokeReboot&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:runCommand&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DocumentName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS-RunShellScript&lt;/span&gt;
      &lt;span class="na"&gt;InstanceIds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;InstanceIds&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
      &lt;span class="na"&gt;OutputS3BucketName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${output_s3_bucket_name}&lt;/span&gt;
      &lt;span class="na"&gt;OutputS3KeyPrefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${output_s3_key_prefix}&lt;/span&gt;
      &lt;span class="na"&gt;Parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;flag_location=/home/ec2-user/REBOOT_STARTED&lt;/span&gt;
          &lt;span class="s"&gt;if [! -f $flag_location]; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "Creating flag file at $flag_location"&lt;/span&gt;
            &lt;span class="s"&gt;touch $flag_location&lt;/span&gt;
            &lt;span class="s"&gt;echo "Reboot initiated"&lt;/span&gt;
            &lt;span class="s"&gt;exit 194&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;
          &lt;span class="s"&gt;echo "Reboot finished, removing flag file at $flag_location"&lt;/span&gt;
          &lt;span class="s"&gt;rm $flag_location          &lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ExecuteHealthcheck&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:runCommand&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DocumentName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS-RunShellScript&lt;/span&gt;
      &lt;span class="na"&gt;InstanceIds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;InstanceIds&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
      &lt;span class="na"&gt;OutputS3BucketName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${output_s3_bucket_name}&lt;/span&gt;
      &lt;span class="na"&gt;OutputS3KeyPrefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${output_s3_key_prefix}&lt;/span&gt;
      &lt;span class="na"&gt;Parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;sleep 60&lt;/span&gt;
          &lt;span class="s"&gt;if ! curl http://localhost:8080/; then&lt;/span&gt;
            &lt;span class="s"&gt;exit 1&lt;/span&gt;
          &lt;span class="s"&gt;fi          &lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-3/ssm_reboot_with_healthcheck.tf"&gt;Terraform code&lt;/a&gt; to create this document will look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_document"&lt;/span&gt; &lt;span class="s2"&gt;"reboot_with_healthcheck"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"RebootWithHealthcheck"&lt;/span&gt;
  &lt;span class="nx"&gt;document_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Automation"&lt;/span&gt;
  &lt;span class="nx"&gt;document_format&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"YAML"&lt;/span&gt;

  &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;templatefile&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"documents/reboot_with_healthcheck_template.yml"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;output_s3_bucket_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;script_bucket&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;output_s3_key_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ssm_output/"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Testing for failure
&lt;/h3&gt;

&lt;p&gt;After this has been applied in our environment let's get our test set up. In a terminal window from your machine have this script running in the background to simulate load.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl http://HelloWorldALB-128172928.eu-west-1.elb.amazonaws.com:80 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$resp&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; html&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$resp&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oPm1&lt;/span&gt; &lt;span class="s2"&gt;"(?&amp;lt;=&amp;lt;title&amp;gt;)[^&amp;lt;]+"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Error - &lt;/span&gt;&lt;span class="nv"&gt;$error&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$resp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;0.5
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we need to invoke the new reboot document against an instance (see &lt;a href="https://dev.to/jdheyburn/automate-instance-hygiene-with-aws-ssm-automation-documents-4g6f#testing-automation-documents"&gt;here&lt;/a&gt; for how we achieved this last time). Once it is running let's monitor the output of the command in your terminal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hello, World! From ip-172-31-18-158.eu-west-1.compute.internal
Hello, World! From ip-172-31-14-19.eu-west-1.compute.internal
Hello, World! From ip-172-31-18-158.eu-west-1.compute.internal
Error - 502 Bad Gateway
Error - 502 Bad Gateway
Hello, World! From ip-172-31-14-19.eu-west-1.compute.internal
Hello, World! From ip-172-31-18-158.eu-west-1.compute.internal
Error - 502 Bad Gateway
Hello, World! From ip-172-31-14-19.eu-west-1.compute.internal
Hello, World! From ip-172-31-18-158.eu-west-1.compute.internal
Hello, World! From ip-172-31-14-19.eu-west-1.compute.internal
Error - 502 Bad Gateway
Hello, World! From ip-172-31-18-158.eu-west-1.compute.internal
Error - 502 Bad Gateway
Hello, World! From ip-172-31-14-19.eu-west-1.compute.internal
Hello, World! From ip-172-31-14-19.eu-west-1.compute.internal
Hello, World! From ip-172-31-18-158.eu-west-1.compute.internal
Hello, World! From ip-172-31-18-158.eu-west-1.compute.internal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not good! 😧&lt;/p&gt;

&lt;p&gt;These error messages are coming from the load balancer, &lt;a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-troubleshooting.html#http-502-issues"&gt;indicating&lt;/a&gt; that the underlying backend wasn't able to complete the request. This is happening when the instance gets rebooted as specified in our document.&lt;/p&gt;

&lt;p&gt;The load balancer hasn't had enough time to determine whether the instance is unhealthy or not - as dictated from our &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-3/alb.tf#L19"&gt;health check policy&lt;/a&gt; (2 failed checks with 6 seconds between them) - and so still forwards traffic to it even though it cannot respond.&lt;/p&gt;

&lt;p&gt;We can actually view this disruption in &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/working_with_metrics.html"&gt;CloudWatch Metrics&lt;/a&gt; too. ALBs expose &lt;a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-cloudwatch-metrics.html#load-balancer-metrics-alb"&gt;metrics&lt;/a&gt; for us which we can monitor against, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RequestCount

&lt;ul&gt;
&lt;li&gt;Informs us how many incoming requests the ALB is receiving&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;HTTPCode_ELB_5XX_Count

&lt;ul&gt;
&lt;li&gt;How many HTTP 5XX error codes are being returned by the ALB&lt;/li&gt;
&lt;/ul&gt;


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

&lt;p&gt;We can chart them together for visualisation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-3/non-graceful-results.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--CTXAIwZ1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-3/non-graceful-results.png" alt="A graph in CloudWatch showing the number of requests being served by the ALB, along with occasional HTTP 5XX counts - corresponding at the same time the instances were being rebooted"&gt; &lt;/a&gt;HTTPCode_ELB_5XX_Count only reports on failures - if the metric is missing data points then no errors occurred at that time&lt;/p&gt;

&lt;p&gt;While we are the only users hitting this, had this been a production box hit by 1,000s of users, each one of them would experience an issue with your application, and equals lost customers! 😡&lt;/p&gt;

&lt;p&gt;What we need is a means of removing the node from the load balancer rotation so that we can safely perform maintenance on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Removing instances from load balancer rotation
&lt;/h2&gt;

&lt;p&gt;Load balancer target groups have an API endpoint that allow you to drain connections from backends - where the load balancer stops any &lt;em&gt;new&lt;/em&gt; requests being forwarded to that backend, and allows existing requests to complete. This can be done via - you guessed it - Automation Documents!&lt;/p&gt;

&lt;h3&gt;
  
  
  Graceful load balancer document
&lt;/h3&gt;

&lt;p&gt;You can see the document in its entirety &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-3/documents/graceful_patch_instance.yml"&gt;here&lt;/a&gt;. The steps that it performs are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check that the target group is healthy

&lt;ul&gt;
&lt;li&gt;We want to ensure we're not fuelling a dumpster fire&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Check that the instance we're targeting is in the target group we're modifying

&lt;ul&gt;
&lt;li&gt;Otherwise what's the point? 🙃&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Remove the instance from the target group and wait for it to be removed&lt;/li&gt;
&lt;li&gt;Execute our maintenance document&lt;/li&gt;
&lt;li&gt;Register the instance back and wait for it to be added back&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Document metadata
&lt;/h4&gt;

&lt;p&gt;As this is an automation document, the &lt;code&gt;schemaVersion&lt;/code&gt; should be &lt;code&gt;0.3&lt;/code&gt;. We're using two parameters here to run the document:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;TargetGroupArn&lt;/code&gt; - the ARN of the target group we are making modifications too&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;InstanceId&lt;/code&gt; - the instance that is undergoing maintenance
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;schemaVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.3"&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gracefully reboot instance with healthchecks&lt;/span&gt;
&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;InstanceId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;String&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;The instance to target&lt;/span&gt;
  &lt;span class="na"&gt;TargetGroupArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;String&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;The target group ARN for the instance&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Sanity checks
&lt;/h4&gt;

&lt;p&gt;The first two steps of the document are sanity checking the target group to ensure preconditions are met before we introduce change. We're using the &lt;code&gt;aws:assertAwsResourceProperty&lt;/code&gt; &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/automation-action-assertAwsResourceProperty.html"&gt;action&lt;/a&gt; to allow us to query against the &lt;a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/Welcome.html"&gt;AWS ELBv2 API&lt;/a&gt; (Elastic Load Balancer v2) and verify a response is what we expect it to be.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can see all the API endpoints available for ELBv2 at this &lt;a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_Operations.html"&gt;location&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The response of the &lt;a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_DescribeTargetHealth.html"&gt;DescribeTargetHealth&lt;/a&gt; endpoint returns an object that is structured like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"TargetHealthDescriptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"i-083c8ca9c9b74e1cd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"HealthCheckPort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"8080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"TargetHealth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"State"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"healthy"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"i-08a63b118a0b2a6b7"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"HealthCheckPort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"8080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"TargetHealth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"State"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"healthy"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"i-08c656b7160dd6729"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"HealthCheckPort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"8080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"TargetHealth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"State"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"healthy"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This represents all the backends that are configured against the target group. The property selector &lt;code&gt;$.TargetHealthDescriptions..TargetHealth.State&lt;/code&gt; specified in the document will check against &lt;em&gt;all&lt;/em&gt; state fields to see if they are healthy. If any of them aren't then the document will be aborted. As mentioned before, this check is performed to ensure we're not causing more problems for ourselves if any of the nodes are unhealthy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AssertTargetGroupHealthBefore&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Assert the target group is healthy before we bounce Tomcat&lt;/span&gt;
  &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:assertAwsResourceProperty&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;elbv2&lt;/span&gt;
    &lt;span class="na"&gt;Api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DescribeTargetHealth&lt;/span&gt;
    &lt;span class="na"&gt;PropertySelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$.TargetHealthDescriptions..TargetHealth.State&lt;/span&gt;
    &lt;span class="na"&gt;DesiredValues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;healthy&lt;/span&gt;
    &lt;span class="na"&gt;TargetGroupArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;TargetGroupArn&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="na"&gt;maxAttempts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the next sanity check we're ensuring that the instance is definitely in the target group we want to remove it from. This is a slightly different query to the last step where we're specifically requesting for state health on that one instance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AssertInstanceIsInTargetGroup&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Assert the instance is a healthy target of the target group&lt;/span&gt;
  &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:assertAwsResourceProperty&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;elbv2&lt;/span&gt;
    &lt;span class="na"&gt;Api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DescribeTargetHealth&lt;/span&gt;
    &lt;span class="na"&gt;PropertySelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$.TargetHealthDescriptions[0].TargetHealth.State&lt;/span&gt;
    &lt;span class="na"&gt;DesiredValues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;healthy&lt;/span&gt;
    &lt;span class="na"&gt;TargetGroupArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;TargetGroupArn&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="na"&gt;Targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;InstanceId&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="na"&gt;maxAttempts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Remove the instance from rotation
&lt;/h4&gt;

&lt;p&gt;Now our preconditions have been met we can remove the instance using the &lt;code&gt;aws:executeAwsApi&lt;/code&gt; &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/automation-action-executeAwsApi.html"&gt;action&lt;/a&gt;. This action is similar to &lt;code&gt;aws:assertAwsResourceProperty&lt;/code&gt; in that it calls an AWS API endpoint, but we're not checking the response of it - in fact the &lt;a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_DeregisterTargets.html"&gt;DeregisterTargets&lt;/a&gt; endpoint doesn't return anything for us to check against.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DeregisterInstanceFromTargetGroup&lt;/span&gt;
    &lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Proactively remove the instance from the target group&lt;/span&gt;
    &lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:executeAwsApi&lt;/span&gt;
    &lt;span class="s"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;elbv2&lt;/span&gt;
      &lt;span class="na"&gt;Api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DeregisterTargets&lt;/span&gt;
      &lt;span class="na"&gt;TargetGroupArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;TargetGroupArn&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
      &lt;span class="na"&gt;Targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;InstanceId&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once we've done that we need to verify the instance has definitely been removed. Remember that the target group allows for existing connections to complete their requests when it is draining, so deregistering the instance doesn't happen instantaneously - this is where the &lt;code&gt;aws:waitForAwsResourceProperty&lt;/code&gt; helps us.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;WaitForDeregisteredTarget&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Wait for the instance to drain connections&lt;/span&gt;
  &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:waitForAwsResourceProperty&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;elbv2&lt;/span&gt;
    &lt;span class="na"&gt;Api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DescribeTargetHealth&lt;/span&gt;
    &lt;span class="na"&gt;PropertySelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$.TargetHealthDescriptions[0].TargetHealth.State&lt;/span&gt;
    &lt;span class="na"&gt;DesiredValues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;unused&lt;/span&gt;
    &lt;span class="na"&gt;TargetGroupArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;TargetGroupArn&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="na"&gt;Targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;InstanceId&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="na"&gt;maxAttempts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;600&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AssertTargetIsDeregistered&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Assert the instance is no longer a target&lt;/span&gt;
  &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:assertAwsResourceProperty&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;elbv2&lt;/span&gt;
    &lt;span class="na"&gt;Api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DescribeTargetHealth&lt;/span&gt;
    &lt;span class="na"&gt;PropertySelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$.TargetHealthDescriptions[0].TargetHealth.State&lt;/span&gt;
    &lt;span class="na"&gt;DesiredValues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;unused&lt;/span&gt;
    &lt;span class="na"&gt;TargetGroupArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;TargetGroupArn&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="na"&gt;Targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;InstanceId&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Execute maintenance
&lt;/h4&gt;

&lt;p&gt;At this point we're 100% sure that the instance is now removed from the target group and is no longer receiving requests, so let's go ahead and use &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/automation-action-executeAutomation.html"&gt;&lt;code&gt;aws:executeAutomation&lt;/code&gt;&lt;/a&gt; to invoke the maintenance document from earlier. Remember it takes in the &lt;code&gt;InstanceIds&lt;/code&gt; as a parameter to execute on, so we'll need to pass it there too.&lt;/p&gt;

&lt;p&gt;We're specifying an &lt;code&gt;onFailure&lt;/code&gt; too, this tells the document should the step fail then move onto this step instead of the default action which is to abort the rest of the document.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;step:RegisterTarget&lt;/code&gt; is actually the next step after this one, which adds the instance back to the target group. Since it performs health checks for us and won't actually forward to an unhealthy instance, we'll let the target group make the call if this instance can receive traffic or not.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RebootWithHealthcheck&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Reboot the instance with a healthcheck afterward&lt;/span&gt;
  &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:executeAutomation&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;DocumentName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RebootWithHealthcheck&lt;/span&gt;
    &lt;span class="na"&gt;RuntimeParameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;InstanceIds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;InstanceId&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="na"&gt;maxAttempts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;onFailure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;step:RegisterTarget&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Add instance back to target group
&lt;/h4&gt;

&lt;p&gt;In a similar vein to DeregisterTargets, this &lt;a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_RegisterTargets.html"&gt;action&lt;/a&gt; will register the instance back to the target group.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RegisterTarget&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Add the instance back as a target&lt;/span&gt;
  &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:executeAwsApi&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;elbv2&lt;/span&gt;
    &lt;span class="na"&gt;Api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RegisterTargets&lt;/span&gt;
    &lt;span class="na"&gt;TargetGroupArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;TargetGroupArn&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="na"&gt;Targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;InstanceId&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Registering the instance happens instantaneously, but we will have to wait for the target group to perform initial health checks against the instance. Once we've asserted that it's healthy, then the document is complete!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;WaitForHealthyTargetGroup&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Wait for the target group to become healthy again&lt;/span&gt;
  &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:waitForAwsResourceProperty&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;elbv2&lt;/span&gt;
    &lt;span class="na"&gt;Api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DescribeTargetHealth&lt;/span&gt;
    &lt;span class="na"&gt;PropertySelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$.TargetHealthDescriptions..TargetHealth.State&lt;/span&gt;
    &lt;span class="na"&gt;DesiredValues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;healthy&lt;/span&gt;
    &lt;span class="na"&gt;TargetGroupArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;TargetGroupArn&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="na"&gt;maxAttempts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AssertTargetGroupHealthAfter&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Assert the target group is healthy after activity&lt;/span&gt;
  &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:assertAwsResourceProperty&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;elbv2&lt;/span&gt;
    &lt;span class="na"&gt;Api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DescribeTargetHealth&lt;/span&gt;
    &lt;span class="na"&gt;PropertySelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$.TargetHealthDescriptions..TargetHealth.State&lt;/span&gt;
    &lt;span class="na"&gt;DesiredValues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;healthy&lt;/span&gt;
    &lt;span class="na"&gt;TargetGroupArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;TargetGroupArn&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="na"&gt;maxAttempts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
  &lt;span class="na"&gt;isEnd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember that this document only handles one instance at a time, it will typically be up to the caller (i.e. a maintenance window) to rate limit the execution of multiple instances one at a time. We explored this in the &lt;a href="https://dev.to/jdheyburn/automate-instance-hygiene-with-aws-ssm-automation-documents-4g6f"&gt;previous post&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Terraform additions and updates
&lt;/h3&gt;

&lt;p&gt;The above document can be represented in &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-3/ssm_document_graceful_reboot.tf"&gt;Terraform&lt;/a&gt; to provision it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_document"&lt;/span&gt; &lt;span class="s2"&gt;"graceful_reboot_instance"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"RebootInstanceGraceful"&lt;/span&gt;
  &lt;span class="nx"&gt;document_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Automation"&lt;/span&gt;
  &lt;span class="nx"&gt;document_format&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"YAML"&lt;/span&gt;

  &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;templatefile&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"documents/graceful_patch_instance.yml"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;reboot_with_healthcheck_document_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ssm_document&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reboot_with_healthcheck&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll also need to update our &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-3/maintenance_window.tf#L22"&gt;maintenance window task&lt;/a&gt; to correctly reflect this new document, along with the new parameters it takes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_maintenance_window_task"&lt;/span&gt; &lt;span class="s2"&gt;"patch_with_healthcheck"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;window_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ssm_maintenance_window&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patch_with_healthcheck&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;task_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AUTOMATION"&lt;/span&gt;
  &lt;span class="nx"&gt;task_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ssm_document&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;graceful_reboot_instance&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
  &lt;span class="nx"&gt;service_role_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patch_mw_role&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;

  &lt;span class="nx"&gt;max_concurrency&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt;
  &lt;span class="nx"&gt;max_errors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0"&lt;/span&gt;

  &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"WindowTargetIds"&lt;/span&gt;
    &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_ssm_maintenance_window_target&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patch_with_healthcheck_target&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;task_invocation_parameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;automation_parameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;document_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"$LATEST"&lt;/span&gt;

      &lt;span class="nx"&gt;parameter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"InstanceId"&lt;/span&gt;
        &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"{{ TARGET_ID }}"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;parameter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"TargetGroupArn"&lt;/span&gt;
        &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hello_world_alb&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target_group_arns&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And lastly, we'll need to update the &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-3/maintenance_window_iam.tf#L30"&gt;IAM role permissions&lt;/a&gt; for &lt;code&gt;aws_iam_role.patch_mw_role.arn&lt;/code&gt; as it will be invoking more actions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"mw_role_additional"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AllowSSM"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;

    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"ssm:DescribeInstanceInformation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"ssm:ListCommandInvocations"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AllowElBRead"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;

    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"elasticloadbalancing:DescribeTargetHealth"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AllowELBWrite"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;

    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"elasticloadbalancing:DeregisterTargets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"elasticloadbalancing:RegisterTargets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hello_world_alb&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target_group_arns&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Testing the new document
&lt;/h3&gt;

&lt;p&gt;You can test the automation document by following the &lt;a href="https://dev.to/jdheyburn/automate-instance-hygiene-with-aws-ssm-automation-documents-4g6f#testing-automation-documents"&gt;same process as before&lt;/a&gt;, else you can test the whole stack via changing the &lt;a href="https://dev.to/jdheyburn/automate-instance-hygiene-with-aws-ssm-automation-documents-4g6f#testing-automation-documents-in-maintenance-windows"&gt;execution time of the maintenance window&lt;/a&gt;. I'll be following along with the latter.&lt;/p&gt;

&lt;p&gt;While the document is running you can re-use the same command to hit the ALB endpoint from earlier to see how traffic is distributed amongst the instances. You'll first see that it will only execute on one instance at a time, which was the enhancement we introduced in the &lt;a href="https://dev.to/jdheyburn/automate-instance-hygiene-with-aws-ssm-automation-documents-4g6f"&gt;last post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-3/graceful-document-overview.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l20ZaBd9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-3/graceful-document-overview.png" alt="AWS console showing the automation document execution view. There are 3 task invocations, one for each instance in scope - they have all executed successfully and only one invocation was executed at a time."&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When we drill down into each invocation we can see the automation steps doing their magic.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-3/graceful-document-invocation-detail.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ml5tG2LD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-3/graceful-document-invocation-detail.png" alt="AWS console showing the individual steps of the automation document removing the targeted instance from rotation before executing the maintenance automation document"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can have a look back at the ALB metrics again to see if we received any errors.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-3/graceful-results.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zUISHVJv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-3/graceful-results.png" alt="CloudWatch metrics view for the ALB RequestCount and HTTP_ELB_5XX_Count. The former hovers at approximately 100 requests per minute, whereas there are no error counts being reported."&gt; &lt;/a&gt;No error - no problem!&lt;/p&gt;

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

&lt;p&gt;Each post prior to this one in the series has been leading up to where we are now - a means of achieving automated zero downtime maintenance for anything that sits behind an AWS ALB.&lt;/p&gt;

&lt;p&gt;SSM is very much a bit of a beast and I hope that this series has helped clear the fog and given yourselves an idea of what you can do with SSM to automate a variety of tasks in your AWS estate.&lt;/p&gt;

&lt;p&gt;Happy automating! 💪 🙌&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>automation</category>
    </item>
    <item>
      <title>Hugo Minify RSS Code Indentation Fix</title>
      <dc:creator>Joseph Heyburn</dc:creator>
      <pubDate>Mon, 14 Dec 2020 00:00:00 +0000</pubDate>
      <link>https://dev.to/jdheyburn/hugo-minify-rss-code-indentation-fix-3dga</link>
      <guid>https://dev.to/jdheyburn/hugo-minify-rss-code-indentation-fix-3dga</guid>
      <description>&lt;p&gt;It's certainly been a while since the &lt;a href="https://dev.to/jdheyburn/three-steps-to-improve-hugo-s-rss-feeds-58ob"&gt;previous post&lt;/a&gt; in this series, which has become the home of any updates I make to my &lt;a href="https://gohugo.io/"&gt;Hugo&lt;/a&gt; website.&lt;/p&gt;

&lt;p&gt;This post is a quick one but it's something I've been meaning to fix for a while. The RSS feeds that are generated will also include code blocks as defined through &lt;a href="https://gohugo.io/content-management/syntax-highlighting/#highlighting-in-code-fences"&gt;code fences&lt;/a&gt; or through &lt;code&gt;{{&amp;lt; highlight &amp;gt;}}&lt;/code&gt; shortcodes in your post content.&lt;/p&gt;

&lt;p&gt;However for some reason the code blocks generated in my RSS feeds were losing their indentation. Take this code snippet example from my &lt;a href="https://dev.to/jdheyburn/automate-instance-hygiene-with-aws-ssm-automation-documents-4g6f"&gt;latest post&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_document"&lt;/span&gt; &lt;span class="s2"&gt;"patch_with_healthcheck"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PatchWithHealthcheck"&lt;/span&gt;
&lt;span class="nx"&gt;document_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Automation"&lt;/span&gt;
&lt;span class="nx"&gt;document_format&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"YAML"&lt;/span&gt;
&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;templatefile&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;
&lt;span class="s2"&gt;"documents/patch_with_healthcheck_template.yml"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;healthcheck_document_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ssm_document&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;perform_healthcheck_s3&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
&lt;span class="nx"&gt;output_s3_bucket_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;script_bucket&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
&lt;span class="nx"&gt;output_s3_key_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ssm_output/"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whereas it should be rendering as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_document"&lt;/span&gt; &lt;span class="s2"&gt;"patch_with_healthcheck"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PatchWithHealthcheck"&lt;/span&gt;
  &lt;span class="nx"&gt;document_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Automation"&lt;/span&gt;
  &lt;span class="nx"&gt;document_format&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"YAML"&lt;/span&gt;

  &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;templatefile&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"documents/patch_with_healthcheck_template.yml"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;healthcheck_document_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ssm_document&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;perform_healthcheck_s3&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;output_s3_bucket_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;script_bucket&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;output_s3_key_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ssm_output/"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is happening during the &lt;a href="https://github.com/jdheyburn/jdheyburn.co.uk/blob/master/.github/workflows/deploy.yml#L31"&gt;GitHub Action&lt;/a&gt; that builds the website - I am using the &lt;code&gt;--minify&lt;/code&gt; parameter which follows &lt;a href="https://gohugo.io/getting-started/configuration/#configure-minify"&gt;this configuration&lt;/a&gt; by default, and the highlighted line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[minify]&lt;/span&gt;
  &lt;span class="py"&gt;disableCSS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="py"&gt;disableHTML&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="py"&gt;disableJS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="py"&gt;disableJSON&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="py"&gt;disableSVG&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="py"&gt;disableXML&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="py"&gt;minifyOutput&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nn"&gt;[minify.tdewolff]&lt;/span&gt;
    &lt;span class="nn"&gt;[minify.tdewolff.css]&lt;/span&gt;
      &lt;span class="py"&gt;decimals&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;-1&lt;/span&gt;
      &lt;span class="py"&gt;keepCSS2&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nn"&gt;[minify.tdewolff.html]&lt;/span&gt;
      &lt;span class="py"&gt;keepConditionalComments&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="py"&gt;keepDefaultAttrVals&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="py"&gt;keepDocumentTags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="py"&gt;keepEndTags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="py"&gt;keepQuotes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="py"&gt;keepWhitespace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="nn"&gt;[minify.tdewolff.js]&lt;/span&gt;
    &lt;span class="nn"&gt;[minify.tdewolff.json]&lt;/span&gt;
    &lt;span class="nn"&gt;[minify.tdewolff.svg]&lt;/span&gt;
      &lt;span class="py"&gt;decimals&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;-1&lt;/span&gt;
    &lt;span class="nn"&gt;[minify.tdewolff.xml]&lt;/span&gt;
      &lt;span class="py"&gt;keepWhitespace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So what's happening is the RSS XML that Hugo generates for us is then being &lt;a href="https://en.wikipedia.org/wiki/Minification_(programming)"&gt;minified&lt;/a&gt; to remove the whitespace generated in that file. This includes the whitespace that's used to indent the code in the RSS feeds. Not. Good.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;In order to fix this we just need to copy the default minify config as mentioned above and change the last line to &lt;code&gt;keepWhitespace = true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can view my &lt;a href="https://github.com/jdheyburn/jdheyburn.co.uk/commit/e56aaf581283eb7a7a4d97ca7a30553beda09271"&gt;git commit&lt;/a&gt; that fixes this - should you want to include the fix too.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'm using &lt;code&gt;toml&lt;/code&gt; for my config, so make sure you change it to what your config is defined in (&lt;code&gt;yaml&lt;/code&gt; / &lt;code&gt;json&lt;/code&gt;)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I tried to experiment to see if the whole config was required or not, but it appears so - if you found a way to omit some lines then let me know.&lt;/p&gt;

&lt;p&gt;That's it - hope this help you with fixing your RSS feeds too!&lt;/p&gt;

</description>
      <category>hugo</category>
      <category>rss</category>
      <category>writing</category>
    </item>
    <item>
      <title>Automate Instance Hygiene with AWS SSM: Automation Documents</title>
      <dc:creator>Joseph Heyburn</dc:creator>
      <pubDate>Fri, 04 Dec 2020 00:00:00 +0000</pubDate>
      <link>https://dev.to/jdheyburn/automate-instance-hygiene-with-aws-ssm-automation-documents-4g6f</link>
      <guid>https://dev.to/jdheyburn/automate-instance-hygiene-with-aws-ssm-automation-documents-4g6f</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/jdheyburn/automate-instance-hygiene-with-aws-ssm-maintenance-windows-5ck7"&gt;part two&lt;/a&gt; of this &lt;a href="https://dev.to/jdheyburn/series/9477"&gt;series&lt;/a&gt; we looked at how we can automate SSM command documents using SSM Maintenance Windows.&lt;/p&gt;

&lt;p&gt;This part will now explore another type of SSM Document; Automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  tl;dr
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Automation documents can allow us to combine command documents together&lt;/li&gt;
&lt;li&gt;With this, we can utilise maintenance window error thresholds to stop further invocations&lt;/li&gt;
&lt;li&gt;Dynamic invocation of command documents can also be achieved with automation documents&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;All the code for this post can be found on &lt;a href="https://github.com/jdheyburn/terraform-examples/tree/main/aws-ssm-automation-2"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You'll notice that we have changed some things around in this post, so if you've been using &lt;code&gt;terraform apply&lt;/code&gt; in other posts to deploy to your AWS environment, you will notice some destructions.&lt;/p&gt;

&lt;p&gt;Instead of having 1 Windows and 1 Linux EC2 instance, we're now using &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-2/ec2.tf"&gt;3 Linux EC2 instances&lt;/a&gt; - to emulate an application running across multiple instances for redundancy. You don't actually need anything to be running on these instances, just have them visible in the &lt;a href="https://console.aws.amazon.com/systems-manager/managed-instances"&gt;Managed Instances&lt;/a&gt; console.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note that the EC2 instances are tagged with the key &lt;code&gt;App&lt;/code&gt; and value &lt;code&gt;HelloWorld&lt;/code&gt; - we'll be using this to specify our automation document targets.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Automation Documents
&lt;/h2&gt;

&lt;p&gt;Back in &lt;a href="https://dev.to/jdheyburn/automate-instance-hygiene-with-aws-ssm-command-documents-c10"&gt;part one&lt;/a&gt; I gave a brief intro to automation documents. To save the click:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Automation document can call and orchestrate AWS API endpoints on your behalf, including executing Command documents on instances&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Essentially we can combine two command documents into one with an automation document. But why would we want to do this?&lt;/p&gt;

&lt;h3&gt;
  
  
  Introducing proactive healthchecks
&lt;/h3&gt;

&lt;p&gt;Well in the last post, we set up a maintenance window with two tasks; one for invoking &lt;strong&gt;AWS-RunPatchBaseline&lt;/strong&gt; and another for &lt;strong&gt;PerformHealthcheckS3&lt;/strong&gt; (our healthcheck SSM Document) - both of these are &lt;em&gt;command documents&lt;/em&gt;. Say if we had a policy that wanted to ensure that after &lt;strong&gt;AWS-RunPatchBaseline&lt;/strong&gt; was invoked, we would &lt;em&gt;always&lt;/em&gt; want the &lt;strong&gt;PerformHealthcheckS3&lt;/strong&gt; invoked afterward… the Automation Document would help us get there.&lt;/p&gt;

&lt;p&gt;Not only that, the way that our maintenance window is &lt;a href="https://dev.to/jdheyburn/automate-instance-hygiene-with-aws-ssm-maintenance-windows-5ck7#maintenance-window-tasks"&gt;currently structured&lt;/a&gt; is it will invoke &lt;strong&gt;AWS-RunPatchBaseline&lt;/strong&gt; across all instances in scope at the same time. Once they are all done then it will invoke &lt;strong&gt;PerformHealthcheckS3&lt;/strong&gt; across all instances at the same time. This looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Given we have 2 instances; i-111, i-222
T+0: Invoke AWS-RunPatchBaseline on i-111, i-222
T+1: AWS-RunPatchBaseline finishes: i-111, i-222
T+2: Invoke PerformHealthcheckS3 on i-111, i-222
T+3: PerformHealthcheckS3 finishes: i-111, i-222
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Say if you wanted to limit the rate of patching across these instances so that only one instance at a time was patched, and any healthcheck failures aborted the rest of patching, then simply changing &lt;code&gt;max_concurrency&lt;/code&gt; from &lt;code&gt;100%&lt;/code&gt; to &lt;code&gt;1&lt;/code&gt; for each maintenance window task &lt;em&gt;will not achieve this&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Maintenance windows complete one task across all instances in scope before moving onto the next task. If we have Task 1 for Patching and Task 2 for Healthchecking (is that even a word?), then the maintenance window is going to patch &lt;strong&gt;all&lt;/strong&gt; instances first before it performs healthchecks on the instances. There is no way to execute the tasks synchronously on one instance at a time.&lt;/p&gt;

&lt;p&gt;This means that if a bad patch were to be installed in your estate, you could have this order of events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Given we have 2 instances; i-111, i-222
T+0: Invoke AWS-RunPatchBaseline on i-111
T+1: AWS-RunPatchBaseline finishes: i-111
T+2: Invoke AWS-RunPatchBaseline on i-222
T+3: AWS-RunPatchBaseline finishes: i-222
T+4: Invoke PerformHealthcheckS3 on i-111
T+5: PerformHealthcheckS3 FAILS: i-111
T+6: Invoke PerformHealthcheckS3 on i-222
T+7: PerformHealthcheckS3 FAILS: i-222
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the healthcheck has failed for &lt;code&gt;i-222&lt;/code&gt; as well because the bad patch landed on both instances, and production now has an outage.&lt;/p&gt;

&lt;h4&gt;
  
  
  Solution
&lt;/h4&gt;

&lt;p&gt;Thankfully, Automation Documents help us avoid that - by combining the two command documents (AWS-RunPatchBaseline and PerformHealthcheckS3), we can mark this new automation document as a &lt;em&gt;solo maintenance window task&lt;/em&gt; and have it invoked one at a time on instances, and have it abort further invocations if any sub-documents failed within in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Given we have 2 instances; i-111, i-222
T+0: Invoke AWS-RunPatchBaseline on i-111
T+1: AWS-RunPatchBaseline finishes: i-111
T+2: Invoke PerformHealthcheckS3 on i-111
T+3: PerformHealthcheckS3 FAILS: i-111
T+4: Abort invoking AWS-RunPatchBaseline on i-222
T+5: Abort invoking PerformHealthcheckS3 on i-222
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how the failed healthcheck on the first instance caused the rest of task invocations to abort? Here is the order of events for a happy path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Given we have 2 instances; i-111, i-222
T+0: Invoke AWS-RunPatchBaseline on i-111
T+1: AWS-RunPatchBaseline finishes: i-111
T+2: Invoke PerformHealthcheckS3 on i-111
T+3: PerformHealthcheckS3 succeeds: i-111
T+4: Invoke AWS-RunPatchBaseline on i-222
T+5: AWS-RunPatchBaseline finishes: i-222
T+6: Invoke PerformHealthcheckS3 on i-222
T+7: PerformHealthcheckS3 succeeds: i-222
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So we know that automation documents allow us to combine command documents together - this is done using the &lt;code&gt;aws:runCommand&lt;/code&gt; action, though there are &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/automation-actions.html"&gt;many more&lt;/a&gt; actions available to you, some of which we'll explore later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combining command docs into automation
&lt;/h2&gt;

&lt;p&gt;We combine command documents as below in YAML; like command documents they can also be defined in JSON.&lt;/p&gt;

&lt;p&gt;For the first time we are defining a parameter called &lt;code&gt;InstanceIds&lt;/code&gt;, which takes in a list of instance IDs to then pass down to the command documents, as they will need to know what instances to invoke the commands on. The value assigned to this parameter is retrieved back with the notation &lt;code&gt;"{{ InstanceIds }}"&lt;/code&gt;, which you can see being passed into the inputs of the sub-documents.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;InstanceIds&lt;/code&gt; is a special parameter name that AWS recognises and so will provide an instance picker in the GUI of Execute Automation, as shown below.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/instance-id-picker.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4DShRWNK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/instance-id-picker.png" alt="A screenshot of the execute automation page with the instance ID picker being used to select instances in scope"&gt; &lt;/a&gt;The instance picker can be helpful if you only want to target a subset of instances in your estate.&lt;/p&gt;

&lt;p&gt;We're also following logging best practices by having the command output logged to S3.&lt;/p&gt;

&lt;p&gt;Other than that, there's not really a whole lot of difference between this and a command document, so far!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It's important to note the &lt;code&gt;schemaVersion&lt;/code&gt; must be &lt;code&gt;0.3&lt;/code&gt; for automation documents.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;schemaVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.3"&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Executes a patching event on the instance followed by a healthcheck&lt;/span&gt;
&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;InstanceIds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;StringList&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;The instance to target&lt;/span&gt;
&lt;span class="na"&gt;mainSteps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;InvokePatchEvent&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:runCommand&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DocumentName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS-RunPatchBaseline&lt;/span&gt;
      &lt;span class="na"&gt;InstanceIds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;InstanceIds&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
      &lt;span class="na"&gt;OutputS3BucketName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jdheyburn-scripts&lt;/span&gt;
      &lt;span class="na"&gt;OutputS3KeyPrefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ssm_output/&lt;/span&gt;
      &lt;span class="na"&gt;Parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Operation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Scan&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ExecuteHealthcheck&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:runCommand&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DocumentName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PerformHealthcheckS3&lt;/span&gt;
      &lt;span class="na"&gt;InstanceIds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;InstanceIds&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
      &lt;span class="na"&gt;OutputS3BucketName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jdheyburn-scripts&lt;/span&gt;
      &lt;span class="na"&gt;OutputS3KeyPrefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ssm_output/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Terraform automation documents
&lt;/h3&gt;

&lt;p&gt;We can deploy these to AWS using Terraform once again. Note that the &lt;code&gt;document_type&lt;/code&gt; is &lt;code&gt;Automation&lt;/code&gt; and that we're using templating to set the variables in the document, such as referencing the PerformHealthcheckS3 command document ARN.&lt;/p&gt;

&lt;p&gt;You can see the templated version of the document in &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-2/documents/patch_with_healthcheck_template.yml"&gt;GitHub&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_document"&lt;/span&gt; &lt;span class="s2"&gt;"patch_with_healthcheck"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PatchWithHealthcheck"&lt;/span&gt;
  &lt;span class="nx"&gt;document_type&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Automation"&lt;/span&gt;
  &lt;span class="nx"&gt;document_format&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"YAML"&lt;/span&gt;

  &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;templatefile&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"documents/patch_with_healthcheck_template.yml"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;healthcheck_document_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ssm_document&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;perform_healthcheck_s3&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;output_s3_bucket_name&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;script_bucket&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;output_s3_key_prefix&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ssm_output/"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;View the above resource in &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-2/ssm_combined_command.tf"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing automation documents
&lt;/h3&gt;

&lt;p&gt;Now let's go and test this by manually invoking it. This can be done by navigating to &lt;a href="https://console.aws.amazon.com/systems-manager/automation/executions"&gt;Automation&lt;/a&gt; within Systems Manager and clicking &lt;a href="https://console.aws.amazon.com/systems-manager/automation/execute"&gt;Execute Automation&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For a shortcut of invoking the commands, you can use the below command to invoke the below CLI command. Then you may skip to the results.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ssm start-automation-execution &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--document-name&lt;/span&gt; &lt;span class="s2"&gt;"PatchWithHealthcheck"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--document-version&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;DEFAULT"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target-parameter-name&lt;/span&gt; InstanceIds &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-errors&lt;/span&gt; &lt;span class="s2"&gt;"0"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-concurrency&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; eu-west-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Setup
&lt;/h4&gt;

&lt;p&gt;Navigate to the &lt;strong&gt;Owned by me&lt;/strong&gt; tab and select the name of your created document, &lt;strong&gt;PatchWithHealthcheck&lt;/strong&gt; , then click &lt;strong&gt;Next&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Because we want to test this document executing one at a time on an instance, we'll need to select the &lt;strong&gt;Rate control&lt;/strong&gt; option.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/execute-automation-1.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--giGmEuQA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/execute-automation-1.png" alt="Execute automation document page for PatchHealthcheck, Rate Control is selected"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We'll need to select what instances are our targets. The instances in scope for this document are &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-2/ec2.tf#L23"&gt;tagged&lt;/a&gt; with the key &lt;code&gt;App&lt;/code&gt; with the value &lt;code&gt;HelloWorld&lt;/code&gt;, so let's use that as our criteria. Note this is the most scalable solution for targeting instances.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/execute-automation-2.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--CtEUFxy0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/execute-automation-2.png" alt="A screenshot of the execute automation page with tag key App and tag value HelloWorld specified"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then we need to specify how the rate of invocation should be controlled. Our criteria for this is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Execute on 1 instance at a time&lt;/li&gt;
&lt;li&gt;Abort further invocations if any produce a error&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Therefore we need to set the concurrency to 1 and the error threshold to 0.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/execute-automation-3.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--r-Ib3TJi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/execute-automation-3.png" alt="A screenshot of the execute automation page with concurrency set to 1 and the error threshold set to 0"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once all done then click &lt;strong&gt;Execute&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Results
&lt;/h4&gt;

&lt;p&gt;As the execution progresses you'll notice it invokes the document on one instance at a time. As it completes you'll have a screen that looks like the below as you click onto the execution detail page. Notice that the start and end times of each instance invocation do not overlap with one another.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Step name is the same as the instance ID in this case&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/execute-automation-4.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--m64hIcXz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/execute-automation-4.png" alt="A screenshot of the successfully completed execution detail page, all executed steps across all instances are successful and did not overlap one another"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can dive into each step invocation (the blue text in the above screenshot) to view the commands that were invoked.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/execute-automation-5.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KIcNLcKO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/execute-automation-5.png" alt="A screenshot of a successful automation step, there is a clickable URL for the step execution ID"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/execute-automation-6.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SFnmVapW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/execute-automation-6.png" alt="A screenshot of a successful automation document PatchWithHealthcheck - both patching and healthcheck run command steps are successful"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At this detail we can see the individual &lt;code&gt;aws:runCommand&lt;/code&gt; actions performed on the instance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure testing
&lt;/h3&gt;

&lt;p&gt;Okay so we've confirmed the document now only invokes synchronously. Let's now test to see if further invocations are aborted when there is a failure.&lt;/p&gt;

&lt;p&gt;To simulate the failure, we can borrow a trick from the &lt;a href="https://dev.to/jdheyburn/automate-instance-hygiene-with-aws-ssm-command-documents-c10#testing-in-aws-ssm-console"&gt;first post&lt;/a&gt; in this series by flipping the healthcheck script from &lt;code&gt;&amp;gt;&lt;/code&gt; to &lt;code&gt;&amp;lt;&lt;/code&gt;. Once that change is deployed we can re-run the document using the same method as above.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/failed-automation-1.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Z0wPAhUe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/failed-automation-1.png" alt="Simulating a failure, now the rate-limited automation document fails on the first instance invocation"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can see the failure, and that the automation did not invoke on more instances! We can drill down into the invocation to see why it failed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/failed-automation-2.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--w0iLC_dF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/failed-automation-2.png" alt="The execution detail page for PatchWithHealthcheck, the patch event succeeded but the healthcheck is marked as failed"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From this page you can then continue to drill down to the run command output to determine the cause of the failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring automation tasks for maintenance windows
&lt;/h2&gt;

&lt;p&gt;Now that we've tested the automation document and we're happy to have it automated, let's get this added to a maintenance window task. We can reuse the same maintenance window as we created last time, but with some differences.&lt;/p&gt;

&lt;h3&gt;
  
  
  Maintenance window task
&lt;/h3&gt;

&lt;p&gt;Since we are targeting an automation document, there are some differences we need to account for when compared to &lt;a href="https://dev.to/jdheyburn/automate-instance-hygiene-with-aws-ssm-maintenance-windows-5ck7#maintenance-window-tasks"&gt;command tasks&lt;/a&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Specify the &lt;code&gt;task_type&lt;/code&gt; as &lt;code&gt;AUTOMATION&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Update the &lt;code&gt;task_arn&lt;/code&gt; to our new document accordingly&lt;/li&gt;
&lt;li&gt;Ensure our new combined document is only invoked on one instance at a time, so &lt;code&gt;max_concurrency&lt;/code&gt; is set to &lt;code&gt;1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Within &lt;code&gt;task_invocation_parameters&lt;/code&gt; we use &lt;code&gt;automation_parameters&lt;/code&gt; as opposed to &lt;code&gt;run_command_parameters&lt;/code&gt;.

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;document_version&lt;/code&gt; allows us to target a specific document version&lt;/li&gt;
&lt;li&gt;any parameters required by the document are defined within &lt;code&gt;parameter&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Remember that our document takes in &lt;code&gt;InstanceIds&lt;/code&gt; as a parameter? Well you'll notice that the value is set to &lt;code&gt;"{{ TARGET_ID }}"&lt;/code&gt;. This is known in AWS as a &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/mw-cli-register-tasks-parameters.html"&gt;pseudo parameter&lt;/a&gt;, whereby the instance ID returned by &lt;code&gt;WindowTargetIds&lt;/code&gt; will be passed into the automation document.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Depending on what &lt;code&gt;resource_type&lt;/code&gt; your &lt;code&gt;aws_ssm_maintenance_window_target&lt;/code&gt; is set up as will result in a different value to &lt;code&gt;{{ TARGET_ID }}&lt;/code&gt; - in our case ours is &lt;code&gt;INSTANCE&lt;/code&gt;, so this becomes the instance ID.&lt;/p&gt;

&lt;p&gt;See the &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/mw-cli-register-tasks-parameters.html#pseudo-parameters"&gt;AWS docs&lt;/a&gt; for a full breakdown.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_maintenance_window_task"&lt;/span&gt; &lt;span class="s2"&gt;"patch_with_healthcheck"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;window_id&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ssm_maintenance_window&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patch_with_healthcheck&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;task_type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AUTOMATION"&lt;/span&gt;
  &lt;span class="nx"&gt;task_arn&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ssm_document&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patch_with_healthcheck&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;priority&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
  &lt;span class="nx"&gt;service_role_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patch_mw_role&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;

  &lt;span class="nx"&gt;max_concurrency&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt;
  &lt;span class="nx"&gt;max_errors&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0"&lt;/span&gt;

  &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"WindowTargetIds"&lt;/span&gt;
    &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_ssm_maintenance_window_target&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patch_with_healthcheck_target&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;task_invocation_parameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;automation_parameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;document_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"$LATEST"&lt;/span&gt;

      &lt;span class="nx"&gt;parameter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"InstanceIds"&lt;/span&gt;
        &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"{{ TARGET_ID }}"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can view the above resource in &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-2/maintenance_window.tf#L22"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Maintenance window target
&lt;/h3&gt;

&lt;p&gt;We need to update the target to use tag lookups against the instances - this mimics how we tested our automation document earlier on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_maintenance_window_target"&lt;/span&gt; &lt;span class="s2"&gt;"patch_with_healthcheck_target"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;window_id&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ssm_maintenance_window&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patch_with_healthcheck&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PatchWithHealthcheckTargets"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"All instances that should be patched with a healthcheck after"&lt;/span&gt;
  &lt;span class="nx"&gt;resource_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"INSTANCE"&lt;/span&gt;

  &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tag:App"&lt;/span&gt;
    &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"HelloWorld"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can view the above resource in &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-2/maintenance_window.tf#L10"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Additional IAM policies
&lt;/h3&gt;

&lt;p&gt;We'll need to also attach some new permissions to the IAM role for the maintenance window, &lt;code&gt;aws_iam_role.patch_mw_role.arn&lt;/code&gt;, to allow the automation document to perform a lookup on instances by their tag as defined in the updated &lt;code&gt;aws_ssm_maintenance_window_target.patch_with_healthcheck_target&lt;/code&gt; resource.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"mw_role_additional"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AllowSSM"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;

    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"ssm:DescribeInstanceInformation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"ssm:ListCommandInvocations"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy"&lt;/span&gt; &lt;span class="s2"&gt;"mw_role_add"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"MwRoleAdd"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Additonal permissions needed for MW"&lt;/span&gt;

  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mw_role_additional&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"mw_role_add"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patch_mw_role&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;policy_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_policy&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mw_role_add&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can view the above resources in &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-2/maintenance_window_iam.tf#L30"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Simply put, we're creating an new IAM policy with anything additional that the maintenance window requires for it to operate. We'll use this policy to add any new actions if we need to in the future.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing automation documents in maintenance windows
&lt;/h3&gt;

&lt;p&gt;Once you've got the config above applied you'll need to run a test. Just like &lt;a href="https://dev.to/jdheyburn/automate-instance-hygiene-with-aws-ssm-maintenance-windows-5ck7#testing-the-barebones-maintenance-window"&gt;last time&lt;/a&gt;, you can do this by changing the maintenance window execution time to something relatively close to your current time.&lt;/p&gt;

&lt;p&gt;Once the execution is complete (and hopefully it was successful, if not then check back at the configuration), you'll see that there will be 3 task invocations, one for each instance, and that none of them overlapped one another.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/automation-maint-window-success.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--J06-BZky--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/automation-maint-window-success.png" alt="Execution overview for the maintenance window invocation - we have 3 task invocations, one for each instance and all successful, for which only one was invoked at a time"&gt; &lt;/a&gt;Notice how the start and end times don't overlap&lt;/p&gt;

&lt;p&gt;Just like for command documents, you can view the detail for each invocation made on the instance. Here we can see the two steps that make up our newly created command document.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/automation-maint-window-success-detail.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--tuD6d5md--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/automation-maint-window-success-detail.png" alt="Task invocation detail view for one instance. There are two steps, one for invoking a patch event and another for performing a healthcheck - both are successful"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But the whole point of this exercise was to proactively handle errors right? So let's introduce some by doing what we've done before and change the healthcheck script to fail.&lt;/p&gt;

&lt;p&gt;Once the new "broken" script is applied we can rerun another test of the maintenance window.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/automation-maint-window-failure.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--eCA0RZGk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/automation-maint-window-failure.png" alt="Execution overview for the maintenance window invocation - we have 3 task invocations, one for each instance, 1 failed which then aborted further task invocations on the remaining instances"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This time round we can see that there was a failure in one task invocation, which then aborted further invocations on the remaining instances! Just as before, we can do a deep dive into the invocation to determine why it had failed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/automation-maint-window-failure-detail.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4r3X3-SP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/automation-maint-window-failure-detail.png" alt="Task invocation detail view for one instance. There are two steps, one for invoking a patch event and another for performing a healthcheck - the patch was successful but the healthcheck failed"&gt; &lt;/a&gt;From here you can follow the command invoked by the step to troubleshoot the failure&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: Using automation to dynamically invoke command documents
&lt;/h2&gt;

&lt;p&gt;In this post we've looked at a common maintenance task in the form of an SSM command document called &lt;strong&gt;AWS-RunPatchBaseline&lt;/strong&gt;, and created an automation document that will always invoke our healthcheck script after this invocation.&lt;/p&gt;

&lt;p&gt;You may have more command documents that perform some form of maintenance on instances, for which you would also want the healthcheck script to execute after as well.&lt;/p&gt;

&lt;p&gt;Instead of copying and pasting these automation documents, we can create just one automation document which takes in the command document ARN as a parameter, dynamically invoke it, and then have a hardcoded step afterward for executing a healthcheck!&lt;/p&gt;

&lt;p&gt;In it's raw YAML form, we would get a document that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;schemaVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.3"&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Executes a maintenance event on the instance followed by a healthcheck&lt;/span&gt;
&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;InstanceIds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;StringList&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;The instance to target&lt;/span&gt;
  &lt;span class="na"&gt;DocumentArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;String&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;The document arn to invoke&lt;/span&gt;
  &lt;span class="na"&gt;InputParameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;StringMap&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Parameters that should be passed to the document specified in DocumentArn&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
&lt;span class="na"&gt;mainSteps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;InvokeMaintenanceEvent&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:runCommand&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DocumentName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;DocumentArn&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
      &lt;span class="na"&gt;InstanceIds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;InstanceIds&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
      &lt;span class="na"&gt;OutputS3BucketName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${output_s3_bucket_name}&lt;/span&gt;
      &lt;span class="na"&gt;OutputS3KeyPrefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${output_s3_key_prefix}&lt;/span&gt;
      &lt;span class="na"&gt;Parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;InputParameters&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ExecuteHealthcheck&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:runCommand&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DocumentName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${healthcheck_document_arn}&lt;/span&gt;
      &lt;span class="na"&gt;InstanceIds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;InstanceIds&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
      &lt;span class="na"&gt;OutputS3BucketName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${output_s3_bucket_name}&lt;/span&gt;
      &lt;span class="na"&gt;OutputS3KeyPrefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${output_s3_key_prefix}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just like what we've done for &lt;code&gt;InstanceIds&lt;/code&gt;, we're taking in the &lt;code&gt;DocumentArn&lt;/code&gt; as a parameter and providing it as the input for the &lt;code&gt;aws:runCommand&lt;/code&gt; step. Some documents will also take parameters, so we can allow the caller to specify them using &lt;code&gt;InputParameters&lt;/code&gt;, which is defined as a &lt;code&gt;StringMap&lt;/code&gt; type - allowing it to then be used in as parameters for the document we are invoking.&lt;/p&gt;

&lt;p&gt;When we create this document in the console and then execute it, we can then dynamically add in the document we want to invoke.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/maintenance-wrapper-setup.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--MjzhKMSm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/maintenance-wrapper-setup.png" alt="The setup page for the maintenance wrapper document, we're specifying in the text field the command document to invoke, which is AWS-RunPatchBaseline. We've also added in the parameters for the document to run under InputParameters"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then just like any other document we can invoke it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/maintenance-wrapper-success.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--v4RsOM-3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/maintenance-wrapper-success.png" alt="The automation execution overview page shows that two steps were invoked, InvokeMaintenanceEvent and ExecuteHealthcheck, both are successful. We can also see the input for InvokeMaintenanceEvent is specified as AWS-RunPatchBaseline, and the parameters we used previously."&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can see that the input from the previous screen was passed to the step. Let's keep going until we hit the command invocation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/maintenance-wrapper-command-detail.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YZ4UwUEf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-2/maintenance-wrapper-command-detail.png" alt="A screenshot of the command invocation page - it executed successfully and the document listed as being invoked was AWS-RunPatchBaseline"&gt; &lt;/a&gt;The document name was dynamically passed to the command execution&lt;/p&gt;

&lt;h3&gt;
  
  
  Terraforming dynamic command documents
&lt;/h3&gt;

&lt;p&gt;So now we've confirmed that documents can be dynamically invoked, let's get this Terraformed. You can view this in &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-2/ssm_maintenance_document.tf"&gt;GitHub&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_document"&lt;/span&gt; &lt;span class="s2"&gt;"maintenance_wrapper"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"MaintenanceWithHealthcheck"&lt;/span&gt;
  &lt;span class="nx"&gt;document_type&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Automation"&lt;/span&gt;
  &lt;span class="nx"&gt;document_format&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"YAML"&lt;/span&gt;

  &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;templatefile&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"documents/maintenance_wrapper_template.yml"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;healthcheck_document_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ssm_document&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;perform_healthcheck_s3&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;output_s3_bucket_name&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;script_bucket&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;output_s3_key_prefix&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ssm_output/"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not a lot has really changed in this when we compare it to our &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-2/ssm_combined_command.tf"&gt;one from earlier&lt;/a&gt;, but the difference is in the &lt;code&gt;parameters&lt;/code&gt; field for the &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-2/documents/maintenance_wrapper_template.yml"&gt;document&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And then you can include it as a maintenance window task as below; I'm reusing the same maintenance window task as before.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_maintenance_window_task"&lt;/span&gt; &lt;span class="s2"&gt;"patch_with_healthcheck"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;window_id&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ssm_maintenance_window&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patch_with_healthcheck&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;task_type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AUTOMATION"&lt;/span&gt;
  &lt;span class="nx"&gt;task_arn&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ssm_document&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maintenance_wrapper&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;priority&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
  &lt;span class="nx"&gt;service_role_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patch_mw_role&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;

  &lt;span class="nx"&gt;max_concurrency&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt;
  &lt;span class="nx"&gt;max_errors&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0"&lt;/span&gt;

  &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"WindowTargetIds"&lt;/span&gt;
    &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_ssm_maintenance_window_target&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patch_with_healthcheck_target&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;task_invocation_parameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;automation_parameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;document_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"$LATEST"&lt;/span&gt;

      &lt;span class="nx"&gt;parameter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"InstanceIds"&lt;/span&gt;
        &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"{{ TARGET_ID }}"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;parameter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DocumentArn"&lt;/span&gt;
        &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"AWS-RunPatchBaseline"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;parameter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"InputParameters"&lt;/span&gt;
        &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Operation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Scan"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can then copy and paste the same task resource, only changing the values for the &lt;code&gt;InputParameters&lt;/code&gt; and &lt;code&gt;DocumentArn&lt;/code&gt; parameters accordingly. If the document you are calling doesn't take any parameters, then you can just omit that block.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You'll have to ensure that the IAM role the maintenance window task is assuming has the correct IAM permissions as required by the document you're calling.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;What we've done in this post is taken our rudimentary command document, prone to introducing errors into our estate, and converted it to an automation document. With the right SSM maintenance window settings, you can ensure that any maintenance tasks you need to perform on your EC2 instances are done so in a manner that reduces the risk of errors in your environment.&lt;/p&gt;

&lt;p&gt;Next time, we'll be taking this a &lt;em&gt;step further&lt;/em&gt; to proactively remove EC2 instances from circulation when behind a load balancer for maintenance activities.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>automation</category>
    </item>
    <item>
      <title>Automate Instance Hygiene with AWS SSM: Maintenance Windows</title>
      <dc:creator>Joseph Heyburn</dc:creator>
      <pubDate>Mon, 16 Nov 2020 00:00:00 +0000</pubDate>
      <link>https://dev.to/jdheyburn/automate-instance-hygiene-with-aws-ssm-maintenance-windows-5ck7</link>
      <guid>https://dev.to/jdheyburn/automate-instance-hygiene-with-aws-ssm-maintenance-windows-5ck7</guid>
      <description>&lt;p&gt;&lt;a href="https://dev.to/jdheyburn/automate-instance-hygiene-with-aws-ssm-command-documents-c10"&gt;Last time&lt;/a&gt; we looked at writing our own SSM Command Document for the purpose of executing a healthcheck script on a set of EC2 instances across multiple platforms.&lt;/p&gt;

&lt;p&gt;In this post we’ll be exploring how we can automate this using maintenance windows - also within the SSM suite. This is something I’ve &lt;a href="https://dev.to/jdheyburn/using-terraform-to-manage-aws-patch-baselines-at-enterprise-scale-2nck#ssm-amp-patch-manager"&gt;covered before&lt;/a&gt;, but want to extend on that to show how its done.&lt;/p&gt;

&lt;h2&gt;
  
  
  tl;dr
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;We can use SSM Maintenance Windows to automate our newly created command documents on a schedule&lt;/li&gt;
&lt;li&gt;Multiple command documents can be combined in a maintenance window, such as a patching event followed by a healthcheck&lt;/li&gt;
&lt;li&gt;This provides us with a means of viewing historical invocations on whatever workflow we’ve automated&lt;/li&gt;
&lt;li&gt;By storing command outputs to S3, we can ensure we can recover logs that are too large to display in the console&lt;/li&gt;
&lt;li&gt;Using S3 Lifecycle Rules we can remove aged logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once again all the Terraform code for this post is available on GitHub. It is split into two parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://github.com/jdheyburn/terraform-examples/tree/main/aws-ssm-automation-1-barebones"&gt;aws-ssm-automation-1-barebones&lt;/a&gt; is for the barebones walkthrough
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/jdheyburn/terraform-examples/tree/main/aws-ssm-automation-1-logging"&gt;aws-ssm-automation-1-logging&lt;/a&gt; is for the logging enhancement
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Intro to Maintenance Windows
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-maintenance.html"&gt;Maintenance Windows&lt;/a&gt;, are a means of executing some automation workflow in your AWS estate on a schedule. Got an SSM Document you’ve written and want it automated? What about a &lt;a href="https://aws.amazon.com/lambda/"&gt;Lambda&lt;/a&gt; you want invoked at a regular schedule? Or maybe it’s a &lt;a href="https://aws.amazon.com/step-functions/"&gt;Step Function&lt;/a&gt;? Whatever the use case, Maintenance Windows are for you - just don’t be fooled by the name - they don’t necessarily have to be &lt;em&gt;just&lt;/em&gt; for maintenance!&lt;/p&gt;

&lt;h3&gt;
  
  
  Similarities to EventBridge Rules
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“But wait”&lt;/strong&gt; , &lt;em&gt;I hear you ask&lt;/em&gt;, &lt;strong&gt;“don’t CloudWatch/EventBridge Rules also allow you to invoke events on a schedule too?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Yes they do - both Maintenance Windows and &lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/what-is-amazon-eventbridge.html"&gt;EventBridge&lt;/a&gt; Rules (the bigger sibling of CloudWatch Rules) use &lt;a href="https://en.wikipedia.org/wiki/Cron#CRON_expression"&gt;cron expressions&lt;/a&gt; to define the schedule they should run on. The primary difference between the two is that Maintenance Windows allow you to &lt;strong&gt;specify the timezone&lt;/strong&gt; that the cron expression adheres to, whereas EventBridge is &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html"&gt;&lt;strong&gt;tied to UTC&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-1/eventbridge-rule-create.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KzCqDdH7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-1/eventbridge-rule-create.png" alt="The EventBridge rule creation page, there is no option to schedule the rule to a timezone"&gt; &lt;/a&gt;No timezone, no party&lt;/p&gt;

&lt;p&gt;So using maintenance windows can be handy if you’re in a non-UTC timezone and you don’t have to constantly convert your local timezone to UTC to schedule events. More importantly, maintenance windows will respect daylight savings time (DST) if your timezone observes it, so you can be sure your automation will be invoked at the same time in the specified timezone throughout the year.&lt;/p&gt;

&lt;p&gt;On the other hand, EventBridge Rules are fixed to UTC; meaning if your timezone does observe DST, then you’ll find your automation could be off by an hour for some portion of the year (unless you change it of course - but who wants to be changing automation twice a year??).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I don’t think I’ve ever met a software engineer that’s a fan of DST!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Notably as well, you can view the execution history of maintenance windows as they’ve occurred in the past, allowing you to quickly see whether a particular invocation was successful or not - and drill down into any failures.&lt;/p&gt;

&lt;p&gt;I’m not bashing EventBridge Rules, in fact, it is easier to set up than Maintenance Windows. But there’s always the right tool for the job.&lt;/p&gt;

&lt;p&gt;For the rest of this post, we’re going to be exploring how to automate the command document we created last time. Later on in the series we’ll be looking at using maintenance windows to automate automation documents.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automating command documents with maintenance windows
&lt;/h3&gt;

&lt;p&gt;So what’s the purpose of creating the command document we achieved last time? Well we’re not going to be manually invoking it like what we have been doing so far - as engineers we need to be automating as many repetitive tasks as possible.&lt;/p&gt;

&lt;p&gt;To summarise where we are now, we’ve produced a Command document which when executed, automates the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Downloads a healthcheck script from S3&lt;/li&gt;
&lt;li&gt;Executes the healthcheck script, failing the command invocation if healthcheck does not pass&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Healthchecks are important to run both continuously in our environment, as a means of monitoring and verifying the estate is working as intended, before your users notice. They are also necessary to run after a change has been introduced to the environment, such as a new code deployment, or even a patching event via the &lt;strong&gt;AWS-RunPatchBaseline&lt;/strong&gt; document.&lt;/p&gt;

&lt;h2&gt;
  
  
  Barebones maintenance window with AWS-RunPatchBaseline
&lt;/h2&gt;

&lt;p&gt;We’re going to use Terraform again to build out a minimal maintenance window. You can view the code for it &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-1-barebones/maintenance_window.tf"&gt;here&lt;/a&gt;. You’ll notice the repository this file sits in is identical to the one from the &lt;a href="https://github.com/jdheyburn/terraform-examples/tree/main/aws-ssm-automation-0"&gt;first post&lt;/a&gt;, except I’ve included the new resources that will enable us to accomplish the requirement.&lt;/p&gt;

&lt;p&gt;Here is a breakdown of each of the resources we’re going to create.&lt;/p&gt;

&lt;h3&gt;
  
  
  Maintenance Window
&lt;/h3&gt;

&lt;p&gt;This creates the maintenance window resource, which is then referred to in the subsequent resources we create.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It’s nothing more than that cron expression I mentioned earlier, along with the timezone it should execute in&lt;/li&gt;
&lt;li&gt;We specify how long the window lasts for, and the cutoff; both of which are specified in hours

&lt;ul&gt;
&lt;li&gt;The cutoff indicates how long before the end of the window should AWS not schedule any new tasks in that window
&lt;/li&gt;
&lt;/ul&gt;


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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_maintenance_window"&lt;/span&gt; &lt;span class="s2"&gt;"patch_with_healthcheck"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PatchWithHealthcheck"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Daily patch event with a healthcheck afterward"&lt;/span&gt;
  &lt;span class="nx"&gt;schedule&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cron(0 9 ? * * *)"&lt;/span&gt; &lt;span class="c1"&gt;# Everyday at 9am UK time&lt;/span&gt;
  &lt;span class="nx"&gt;schedule_timezone&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Europe/London"&lt;/span&gt;
  &lt;span class="nx"&gt;duration&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
  &lt;span class="nx"&gt;cutoff&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Maintenance Window Target
&lt;/h3&gt;

&lt;p&gt;We need a means of telling the window what instances to target, and the &lt;code&gt;aws_ssm_maintenance_window_target&lt;/code&gt; resource is how you do it. Below I’m demonstrating two methods of doing this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Specify the instance IDs directly

&lt;ul&gt;
&lt;li&gt;Handy if you have a fixed list of instances you only want to be included in the maintenance window&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Target instances by their tags

&lt;ul&gt;
&lt;li&gt;This is much for scalable, and means you don’t have to keep adding instance IDs to the list&lt;/li&gt;
&lt;li&gt;When the maintenance window executes, it will filter instances with this tag key and value combo for what to target&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;An optimum tag to use would be &lt;code&gt;Patch Group&lt;/code&gt; &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-patch-patchgroups.html"&gt;described here&lt;/a&gt; - which I have &lt;a href="https://dev.to/jdheyburn/using-terraform-to-manage-aws-patch-baselines-at-enterprise-scale-2nck#ssm-amp-patch-manager"&gt;mentioned previously&lt;/a&gt;. For the sake of this demo, we will keep it simple by targeting instance IDs.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_maintenance_window_target"&lt;/span&gt; &lt;span class="s2"&gt;"patch_with_healthcheck_target"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;window_id&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ssm_maintenance_window&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patch_with_healthcheck&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PatchWithHealthcheckTargets"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"All instances that should be patched with a healthcheck after"&lt;/span&gt;
  &lt;span class="nx"&gt;resource_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"INSTANCE"&lt;/span&gt;

  &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"InstanceIds"&lt;/span&gt;
    &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;concat&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;windows_ec2&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;linux_ec2&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
    &lt;span class="err"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# Using tags is more scalable&lt;/span&gt;
  &lt;span class="c1"&gt;#   targets {&lt;/span&gt;
  &lt;span class="c1"&gt;#     key    = "tag:Terraform"&lt;/span&gt;
  &lt;span class="c1"&gt;#     values = ["true"]&lt;/span&gt;
  &lt;span class="c1"&gt;#   }&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Maintenance Window Tasks
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Patching task
&lt;/h4&gt;

&lt;p&gt;Now for the tasks… remember that we want to execute our healthcheck SSM document after a patch event right? We need to build a task for executing the &lt;strong&gt;AWS-RunPatchBaseline&lt;/strong&gt; document.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_maintenance_window_task"&lt;/span&gt; &lt;span class="s2"&gt;"patch_task"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;window_id&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ssm_maintenance_window&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patch_with_healthcheck&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;task_type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"RUN_COMMAND"&lt;/span&gt;
  &lt;span class="nx"&gt;task_arn&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS-RunPatchBaseline"&lt;/span&gt;
  &lt;span class="nx"&gt;priority&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
  &lt;span class="nx"&gt;service_role_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patch_mw_role&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;

  &lt;span class="nx"&gt;max_concurrency&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"100%"&lt;/span&gt;
  &lt;span class="nx"&gt;max_errors&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

  &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"WindowTargetIds"&lt;/span&gt;
    &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_ssm_maintenance_window_target&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patch_with_healthcheck_target&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;task_invocation_parameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;run_command_parameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;timeout_seconds&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;

      &lt;span class="nx"&gt;parameter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Operation"&lt;/span&gt;
        &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Scan"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s run through the main attributes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;window_id&lt;/code&gt; - the maintenance window to associate this task with&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;task_type&lt;/code&gt; - what kind of task this is

&lt;ul&gt;
&lt;li&gt;since we’re executing a command document, the value here is &lt;code&gt;RUN_COMMAND&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;task_arn&lt;/code&gt; - the ARN of the document you wish to run

&lt;ul&gt;
&lt;li&gt;note the document name can also be used here, as demonstrated above&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;priority&lt;/code&gt; - defines in what order should tasks be executed in, whereby the lower the number given, the earlier the task is executed in the window

&lt;ul&gt;
&lt;li&gt;e.g. a task priority of &lt;code&gt;1&lt;/code&gt; gets executed before one with &lt;code&gt;10&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;tasks with the same priority get executed in parallel&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;service_role_arn&lt;/code&gt; - tells which IAM role should be assumed to execute this task as

&lt;ul&gt;
&lt;li&gt;we’ll get an explanation of this later&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max_concurrency&lt;/code&gt; - specifies how many instances this task should be invoked on simultaenously

&lt;ul&gt;
&lt;li&gt;it can take a percentage as a value, only applying to that percentage of target instances at a time&lt;/li&gt;
&lt;li&gt;i.e. &lt;code&gt;50%&lt;/code&gt; indicates only half of targeted instances will have the task executed at a time&lt;/li&gt;
&lt;li&gt;or it can take a fixed number such as &lt;code&gt;1&lt;/code&gt;; indicating that only one instance should have this task executed at a time&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;100%&lt;/code&gt; indicates this task will be invoked on all targets at the same time&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max_errors&lt;/code&gt; - indicates how many errors should be thrown before we abort further invocations

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;0&lt;/code&gt; indicates any error will abort the maintenance window and set its result to failed&lt;/li&gt;
&lt;/ul&gt;


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

&lt;p&gt;The &lt;code&gt;targets&lt;/code&gt; block allows us to define what instances to target this on - we’re referencing the &lt;code&gt;aws_ssm_maintenance_window_target&lt;/code&gt; resource we created previously.&lt;/p&gt;

&lt;p&gt;Lastly the &lt;code&gt;task_invocation_parameters&lt;/code&gt; allows us to customise how the document should be ran via the &lt;code&gt;parameter&lt;/code&gt; setting - which is passed to the document. For this example we’re only performing the &lt;code&gt;Scan&lt;/code&gt; operation on the document, for testing purposes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Scan&lt;/code&gt; will only check for missing patches - it won’t actually install them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A full list of available commands can be found in the AWS-RunPatchBaseline &lt;a href="https://console.aws.amazon.com/systems-manager/documents/AWS-RunPatchBaseline/content"&gt;command document&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-1/run-patch-baseline-parameters.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--734OIhNL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-1/run-patch-baseline-parameters.png" alt="Available parameters for the Run Patch Baseline document; Operation, Snapshot ID, Install Override List, and Reboot Option"&gt; &lt;/a&gt;In the console GUI we can see the available parameters for the AWS-RunPatchBaseline document&lt;/p&gt;

&lt;h4&gt;
  
  
  Healthcheck task
&lt;/h4&gt;

&lt;p&gt;Next we want to define the healthcheck task.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_maintenance_window_task"&lt;/span&gt; &lt;span class="s2"&gt;"healthcheck_task"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;window_id&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ssm_maintenance_window&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patch_with_healthcheck&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;task_type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"RUN_COMMAND"&lt;/span&gt;
  &lt;span class="nx"&gt;task_arn&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ssm_document&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;perform_healthcheck_s3&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;priority&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
  &lt;span class="nx"&gt;service_role_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patch_mw_role&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;

  &lt;span class="nx"&gt;max_concurrency&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"100%"&lt;/span&gt;
  &lt;span class="nx"&gt;max_errors&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

  &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"WindowTargetIds"&lt;/span&gt;
    &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_ssm_maintenance_window_target&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patch_with_healthcheck_target&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;task_invocation_parameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;run_command_parameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;timeout_seconds&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There’s not a whole lot of difference here compared to the patching task; our healthcheck script takes no &lt;code&gt;parameters&lt;/code&gt; so we can leave them out (although if yours does, you’ll need to add it here!), and the &lt;code&gt;task_arn&lt;/code&gt; points to the command document we created last time.&lt;/p&gt;

&lt;p&gt;Probably the most significant change though is the &lt;code&gt;priority&lt;/code&gt;. Remember that the priority number indicates the ordering of tasks to be invoked? Our patching task had a priority of &lt;code&gt;10&lt;/code&gt;, whereby our healthcheck task is &lt;code&gt;20&lt;/code&gt;. &lt;strong&gt;This means the patch task will be invoked &lt;em&gt;before&lt;/em&gt; the healthcheck one.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I could have set the priority of the patching and healthcheck tasks to &lt;code&gt;1&lt;/code&gt; and &lt;code&gt;2&lt;/code&gt; respectively to achieve the same thing.&lt;/p&gt;

&lt;p&gt;However, giving some distance between them means you can programmatically add new tasks before/after each other.&lt;/p&gt;

&lt;p&gt;Want a post-patch, pre-healthcheck task? Attach a new task with priority &lt;code&gt;15&lt;/code&gt;!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  IAM role for maintenance window
&lt;/h3&gt;

&lt;p&gt;We’ve been referencing &lt;code&gt;aws_iam_role.patch_mw_role.arn&lt;/code&gt; as our task &lt;code&gt;service_role_arn&lt;/code&gt;. You can view the code for it &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-1-barebones/maintenance_window_iam.tf"&gt;here&lt;/a&gt; - but let’s run through them quickly.&lt;/p&gt;

&lt;p&gt;All we’re doing is creating an IAM role, allowing the EC2 and SSM AWS services to assume said role, and applying the predefined AWS policy &lt;a href="https://console.aws.amazon.com/iam/home#/policies/arn:aws:iam::aws:policy/service-role/AmazonSSMMaintenanceWindowRole"&gt;AmazonSSMMaintenanceWindowRole&lt;/a&gt; to that role. This policy gives some basic permissions to the role which allow it to execute commands and more on the instances.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"patch_mw_role_assume"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;principals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Service"&lt;/span&gt;

      &lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s2"&gt;"ec2.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"ssm.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"patch_mw_role"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PatchingMaintWindow"&lt;/span&gt;
  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patch_mw_role_assume&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy"&lt;/span&gt; &lt;span class="s2"&gt;"ssm_maintenance_window"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:iam::aws:policy/service-role/AmazonSSMMaintenanceWindowRole"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"patch_mw_role_attach"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patch_mw_role&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;policy_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ssm_maintenance_window&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Testing the barebones maintenance window
&lt;/h3&gt;

&lt;p&gt;Once we’ve ran &lt;code&gt;terraform apply&lt;/code&gt; on all the above, we can test the maintenance window out. Currently we have it set to run at 9am UK time, which may or may not be a long time away - so change it manually in &lt;a href="https://console.aws.amazon.com/systems-manager/maintenance-windows"&gt;the console&lt;/a&gt; to a time not far away from your time now.&lt;/p&gt;

&lt;p&gt;Once it’s done executing, you can navigate to the history tab to view the execution.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-1/maintenance-window-history.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GTsU2KYe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-1/maintenance-window-history.png" alt="The execution history for the maintenance window, showing both successful and failed executions"&gt; &lt;/a&gt;Let’s hope you have more luck on your first attempts running this than I did!&lt;/p&gt;

&lt;p&gt;You can select any execution and drill down into it with the &lt;strong&gt;View details&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-1/mw-execution-details.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--40dqwusx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-1/mw-execution-details.png" alt="A detailed look into a maintenance window execution"&gt; &lt;/a&gt;You can see that the tasks got executed in the order we defined them in&lt;/p&gt;

&lt;p&gt;We can go deeper in the execution details and pull out the result of individual commands by selecting &lt;strong&gt;View details&lt;/strong&gt; on the task invocation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-1/task-invocation-command-detail.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hgjcfk8_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-1/task-invocation-command-detail.png" alt="Detailed breakdown of the RunPatchBaseline command, showing success all round"&gt; &lt;/a&gt;The maintenance window has redirected us to the same page as when we manually invoked the command documents in the last post&lt;/p&gt;

&lt;p&gt;Clicking on one of the instance IDs in the above screenshot will take us to the command output for that instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logging command output to S3
&lt;/h2&gt;

&lt;p&gt;Maintenance windows by default only capture the first 2500 characters of a command output, if your command outputs more than this then it gets truncated. This can be a problem if you have a task failure and need to examine the output for the reason why it failed.&lt;/p&gt;

&lt;p&gt;Take the &lt;strong&gt;AWS-RunPatchBaseline&lt;/strong&gt; output on a Linux instance for example. It’s pretty hefty, and so we lose a lot of context on what actually happened:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-1/truncated-command-output.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--sTdoNqLt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-1/truncated-command-output.png" alt="Log output of the patch event on a Linux instance, with the words Output Truncated at the end."&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Method
&lt;/h3&gt;

&lt;p&gt;To combat this, maintenance windows allow you to dump command output to an S3 bucket, so that you can retrieve it later. In the last post we created an S3 bucket to store our SSM scripts (&lt;code&gt;aws_s3_bucket.script_bucket.arn&lt;/code&gt;), we can reuse that bucket to store our command logs too.&lt;/p&gt;

&lt;p&gt;In order to do this there are some steps we need to take:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The S3 bucket policy needs to permit the EC2 instance role &lt;code&gt;aws_iam_role.vm_base&lt;/code&gt; to &lt;code&gt;s3:PutObject&lt;/code&gt; on &lt;code&gt;"${aws_s3_bucket.script_bucket.arn}/ssm_output/*"&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ssm_output/&lt;/code&gt; is the directory/prefix in the S3 bucket where we will store the logs&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The KMS key used to encrypt objects in the target S3 bucket needs to permit instance role &lt;code&gt;aws_iam_role.vm_base&lt;/code&gt; to &lt;code&gt;kms:GenerateDataKey&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The instance role &lt;code&gt;aws_iam_role.vm_base&lt;/code&gt; needs permissions to do the above on its respective side&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can view the changes required in &lt;a href="https://github.com/jdheyburn/terraform-examples/tree/main/aws-ssm-automation-1-logging"&gt;GitHub&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-1-logging/ssm_command_s3.tf#L99"&gt;S3 bucket policy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-1-logging/ssm_command_s3.tf#L35"&gt;KMS key policy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-1-logging/ec2_iam.tf#L33"&gt;EC2 instance role&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once that is done we’ll need to add some new attributes to the maintenance window tasks, telling it where to dump the command output to.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_maintenance_window_task"&lt;/span&gt; &lt;span class="s2"&gt;"task_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="c1"&gt;# ... other attributes hidden&lt;/span&gt;

  &lt;span class="nx"&gt;task_invocation_parameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;run_command_parameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;output_s3_bucket&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;script_bucket&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
      &lt;span class="nx"&gt;output_s3_key_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ssm_output/"&lt;/span&gt;

      &lt;span class="c1"&gt;# ... other attributes hidden&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you have the new config written, then you can &lt;code&gt;terraform apply&lt;/code&gt; and run another test on the maintenance window.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-1/command-s3-output-button.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--AXGzDeiF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-1/command-s3-output-button.png" alt="Detailed view of the run patch baseline task, showing a button called Amazon S3 which redirects us to where the logs are stored in S3"&gt; &lt;/a&gt;We now get a button that can redirect us to where the logs are stored in S3&lt;/p&gt;

&lt;p&gt;If we click on this button, we can view the logs being stored in S3. Follow the path in S3 until you reach the S3 object containing the logs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-1/log-object-in-s3.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4s8BhCjW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-1/log-object-in-s3.png" alt="The S3 object containing the logs"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now you can open this using the &lt;strong&gt;Object actions&lt;/strong&gt; button in the top-right hand corner to view the entire logs!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-1/open-logs-in-browser.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KAt0uPBy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-1/open-logs-in-browser.png" alt="Opening the logs in the browser, we see the complete text output of the run patch baseline command invoked"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Logging problems
&lt;/h3&gt;

&lt;p&gt;It’s worth noting that SSM does not raise an error if an instance cannot push logs to S3 - the &lt;strong&gt;Amazon S3&lt;/strong&gt; button will redirect you to an object in S3 that does not exist. So if your logs are not appearing in S3 then ensure you’ve followed the steps above.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-1/failed-log-upload-to-s3.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--tImsyWNN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-1/failed-log-upload-to-s3.png" alt="An empty S3 object page, caused by incorrectly setting up S3 output logging"&gt; &lt;/a&gt;An example showing if logs were not successfully uploaded to S3 - if you get this then double check you’ve set everything up right!&lt;/p&gt;

&lt;h3&gt;
  
  
  Removing old command logs
&lt;/h3&gt;

&lt;p&gt;Now that we have maintenance windows storing our logs in S3, we should ensure we’re maintaining a good level of hygiene by removing old logs - otherwise our S3 bucket is going to store more and more logs, costing us more money.&lt;/p&gt;

&lt;p&gt;S3 has a feature called &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lifecycle-mgmt.html"&gt;Lifecycle Rules&lt;/a&gt; which tells S3 how to handle objects throughout their lifecycle. We can tell it to move old files to a cheaper storage class, archive them to &lt;a href="https://aws.amazon.com/glacier/"&gt;S3 Glacier&lt;/a&gt; (AWS’s long-term storage service), or just simply delete them!&lt;/p&gt;

&lt;p&gt;Given we’re not exactly sentimental with logs, we can define a policy that will remove any logs older than 3 months (90 days).&lt;/p&gt;

&lt;p&gt;This is very easy for us to add, we simply need to make the addition below to our &lt;code&gt;aws_s3_bucket&lt;/code&gt; resource.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"script_bucket"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;# ... other attributes hidden&lt;/span&gt;

  &lt;span class="c1"&gt;# Remove old SSM command output logs&lt;/span&gt;
  &lt;span class="nx"&gt;lifecycle_rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"RemoveOldSSMOutputLogs"&lt;/span&gt;
    &lt;span class="nx"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

    &lt;span class="nx"&gt;prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ssm_output/"&lt;/span&gt;

    &lt;span class="nx"&gt;expiration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;days&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can add more rules if you like, such as a &lt;code&gt;transition&lt;/code&gt; block to move it to cold storage before deleting if you wish. Take a look at the &lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket#using-object-lifecycle"&gt;Terraform documentation&lt;/a&gt; for the resource for example of this.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-1/s3-lifecycle-policy.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ftM5OUI9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-1/s3-lifecycle-policy.png" alt="The created S3 lifecycle rule can be seen here, indicating that objects in the ssm_output directory are discarded after 90 days"&gt; &lt;/a&gt;Logs older than 90 days certainly do not spark joy…!&lt;/p&gt;

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

&lt;p&gt;With the first two posts of this series, you have enough to be able to create your own automated series of commands that can be executed on your EC2 instances.&lt;/p&gt;

&lt;p&gt;You can also use SSM documents to retrieve files from instances - such as log files. Or even use them to update third-party software on the instances.&lt;/p&gt;

&lt;p&gt;The next post and thereafter we’ll be exploring the Command Documents sibling; Automation Documents, and exploring how these can further enhance automation to other AWS services beyond EC2 instances.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>automation</category>
    </item>
    <item>
      <title>Automate Instance Hygiene with AWS SSM: Command Documents</title>
      <dc:creator>Joseph Heyburn</dc:creator>
      <pubDate>Fri, 30 Oct 2020 00:00:00 +0000</pubDate>
      <link>https://dev.to/jdheyburn/automate-instance-hygiene-with-aws-ssm-command-documents-c10</link>
      <guid>https://dev.to/jdheyburn/automate-instance-hygiene-with-aws-ssm-command-documents-c10</guid>
      <description>&lt;p&gt;It’s been a while since my &lt;a href="https://dev.to/jdheyburn/assertions-in-gotests-test-generation-6h1"&gt;last post&lt;/a&gt;… which could be down to me trying to salvage something out of summer! 😅&lt;/p&gt;

&lt;p&gt;In this post I want to talk a little bit more about &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/what-is-systems-manager.html"&gt;AWS SSM&lt;/a&gt;. This was something I touched on when discussing patch baselines in a &lt;a href="https://dev.to/jdheyburn/using-terraform-to-manage-aws-patch-baselines-at-enterprise-scale-2nck#ssm-amp-patch-manager"&gt;previous post&lt;/a&gt;, and within there is another service known as &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-automation.html"&gt;Automation&lt;/a&gt;. On first glance it may look pretty dull, but once you scratch the surface there are a number of capabilities it can unlock for you. I found a distinct lack of resources on how to write these documents, so this post will aim to help you get started on doing so!&lt;/p&gt;

&lt;p&gt;This will be part one of a four part series about SSM Automation. The outline of the posts will be:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Re-introduction to SSM Documents in general, specifically the Command document by writing our own healthcheck document&lt;/li&gt;
&lt;li&gt;How to automate command documents with SSM Maintenance Windows&lt;/li&gt;
&lt;li&gt;Looking into Automation documents, and integrating them with maintenance windows&lt;/li&gt;
&lt;li&gt;Advanced use case for Automation documents&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  tl;dr
&lt;/h2&gt;

&lt;p&gt;To summarise the key parts of this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSM can be used to automate several parts of your estate&lt;/li&gt;
&lt;li&gt;Command documents are a means of executing logically indifferent commands across multiple platforms&lt;/li&gt;
&lt;li&gt;How to write a basic command document in Terraform&lt;/li&gt;
&lt;li&gt;How to write a verbose command document in Terraform&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;This series assumes you have some knowledge of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terraform&lt;/li&gt;
&lt;li&gt;These AWS services:

&lt;ul&gt;
&lt;li&gt;EC2&lt;/li&gt;
&lt;li&gt;IAM&lt;/li&gt;
&lt;li&gt;S3&lt;/li&gt;
&lt;/ul&gt;


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

&lt;p&gt;All source code for this post is available on &lt;a href="https://github.com/jdheyburn/terraform-examples/tree/main/aws-ssm-automation-0"&gt;GitHub&lt;/a&gt;, I’ll be referencing it throughout.&lt;/p&gt;

&lt;h2&gt;
  
  
  SSM Re-primer
&lt;/h2&gt;

&lt;p&gt;I’ve mentioned &lt;a href="https://dev.to/jdheyburn/using-terraform-to-manage-aws-patch-baselines-at-enterprise-scale-2nck#ssm-amp-patch-manager"&gt;SSM Documents&lt;/a&gt; before; to save you a click:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-ssm-docs.html"&gt;SSM Document&lt;/a&gt; is essentially an automation script that you can perform on one or more instances at a time, with conditions to apply different sets of scripts depending on the operating system (OS) platform (i.e. Windows / Linux).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In that post I talk about how &lt;strong&gt;AWS-RunPatchBaseline&lt;/strong&gt; is an example of a &lt;code&gt;COMMAND&lt;/code&gt; document. Another type of document is known as &lt;code&gt;AUTOMATION&lt;/code&gt;. AWS &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-ssm-docs.html"&gt;describes them&lt;/a&gt; as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Command

&lt;ul&gt;
&lt;li&gt;Uses command documents to run commands&lt;/li&gt;
&lt;li&gt;State Manager uses command documents to apply a configuration&lt;/li&gt;
&lt;li&gt;These actions can be run on one or more targets at any point during the lifecycle of an instance&lt;/li&gt;
&lt;li&gt;Maintenance Windows uses command documents to apply a configuration based on the specified schedule&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Automation

&lt;ul&gt;
&lt;li&gt;Use automation documents when performing common maintenance and deployment tasks such as creating or updating an Amazon Machine Image (AMI)&lt;/li&gt;
&lt;li&gt;State Manager uses automation documents to apply a configuration&lt;/li&gt;
&lt;li&gt;These actions can be run on one or more targets at any point during the lifecycle of an instance&lt;/li&gt;
&lt;li&gt;Maintenance Windows uses automation documents to perform common maintenance and deployment tasks based on the specified schedule&lt;/li&gt;
&lt;/ul&gt;


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

&lt;p&gt;I like to use the below to differentiate between the two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Command documents execute scripts on instances&lt;/li&gt;
&lt;li&gt;Automation document can call and orchestrate AWS API endpoints on your behalf, including executing Command documents on instances&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are also &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-ssm-docs.html"&gt;other types&lt;/a&gt; too which are beyond the scope of this series.&lt;/p&gt;

&lt;h3&gt;
  
  
  SSM Managed Instances
&lt;/h3&gt;

&lt;p&gt;In order to have command documents executed on your instances, they will need to become &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/managed_instances.html"&gt;managed instances&lt;/a&gt;. This requires having the below set up correctly on your instances:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent.html"&gt;SSM Agent&lt;/a&gt; installed on your instance

&lt;ul&gt;
&lt;li&gt;done so by default on all Amazon Linux AMIs and Windows AMIs&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;connectivity from your instances to the &lt;a href="https://docs.aws.amazon.com/general/latest/gr/ssm.html"&gt;following endpoints&lt;/a&gt;:

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;https://ssm.REGION.amazonaws.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://ssmmessages.REGION.amazonaws.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://ec2messages.REGION.amazonaws.com&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;the &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-setting-up-messageAPIs.html"&gt;correct IAM permissions&lt;/a&gt; applied on your EC2 instance profile

&lt;ul&gt;
&lt;li&gt;these are all provided by the AWS IAM policy &lt;a href="https://console.aws.amazon.com/iam/home#/policies/arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore%24serviceLevelSummary"&gt;AmazonSSMManagedInstanceCore&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;the Terraform example for this post shows the &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-0/ec2_iam.tf"&gt;policy being attached&lt;/a&gt; to the EC2 IAM role&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional&lt;/strong&gt; : &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/setup-instance-profile.html"&gt;additional policies&lt;/a&gt; that may be required based on your use case&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-0/managed-instances.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--EBx5bBUn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-0/managed-instances.png" alt="AWS SSM Managed Instances view with two instances appearing as online and managed"&gt; &lt;/a&gt;If your instances are appearing in the &lt;a href="https://console.aws.amazon.com/systems-manager/managed-instances"&gt;managed instances console&lt;/a&gt; then everything is set up correctly; if not then follow the &lt;a href="https://aws.amazon.com/premiumsupport/knowledge-center/systems-manager-ec2-instance-not-appear/"&gt;troubleshooting guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Command documents
&lt;/h2&gt;

&lt;p&gt;Documents are defined in either JSON or, &lt;a href="https://github.com/cblp/yaml-sucks"&gt;for better or for worse&lt;/a&gt;, YAML. You can also define what OS each command should be executed on. This could be helpful if you wanted a healthcheck script to be executed across all (or a subset of) your instances in one action. As an example, my healthcheck script could be to check to see if the CPU is overloaded.&lt;/p&gt;

&lt;h3&gt;
  
  
  Constructing a healthcheck script
&lt;/h3&gt;

&lt;p&gt;I could use the below to perform a healthcheck on a Windows box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$Avg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-WmiObject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Win32_Processor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Measure-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Property&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;LoadPercentage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Average&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Average&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Average&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;If&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$Avg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-gt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;Throw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Instance is unhealthy - Windows"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Write-Output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Instance is healthy - Windows"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the equivalent for Linux would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Sources:&lt;/span&gt;
&lt;span class="c"&gt;# https://stackoverflow.com/a/9229580&lt;/span&gt;
&lt;span class="c"&gt;# https://bits.mdminhazulhaque.io/linux/round-number-in-bash-script.html&lt;/span&gt;
&lt;span class="nv"&gt;avg_cpu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'cpu '&lt;/span&gt; /proc/stat | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{usage=($2+$4)*100/($2+$4+$5)} END {print int(usage)+1}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt; avg_cpu &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 90 &lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Instance is unhealthy - Linux"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Instance is healthy - Linux"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;You’ll notice the scripts output the OS they are running on - this will serve as an explanation for when we run the scripts as a command document later on.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Note these scripts are just rudimentary examples of healthcheck scripts. Your healthcheck script may be checking that services are running ok, whether a task can be performed, etc. For the sake of this example, I’ve decided to do a simple check against the CPU load for demonstration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing in AWS SSM Console
&lt;/h3&gt;

&lt;p&gt;Let’s now test them in the AWS SSM Console. For this example I’ve spun up two EC2 instances, one Linux and one Windows, &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-0/ec2.tf"&gt;using Terraform&lt;/a&gt;. To run the commands we can navigate to &lt;a href="https://console.aws.amazon.com/systems-manager/run-command/send-command"&gt;Run Command&lt;/a&gt; in the console, and run &lt;code&gt;AWS-RunPowerShellScript&lt;/code&gt; and &lt;code&gt;AWS-RunShellScript&lt;/code&gt; for both Windows and Linux EC2s respectively. We don’t care about logging the output of the scripts just yet. Make sure when running the script you manually select what instance to run it on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-0/run-command-script-success-linux.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PuaLrgQ0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-0/run-command-script-success-linux.png" alt="Executing the Linux script successfully on the Linux instance"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-0/run-command-script-success-windows.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--uoQOhdad--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-0/run-command-script-success-windows.png" alt="Executing the Windows script successfully on the Windows instance"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can see they have executed fine. Let’s flip the condition in the healthcheck script so we can test them failing (done by changing &lt;code&gt;&amp;gt;&lt;/code&gt; to &lt;code&gt;&amp;lt;&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-0/run-command-script-failure-linux.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Lrda8SZS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-0/run-command-script-failure-linux.png" alt="AWS indicating a failure in the command due to a failure occuring in the script"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is cool - in the script we can define what constitutes as a failure and have this propagate up to AWS - notice how the status was Failed. This will come in use later on in the series.&lt;/p&gt;

&lt;h3&gt;
  
  
  Terraforming command documents
&lt;/h3&gt;

&lt;p&gt;Now let’s get these scripts Terraformed so we can reap the &lt;a href="https://dev.to/jdheyburn/using-terraform-to-manage-aws-patch-baselines-at-enterprise-scale-2nck#infrastructure-as-code-primer"&gt;benefits&lt;/a&gt; of infrastructure-as-code. First we need to define the document in the YAML format.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# documents/perform_healthcheck.yml&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;schemaVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.2"&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Perform a healthcheck on the target instance&lt;/span&gt;
&lt;span class="na"&gt;mainSteps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PerformHealthCheckWindows&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:runPowerShellScript&lt;/span&gt;
    &lt;span class="na"&gt;precondition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;StringEquals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;platformType&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Windows&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;runCommand&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$Avg&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(Get-WmiObject&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Win32_Processor&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Measure-Object&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-Property&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;LoadPercentage&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-Average&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Select&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Average).Average"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;If&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;($Avg&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-gt&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;90)&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="nv"&gt;  &lt;/span&gt;&lt;span class="s"&gt;Throw&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Instance&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;unhealthy-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Linux"'&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;}"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Write-Output&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Instance&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;healthy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Windows"'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PerformHealthCheckLinux&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:runShellScript&lt;/span&gt;
    &lt;span class="na"&gt;precondition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;StringEquals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;platformType&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Linux&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;runCommand&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;avg_cpu=$(grep&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'cpu&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/proc/stat&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;awk&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'{usage=($2+$4)*100/($2+$4+$5)}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;END&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{print&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;int(usage)+1}')"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;if&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;((&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;avg_cpu&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;90&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;));&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;then"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="nv"&gt;  &lt;/span&gt;&lt;span class="s"&gt;echo&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Instance&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;unhealthy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Linux"'&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;  &lt;/span&gt;&lt;span class="s"&gt;exit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fi"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;echo&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Instance&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;healthy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Linux"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-0/documents/perform_healthcheck.yml"&gt;GitHub URL&lt;/a&gt; for the above&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Since a document is meant to perform an action (or group of actions) on a set of instances regardless of their operating system (OS), only the steps that apply to the OS platform for the instance they are being executed on will be invoked.&lt;/p&gt;

&lt;p&gt;Therefore when we target this document to run on two types of EC2 instances, one Windows and one Linux, the step &lt;code&gt;PerformHealthCheckWindows&lt;/code&gt; will be executed on the Windows box and vice versa for &lt;code&gt;PerformHealthCheckLinux&lt;/code&gt;. This is because of the &lt;code&gt;precondition&lt;/code&gt; key which filters on the &lt;code&gt;platformType&lt;/code&gt; for the instance. Notably as well, we are targeting the appropriate actions for each platform; &lt;code&gt;aws:runPowerShellScript&lt;/code&gt; for Windows and &lt;code&gt;aws:runShellScript&lt;/code&gt; for Linux.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-plugins.html"&gt;See here&lt;/a&gt; for a full list of what actions you can perform in a command document.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Using Terraform you can deploy out the document to your environment like so.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_document"&lt;/span&gt; &lt;span class="s2"&gt;"perform_healthcheck"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PerformHealthcheck"&lt;/span&gt;
  &lt;span class="nx"&gt;document_type&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Command"&lt;/span&gt;
  &lt;span class="nx"&gt;document_format&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"YAML"&lt;/span&gt;

  &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"documents/perform_healthcheck.yml"&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-0/ssm_command.tf"&gt;GitHub URL&lt;/a&gt; for the above&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once deployed, we can navigate to System Manager in the AWS Console to invoke the document on our estate. This is done in the same manner as when we ran &lt;code&gt;AWS-RunShellScript&lt;/code&gt; above, except now we are targeting &lt;code&gt;PerformHealthcheck&lt;/code&gt;. Since our document has commands for both Linux and Windows, we can have it invoked across both platform types and only the scripts written for their platform will be invoked.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-0/command-document-success.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LhV6Cw5g--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-0/command-document-success.png" alt="Successfully executing the new command document across both Linux and Windows instances"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can see that both executed successfully! You can view the output of each command invocation on each instance like previously. For the screenshot below of the Linux instance, only the Linux step was executed, whereas the Windows step was skipped. You’ll notice our message from earlier “&lt;code&gt;- Linux&lt;/code&gt;” is there, reassuring us that only the Linux script was executed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-0/command-document-success-linux.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--lJzf20hx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-0/command-document-success-linux.png" alt="Focusing on the Linux invocation, highlighting that only the Linux step was invoked"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s dive into some more intermediate documents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verbose command documents
&lt;/h2&gt;

&lt;p&gt;The example above was a very basic example of such a document where we only had a few lines of code to execute. The healthcheck script you write for your service may have several more lines of code to execute, and trying to read lines of code in amongst the document markup format is not easy on the eyes. Here is a snippet of the &lt;strong&gt;AWS-RunPatchBaseline&lt;/strong&gt; document as an example of what I mean. It is written in JSON and has over 100 lines in the &lt;code&gt;runCommand&lt;/code&gt; section.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mainSteps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"precondition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"platformType"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Windows"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws:runPowerShellScript"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PatchWindows"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"timeoutSeconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"runCommand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"# Check the OS version"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"if ([Environment]::OSVersion.Version.Major -le 5) {"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"    Write-Error 'This command is not supported on Windows 2003 or lower.'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"    exit -1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"} elseif ([Environment]::OSVersion.Version.Major -ge 10) {"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"    $sku = (Get-CimInstance -ClassName Win32_OperatingSystem).OperatingSystemSKU"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"    if ($sku -eq 143 -or $sku -eq 144) {"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"        Write-Host 'This command is not supported on Windows 2016 Nano Server.'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"        exit -1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"    }"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"# Check the SSM agent version"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"$ssmAgentService = Get-ItemProperty 'HKLM:SYSTEM&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;CurrentControlSet&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;Services&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;AmazonSSMAgent&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"if (-not $ssmAgentService -or $ssmAgentService.Version -lt '2.0.533.0') {"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"    Write-Host 'This command is not supported with SSM Agent version less than 2.0.533.0.'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"    exit -1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;You can see the &lt;a href="https://console.aws.amazon.com/systems-manager/documents/AWS-RunPatchBaseline/content"&gt;whole thing&lt;/a&gt; on AWS.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Even from this snippet it is hard to distinguish what is going on. Losing out on &lt;a href="https://en.wikipedia.org/wiki/Syntax_highlighting"&gt;syntax highlighting&lt;/a&gt; means the code isn’t readable by any means. And since this is a JSON file format, we lose type hinting for the language we are writing the script in (PowerShell in this case). If the script was saved in as a PowerShell file (&lt;code&gt;.ps1&lt;/code&gt;) then we’d get all those benefits.&lt;/p&gt;

&lt;p&gt;We can fix all these issues by adopting a common pattern when composing command documents. We can have S3 store the script, then have the command document perform these actions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Download the script in question from S3&lt;/li&gt;
&lt;li&gt;Execute the script from the download location&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Such a command document would have a composition as below, whereas this is performing the same healthcheck script previously.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# documents/perform_healthcheck_s3.yml&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;schemaVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.2"&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Perform a healthcheck on the target instance&lt;/span&gt;
&lt;span class="na"&gt;mainSteps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:downloadContent&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DownloadScriptWindows&lt;/span&gt;
    &lt;span class="na"&gt;precondition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;StringEquals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;platformType&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Windows&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;sourceType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;S3&lt;/span&gt;
      &lt;span class="na"&gt;sourceInfo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{"path":"https://s3.amazonaws.com/jdheyburn-scripts/ssm_scripts/PerformHealthcheck.ps1"}'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:runPowerShellScript&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ExecutePerformHealthCheckWindows&lt;/span&gt;
    &lt;span class="na"&gt;precondition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;StringEquals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;platformType&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Windows&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;runCommand&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;..\downloads\PerformHealthcheck.ps1&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:downloadContent&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DownloadScriptLinux&lt;/span&gt;
    &lt;span class="na"&gt;precondition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;StringEquals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;platformType&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Linux&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;sourceType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;S3&lt;/span&gt;
      &lt;span class="na"&gt;sourceInfo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{"path":"https://s3.amazonaws.com/jdheyburn-scripts/ssm_scripts/perform_healthcheck.sh"}'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:runShellScript&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ExecutePerformHealthCheckLinux&lt;/span&gt;
    &lt;span class="na"&gt;precondition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;StringEquals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;platformType&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Linux&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;runCommand&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;../downloads/perform_healthcheck.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-0/documents/perform_healthcheck_s3.yml"&gt;GitHub URL&lt;/a&gt; for the above&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Note the new action called &lt;code&gt;aws:downloadContent&lt;/code&gt; - which you can view the documentation for &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-plugins.html#aws-downloadContent"&gt;here&lt;/a&gt;. Again we’re using the &lt;code&gt;precondition&lt;/code&gt; key to ensure each platforms downloads their respective script. We’re also using the &lt;code&gt;inputs&lt;/code&gt; key to instruct the action where it can download the script from; S3 in this case, at the given S3 bucket location. There is an optional &lt;code&gt;destinationPath&lt;/code&gt; field which allows you to change where it downloads to.&lt;/p&gt;

&lt;p&gt;Once the script is downloaded to the instance, we will need to have it executed. &lt;code&gt;aws:downloadContent&lt;/code&gt; saves the script to a temporary directory for executing SSM command invocations on instances, so we need to reference it in the &lt;code&gt;downloads&lt;/code&gt; directory for it; indicated by the &lt;code&gt;../downloads/perform_healthcheck.sh&lt;/code&gt; command.&lt;/p&gt;

&lt;h3&gt;
  
  
  Terraforming verbose command documents
&lt;/h3&gt;

&lt;p&gt;This involves having your script first uploaded to S3. Thankfully, through the power of &lt;a href="https://dev.to/jdheyburn/using-terraform-to-manage-aws-patch-baselines-at-enterprise-scale-2nck#infrastructure-as-code-primer"&gt;infrastructure-as-code&lt;/a&gt;, you can have this automatically deployed to your environment once the module is written.&lt;/p&gt;

&lt;p&gt;Check the &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-0/ssm_command_s3.tf"&gt;GitHub repository&lt;/a&gt; for the full example, for now let me break down what each bit is doing.&lt;/p&gt;

&lt;h4&gt;
  
  
  KMS
&lt;/h4&gt;

&lt;p&gt;I want my S3 bucket to encrypt objects that are stored there, so we’ll need to create a &lt;a href="https://aws.amazon.com/kms/"&gt;KMS key&lt;/a&gt; and give it an IAM policy permitting any users of the account to decrypt using the key. You can make this more secure by only targeting the IAM roles that require it, instead of the whole account, adopting the &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege"&gt;principle of least privilege&lt;/a&gt; best practice 🔒&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can read up more about IAM policies &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html"&gt;here&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"kms_allow_decrypt"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AllowKMSAdministration"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;

    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"kms:Create*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"kms:Describe*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"kms:Enable*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"kms:List*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"kms:Put*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"kms:Update*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"kms:Revoke*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"kms:Disable*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"kms:Get*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"kms:Delete*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"kms:ScheduleKeyDeletion"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"kms:CancelKeyDeletion"&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;principals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS"&lt;/span&gt;
      &lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::${data.aws_caller_identity.current.account_id}:user/jdheyburn"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AllowDecrypt"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;

    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"kms:Decrypt"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;principals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS"&lt;/span&gt;
      &lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="c1"&gt;# TIP: For increased security only give decrypt permissions to roles that need it&lt;/span&gt;
      &lt;span class="c1"&gt;# identifiers = [aws_iam_role.vm_base.arn]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_kms_key"&lt;/span&gt; &lt;span class="s2"&gt;"script_bucket_key"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"This key is used to encrypt bucket objects"&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kms_allow_decrypt&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  S3
&lt;/h4&gt;

&lt;p&gt;Next we’re creating the S3 bucket to store the scripts; we’re encrypting it with the KMS key we created, &lt;code&gt;aws_kms_key.script_bucket_key&lt;/code&gt;. We’re also applying an IAM policy on the bucket permitting any users of the account to download anything from the &lt;code&gt;ssm_scripts/&lt;/code&gt; prefix in the bucket (this prefix is where the scripts will be stored).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"script_bucket"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"jdheyburn-scripts"&lt;/span&gt;

  &lt;span class="c1"&gt;# Encrypt objects stored in S3&lt;/span&gt;
  &lt;span class="nx"&gt;server_side_encryption_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;apply_server_side_encryption_by_default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;kms_master_key_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_kms_key&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;script_bucket_key&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
        &lt;span class="nx"&gt;sse_algorithm&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aws:kms"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_caller_identity"&lt;/span&gt; &lt;span class="s2"&gt;"current"&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"s3_allow_script_download"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AllowAccountAccess"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;

    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;principals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS"&lt;/span&gt;
      &lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="c1"&gt;# TIP: For increased security only give decrypt permissions to roles that need it&lt;/span&gt;
      &lt;span class="c1"&gt;# identifiers = [aws_iam_role.vm_base.arn]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"${aws_s3_bucket.script_bucket.arn}/ssm_scripts/*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_policy"&lt;/span&gt; &lt;span class="s2"&gt;"script_bucket_policy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;script_bucket&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s3_allow_script_download&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Store Scripts in S3
&lt;/h4&gt;

&lt;p&gt;Now we are uploading the scripts from their &lt;a href="https://github.com/jdheyburn/terraform-examples/tree/main/aws-ssm-automation-0/scripts"&gt;location in the repository&lt;/a&gt; to S3, at the prefix where we defined an IAM policy to pull them from.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;perform_healthcheck_script_fname_windows&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PerformHealthcheck.ps1"&lt;/span&gt;
  &lt;span class="nx"&gt;perform_healthcheck_script_fname_linux&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"perform_healthcheck.sh"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_object"&lt;/span&gt; &lt;span class="s2"&gt;"perform_healthcheck_windows"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;script_bucket&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;key&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ssm_scripts/${local.perform_healthcheck_script_fname_windows}"&lt;/span&gt;
  &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"scripts/${local.perform_healthcheck_script_fname_windows}"&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_object"&lt;/span&gt; &lt;span class="s2"&gt;"perform_healthcheck_linux"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;script_bucket&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;key&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ssm_scripts/${local.perform_healthcheck_script_fname_linux}"&lt;/span&gt;
  &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"scripts/${local.perform_healthcheck_script_fname_linux}"&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  SSM Document
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_document"&lt;/span&gt; &lt;span class="s2"&gt;"perform_healthcheck_s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PerformHealthcheckS3"&lt;/span&gt;
  &lt;span class="nx"&gt;document_type&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Command"&lt;/span&gt;
  &lt;span class="nx"&gt;document_format&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"YAML"&lt;/span&gt;

  &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;templatefile&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"documents/perform_healthcheck_s3_template.yml"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;bucket_name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;script_bucket&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;linux_fname&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;perform_healthcheck_script_fname_linux&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;linux_key&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket_object&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;perform_healthcheck_linux&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;windows_fname&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;perform_healthcheck_script_fname_windows&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;windows_key&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket_object&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;perform_healthcheck_windows&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we’re creating an &lt;code&gt;aws_ssm_document&lt;/code&gt; as we had done before, but the document file we’re targeting this time is a template file.&lt;/p&gt;

&lt;p&gt;Notice how we have the presence of &lt;code&gt;${bucket_name}&lt;/code&gt; and others? These are template variables. With the use of the &lt;a href="https://www.terraform.io/docs/configuration/functions/templatefile.html"&gt;Terraform function&lt;/a&gt; &lt;code&gt;templatefile()&lt;/code&gt;, we can insert Terraform variables into the config to have the template name replaced with the value we’re passing in. In this case, &lt;code&gt;${bucket_name}&lt;/code&gt; will get replaced with the output of &lt;code&gt;aws_s3_bucket.script_bucket.id&lt;/code&gt;, which is &lt;code&gt;jdheyburn-scripts&lt;/code&gt;, and the same for the remaining variables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# documents/perform_healthcheck_s3_template.yml&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;schemaVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.2"&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Perform a healthcheck on the target instance&lt;/span&gt;
&lt;span class="na"&gt;mainSteps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:downloadContent&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DownloadScriptWindows&lt;/span&gt;
    &lt;span class="na"&gt;precondition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;StringEquals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;platformType&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Windows&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;sourceType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;S3&lt;/span&gt;
      &lt;span class="na"&gt;sourceInfo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{"path":"https://s3.amazonaws.com/${bucket_name}/${windows_key}"}'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:runPowerShellScript&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ExecutePerformHealthCheckWindows&lt;/span&gt;
    &lt;span class="na"&gt;precondition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;StringEquals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;platformType&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Windows&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;runCommand&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;..\downloads\${windows_fname}&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:downloadContent&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DownloadScriptLinux&lt;/span&gt;
    &lt;span class="na"&gt;precondition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;StringEquals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;platformType&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Linux&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;sourceType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;S3&lt;/span&gt;
      &lt;span class="na"&gt;sourceInfo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{"path":"https://s3.amazonaws.com/${bucket_name}/${linux_key}"}'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:runShellScript&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ExecutePerformHealthCheckLinux&lt;/span&gt;
    &lt;span class="na"&gt;precondition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;StringEquals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;platformType&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Linux&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;runCommand&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;../downloads/${linux_fname}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-0/documents/perform_healthcheck_s3_template.yml"&gt;GitHub URL&lt;/a&gt; for the above&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  EC2 IAM Permissions
&lt;/h4&gt;

&lt;p&gt;For the pattern to work, you’ll need to attach an IAM policy to the instance to allow it to pull the scripts from the S3 bucket. If we don’t do this then the &lt;code&gt;aws:downloadContent&lt;/code&gt; action will fail. From the Terraform above we’ve already applied the corresponding permissions on the S3 bucket, allowing all users and roles in the account to perform &lt;code&gt;s3:GetObject&lt;/code&gt; on the scripts. We’ve also allowed done the same for performing &lt;code&gt;kms:Decrypt&lt;/code&gt; on the KMS key that encrypts the S3 objects.&lt;/p&gt;

&lt;p&gt;If you’ve been following the &lt;a href="https://github.com/jdheyburn/terraform-examples/blob/main/aws-ssm-automation-0/ec2_iam.tf#L33"&gt;GitHub example&lt;/a&gt;, these required permissions have already been defined - for a recap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"ssm_scripts"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AllowS3"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;

    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"${aws_s3_bucket.script_bucket.arn}/ssm_scripts/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AllowKMS"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;

    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"kms:Decrypt"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_kms_key&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;script_bucket_key&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy"&lt;/span&gt; &lt;span class="s2"&gt;"ssm_scripts"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PullSSMScripts"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Enables instances to download SSM scripts from S3"&lt;/span&gt;

  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ssm_scripts&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"instance_download_scripts"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;# Change this to point to the role(s) for your instances&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vm_base&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;policy_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_policy&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ssm_scripts&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an important concept to remember about setting IAM policies - you will need to ensure that the correct permissions are applied on both the &lt;em&gt;source&lt;/em&gt; of the requestor, as well as the &lt;em&gt;destination&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing verbose documents
&lt;/h3&gt;

&lt;p&gt;Now, let’s give this command a spin in the console. We’re going to execute it the same way we did for other documents earlier, except now targeting &lt;code&gt;PerformHealthcheckS3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-0/s3-command-document-success.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TVhPKi96--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-0/s3-command-document-success.png" alt="Successful invocations for the new document pulling the script to be executed from S3"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you got any failures, make sure to dive into the failed invocation and see why it failed. Did it fail because of your healthcheck command? Then it is working as intended! Although if it failed on &lt;code&gt;aws:downloadContent&lt;/code&gt;, check to make sure your instances are running the latest version of SSM agent. You can do this with the &lt;code&gt;AWS-UpdateSSMAgent&lt;/code&gt; SSM document. Don’t be like me and spend hours troubleshooting against an out-of-date SSM agent! 😂&lt;/p&gt;


&lt;blockquote class="ltag__twitter-tweet"&gt;

  &lt;div class="ltag__twitter-tweet__main"&gt;
    &lt;div class="ltag__twitter-tweet__header"&gt;
      &lt;img class="ltag__twitter-tweet__profile-image" src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZWx7Y-h---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pbs.twimg.com/profile_images/1115257734352523266/9_5n8Lz5_normal.png" alt="Joseph D. Heyburn profile image"&gt;
      &lt;div class="ltag__twitter-tweet__full-name"&gt;
        Joseph D. Heyburn
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__username"&gt;
        &lt;a class="comment-mentioned-user" href="https://dev.to/jdheyburn"&gt;@jdheyburn&lt;/a&gt;

      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__twitter-logo"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--P4t6ys1m--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://practicaldev-herokuapp-com.freetls.fastly.net/assets/twitter-f95605061196010f91e64806688390eb1a4dbc9e913682e043eb8b1e06ca484f.svg" alt="twitter logo"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__body"&gt;
      Trying to write my next blog post about AWS SSM, and had a load of hours wiped out trying to troubleshoot an issue with SSM agents... turns out AWS AMIs install these bad agent versions by default 😭&lt;br&gt;&lt;br&gt;&lt;a href="https://t.co/mgFMVyB5fL"&gt;github.com/aws/amazon-ssm…&lt;/a&gt;
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__date"&gt;
      14:38 PM - 24 Oct 2020
    &lt;/div&gt;


    &lt;div class="ltag__twitter-tweet__actions"&gt;
      &lt;a href="https://twitter.com/intent/tweet?in_reply_to=1320011701480284162" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="/assets/twitter-reply-action.svg" alt="Twitter reply action"&gt;
      &lt;/a&gt;
      &lt;a href="https://twitter.com/intent/retweet?tweet_id=1320011701480284162" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="/assets/twitter-retweet-action.svg" alt="Twitter retweet action"&gt;
      &lt;/a&gt;
      0
      &lt;a href="https://twitter.com/intent/like?tweet_id=1320011701480284162" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="/assets/twitter-like-action.svg" alt="Twitter like action"&gt;
      &lt;/a&gt;
      0
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/blockquote&gt;


&lt;p&gt;Let’s now dive into the output of the Linux instance.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-0/s3-command-document-success-linux.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--wS2tJ-98--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-0/s3-command-document-success-linux.png" alt="Breakdown of steps invoked on Linux for a the command document we executed earlier, Windows steps are skipped"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Just like with the previous document with the script embedded &lt;code&gt;PerformHealthcheck&lt;/code&gt;, we can see the steps conditioned for Windows have been skipped (steps 1-2). Step 3 is where the document is doing real work, downloading the Linux script from the S3 location into the temp directory for SSM, and then executing it in step 4.&lt;/p&gt;

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

&lt;p&gt;I think this has been my longest post thus far, and it’s only part one of this series. SSM is a bit of a beast and is often shrugged off as being a pain to set up and troubleshoot. I put that down to it encapsulating several other services, and a lack of tried and tested documented methods - which I hope this series resolves.&lt;/p&gt;

&lt;p&gt;We covered in this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What SSM Documents are; and the differences between Command and Automation documents&lt;/li&gt;
&lt;li&gt;How to create our own command documents to execute the same business logic on differing instance platforms&lt;/li&gt;
&lt;li&gt;Adopting best practices by storing scripts in S3 and have a command document orchestrate the download and execution of said scripts&lt;/li&gt;
&lt;li&gt;… all while written in Terraform!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any mistakes were made in this post then please &lt;a href="https://jdheyburn.co.uk/blog/automate-instance-hygiene-with-aws-ssm-0/#contact"&gt;contact&lt;/a&gt; me - thanks for reading! 😃&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>automation</category>
    </item>
    <item>
      <title>Assertions in gotests Test Generation</title>
      <dc:creator>Joseph Heyburn</dc:creator>
      <pubDate>Sun, 26 Jul 2020 00:00:00 +0000</pubDate>
      <link>https://dev.to/jdheyburn/assertions-in-gotests-test-generation-6h1</link>
      <guid>https://dev.to/jdheyburn/assertions-in-gotests-test-generation-6h1</guid>
      <description>&lt;p&gt;I’ve been doing some programming in &lt;a href="https://golang.org/"&gt;Go&lt;/a&gt; for a side project again, and I’m back using &lt;a href="https://github.com/cweill/gotests"&gt;gotests&lt;/a&gt; to generate unit tests for functions. For this I’ve been referencing a post I’ve &lt;a href="https://dev.to/jdheyburn/extending-gotests-for-strict-error-tests-4j96"&gt;previously written&lt;/a&gt; in order to help me get them set up. If you’d like more context on the background I’d recommend reading there first.&lt;/p&gt;

&lt;p&gt;Today I’ll be talking about a small enhancement to how the tests are generated to make use of the &lt;a href="https://godoc.org/github.com/stretchr/testify/assert"&gt;assert&lt;/a&gt; package within &lt;a href="https://github.com/stretchr/testify"&gt;testify&lt;/a&gt;. Go already comes with enough for you to write tests, but &lt;code&gt;assert&lt;/code&gt; provides me with more options for comparison in a natural language form. I’ll also be adding support for when a test case returns an unexpected error.&lt;/p&gt;

&lt;p&gt;Don’t care about the waffle? Jump straight to it here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;p&gt;From the last time we visited this, our test code took the format below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Test_validateDogName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt;    &lt;span class="n"&gt;args&lt;/span&gt;
        &lt;span class="n"&gt;want&lt;/span&gt;    &lt;span class="kt"&gt;bool&lt;/span&gt;
        &lt;span class="n"&gt;wantErr&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="p"&gt;}{&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Test error was thrown for dog name with symbols"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"GoodestBoy#1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;wantErr&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dog cannot have symbols in their name"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;validateDogName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wantErr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;reflect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeepEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wantErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"validateDogName() error = %v, wantErr %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wantErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;want&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"validateDogName() = %v, want %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Although this works nicely, there is one issue - we’re not capturing unexpected errors. Or rather, if an error is returned to &lt;code&gt;err&lt;/code&gt;, and &lt;code&gt;tt.wantErr&lt;/code&gt; is set to &lt;code&gt;nil&lt;/code&gt;, then the test will not fail.&lt;/p&gt;

&lt;p&gt;Okay, so we still have the &lt;code&gt;if got != tt.want&lt;/code&gt; condition to fail the test if needed. Although we still could have this condition pass, we want to make sure we capture the error. The test suite is doing currently is &lt;em&gt;assuming&lt;/em&gt; we don’t care about the outcome of &lt;code&gt;err&lt;/code&gt;, just because we didn’t want one as described by &lt;code&gt;tt.wantErr&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I seem to remember assumptions being the &lt;del&gt;brother&lt;/del&gt; mother of something…&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/G-2NimrRPAQ"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Enhancing for Unexpected Errors
&lt;/h2&gt;

&lt;p&gt;In order to enhance what we have already from the original modified &lt;a href="https://gist.github.com/jdheyburn/978e7b84dc9c197bcdd41afece2edab5"&gt;function.tmpl&lt;/a&gt;, we could have it output something like this to capture unexpected errors.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// ... removed for brevity&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;validateDogName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wantErr&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"validateDogName() unexpected error = %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wantErr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;reflect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeepEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wantErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"validateDogName() error = %v, wantErr %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wantErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;want&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"validateDogName() = %v, want %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This enhancement is something we could implement fairly easily. On the other hand, the &lt;code&gt;assert&lt;/code&gt; library gives us a lot more to play with. It essentially is doing the same as the above under the hood, albeit in a cleaner fashion… and I’m all for better code readability!&lt;/p&gt;

&lt;h3&gt;
  
  
  Rewriting for Assert
&lt;/h3&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Test_validateDogName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt;    &lt;span class="n"&gt;args&lt;/span&gt;
        &lt;span class="n"&gt;want&lt;/span&gt;    &lt;span class="kt"&gt;bool&lt;/span&gt;
        &lt;span class="n"&gt;wantErr&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="p"&gt;}{&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Test error was thrown for dog name with symbols"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"GoodestBoy#1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;wantErr&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dog cannot have symbols in their name"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;validateDogName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wantErr&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s"&gt;"Error not expected but got one:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;
                        &lt;span class="s"&gt;"error: %q"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wantErr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EqualError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wantErr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The above code block is what we get when we take the test code above that is using the &lt;code&gt;t.Errorf&lt;/code&gt; function call to record a test failure, and rewrite it to use &lt;code&gt;assert&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What we now need to do is have &lt;code&gt;gotests&lt;/code&gt; generate it for us.&lt;/p&gt;

&lt;h2&gt;
  
  
  Customising Gotests Generated Test v2
&lt;/h2&gt;

&lt;p&gt;We’ll be following a process similar to when I &lt;a href="https://dev.to/jdheyburn/extending-gotests-for-strict-error-tests-4j96#customising-gotests-generated-test"&gt;last did this&lt;/a&gt;. I'm still using VSCode, so you’ll need to find the &lt;a href="https://github.com/cweill/gotests/#demo"&gt;correct plugin&lt;/a&gt; for your editor.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check out gotests and copy the templates directory to a place of your choosing

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;git clone https://github.com/cweill/gotests.git&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cp -R gotests/internal/render/templates ~/.vscode/gotests/templates&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;In order for us to achieve the generated test using &lt;code&gt;assert&lt;/code&gt;, this time we’re going to need to edit two files; &lt;code&gt;function.tmpl&lt;/code&gt; and &lt;code&gt;results.tmpl&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;Overwrite the contents of &lt;code&gt;function.tmpl&lt;/code&gt; with the &lt;a href="https://gist.github.com/jdheyburn/94eb1513395ae46eac6aa9721d089d3c#file-function-tmpl"&gt;contents of this Gist&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Overwrite the contents of &lt;code&gt;results.tmpl&lt;/code&gt; with the &lt;a href="https://gist.github.com/jdheyburn/94eb1513395ae46eac6aa9721d089d3c#file-results-tmpl"&gt;contents of this Gist&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Add the following setting to VSCode’s settings.json

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;"go.generateTestsFlags": ["--template_dir=~/.vscode/gotests/templates"]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now we can generate tests of functions that return the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;only returns an error&lt;/li&gt;
&lt;li&gt;a value, and an error&lt;/li&gt;
&lt;li&gt;multiple values, and an error&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I have the &lt;a href="https://marketplace.visualstudio.com/items?itemName=golang.go"&gt;Go plugin&lt;/a&gt; for VSCode, so I just need to right click over a function to have the dropdown menu appear with an option to generate tests.&lt;/p&gt;

&lt;p&gt;&lt;a href="//generate-unit-tests.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GeSpWAGm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://jdheyburn.co.uk/blog/assertions-in-gotests-test-generation/generate-unit-tests.png" alt="VSCode dropdown with Go plugin"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Only returns an error
&lt;/h3&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Test_validateDogName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt;    &lt;span class="n"&gt;args&lt;/span&gt;
        &lt;span class="n"&gt;wantErr&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="p"&gt;}{&lt;/span&gt;
        &lt;span class="c"&gt;// TODO: Add test cases.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;validateDogName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wantErr&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s"&gt;"Error not expected but got one:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;
                        &lt;span class="s"&gt;"error: %q"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wantErr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EqualError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wantErr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h3&gt;
  
  
  Returns a value, and an error
&lt;/h3&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Test_validateDogName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt;    &lt;span class="n"&gt;args&lt;/span&gt;
        &lt;span class="n"&gt;want&lt;/span&gt;    &lt;span class="kt"&gt;bool&lt;/span&gt;
        &lt;span class="n"&gt;wantErr&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="p"&gt;}{&lt;/span&gt;
        &lt;span class="c"&gt;// TODO: Add test cases.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;validateDogName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wantErr&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s"&gt;"Error not expected but got one:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;
                        &lt;span class="s"&gt;"error: %q"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wantErr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EqualError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wantErr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h3&gt;
  
  
  Returns multiple values, and an error
&lt;/h3&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Test_validateDogName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt;    &lt;span class="n"&gt;args&lt;/span&gt;
        &lt;span class="n"&gt;want&lt;/span&gt;    &lt;span class="kt"&gt;bool&lt;/span&gt;
        &lt;span class="n"&gt;want1&lt;/span&gt;   &lt;span class="kt"&gt;bool&lt;/span&gt;
        &lt;span class="n"&gt;wantErr&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="p"&gt;}{&lt;/span&gt;
        &lt;span class="c"&gt;// TODO: Add test cases.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;validateDogName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wantErr&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s"&gt;"Error not expected but got one:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;
                        &lt;span class="s"&gt;"error: %q"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wantErr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EqualError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wantErr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;want1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h3&gt;
  
  
  Bonus: Only returns values
&lt;/h3&gt;

&lt;p&gt;The below example is for a function that doesn’t produce any errors, but I’m including it for the sake of completeness.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Test_validateDogName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;
        &lt;span class="n"&gt;want&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;}{&lt;/span&gt;
        &lt;span class="c"&gt;// TODO: Add test cases.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;validateDogName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h2&gt;
  
  
  That’s It!
&lt;/h2&gt;

&lt;p&gt;This is just a short update to an enhancement I &lt;a href="https://dev.to/jdheyburn/extending-gotests-for-strict-error-tests-4j96"&gt;previously made&lt;/a&gt; to gotests. The &lt;code&gt;assert&lt;/code&gt; library is awesome for test cases and it’s great to have it autogenerated in my tests too.&lt;/p&gt;

</description>
      <category>go</category>
      <category>testing</category>
      <category>tdd</category>
    </item>
    <item>
      <title>Using Terraform to Manage AWS Patch Baselines at Enterprise Scale</title>
      <dc:creator>Joseph Heyburn</dc:creator>
      <pubDate>Sun, 26 Jul 2020 00:00:00 +0000</pubDate>
      <link>https://dev.to/jdheyburn/using-terraform-to-manage-aws-patch-baselines-at-enterprise-scale-2nck</link>
      <guid>https://dev.to/jdheyburn/using-terraform-to-manage-aws-patch-baselines-at-enterprise-scale-2nck</guid>
      <description>&lt;p&gt;If you’re new to AWS and patching principles then continue reading, else you can skip to juicy stuff below.&lt;/p&gt;

&lt;h2&gt;
  
  
  AWS Primer ☁️
&lt;/h2&gt;

&lt;h3&gt;
  
  
  EC2
&lt;/h3&gt;

&lt;p&gt;Amazon Web Services (AWS) provides Cloud resources to those that require it, &lt;a href="https://www.lastweekinaws.com/blog/4-reasons-lyft-is-smart-to-pay-aws-300m/"&gt;for a cost&lt;/a&gt; mind you. One resource in particular is a box-standard server known on AWS as &lt;a href="https://aws.amazon.com/ec2/"&gt;EC2 Instances&lt;/a&gt; which you can secure shell (&lt;a href="https://www.ssh.com/ssh/"&gt;SSH&lt;/a&gt;) into and do whatever you like with it - the world is now officially your oyster! 🦪&lt;/p&gt;

&lt;p&gt;Just like with your personal laptop, computer, mobile phone, etc., you need to patch your servers with the latest security updates to ensure that you are protected from any adversary gaining access to your devices.&lt;/p&gt;

&lt;p&gt;If they were to gain unauthorised access, they can execute whatever they like on there; whether that be sniffing around your network for some sensitive files, or &lt;a href="https://www.cybersecurity-insiders.com/hackers-cyber-attack-amazon-cloud-to-mine-bitcoins/"&gt;mining cryptocurrency&lt;/a&gt; on your paid-for resources. Just like you earlier - your world is now officially &lt;em&gt;their&lt;/em&gt; oyster!&lt;/p&gt;

&lt;p&gt;EC2 instances are operated by you, and as per the &lt;a href="https://aws.amazon.com/compliance/shared-responsibility-model/"&gt;AWS Shared Responsibility Model&lt;/a&gt;, &lt;em&gt;you&lt;/em&gt; are responsible for ensuring the software is kept secured.&lt;/p&gt;

&lt;h3&gt;
  
  
  SSM &amp;amp; Patch Manager
&lt;/h3&gt;

&lt;p&gt;AWS have a service used to help with the administration of EC2 instances called &lt;a href="https://aws.amazon.com/systems-manager/"&gt;Systems Manager (SSM*)&lt;/a&gt;. SSM is a bit of a beast and covers a lot of different functionalitie, going deep on SSM is beyond the scope of this article - so you can read more about it on the &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/what-is-systems-manager.html"&gt;AWS documentation&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;* SSM formerly was an acronym for Simple Systems Manager - I guess it got more complex than they thought!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To help you keep your instances patched, AWS provided the &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-patch.html"&gt;Patch Manager&lt;/a&gt; - within there it contains a number of tools to help with this. These are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/about-patch-baselines.html"&gt;&lt;strong&gt;Patch Baselines&lt;/strong&gt;&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;a policy which filters available patches for your instances to what should be installed on it&lt;/li&gt;
&lt;li&gt;filters which you can apply to a baseline include&lt;/li&gt;
&lt;li&gt;how many days since the patch was released&lt;/li&gt;
&lt;li&gt;the severity and classification of the patch&lt;/li&gt;
&lt;li&gt;or even explicitly deny individual patches if you know they introduce a bug&lt;/li&gt;
&lt;li&gt;e.g. only install security classified patches marked as critical severity which have been released more than 7 days ago&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-patch-patchgroups.html"&gt;&lt;strong&gt;Patch Groups&lt;/strong&gt;&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;a label which joins together patch baselines, to what instances they should be applied to&lt;/li&gt;
&lt;li&gt;they apply to instances as a tag at a key named &lt;code&gt;Patch Group&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


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

&lt;p&gt;To then perform a patch event on an instance, you will need to execute an SSM Document known as &lt;strong&gt;AWS-RunPatchBaseline&lt;/strong&gt; on your target instances.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-ssm-docs.html"&gt;SSM Document&lt;/a&gt; is essentially an automation script that you can perform on one or more instances at a time, with conditions to apply different sets of scripts depending on the operating system (OS) platform (i.e. Windows / Linux).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;AWS-RunPatchBaseline&lt;/strong&gt; is one such example of a document that has both Windows and Linux stages, and it will check and install patches against the patch baseline that has been applied to your instance, via a patch group.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;However if an EC2 instance is not assigned to a patch group, then AWS will pick the &lt;strong&gt;default baseline&lt;/strong&gt; for that instances operating system&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Say you had a security policy of ensuring instances must check for patches once a week - there’s no need to manually execute the SSM Document yourself. &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-maintenance.html"&gt;Maintenance Windows&lt;/a&gt; can help with that. They are essentially a &lt;a href="https://en.wikipedia.org/wiki/Cron#CRON_expression"&gt;cron expression&lt;/a&gt; that you define to then execute a task. In this case we can create a maintenance window to execute the AWS-RunPatchBaseline document on a weekly basis.&lt;/p&gt;

&lt;p&gt;The resulting relationship of all the above looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Maintenance Windows ──┬── invokes ───&amp;gt; SSM Document ─── queries ───&amp;gt; Patch Baseline
                      |
                      |
                      └── targets ───&amp;gt; Patch Groups ─── assigned to ───&amp;gt; EC2 Instances
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h2&gt;
  
  
  Infrastructure as Code Primer
&lt;/h2&gt;

&lt;p&gt;Creating your resources through a web UI such as AWS Console is okay for learning in. But how do you ensure your infrastructure is repeatable across several environments? Not only that, how do you easily keep a history of the state of your application infrastructure?&lt;/p&gt;

&lt;p&gt;This is where &lt;a href="https://en.wikipedia.org/wiki/Infrastructure_as_code"&gt;infrastructure as code&lt;/a&gt; (IAC) comes in. IAC is where all your infrastructure resources are defined in code, and published onto a source code repository of your choosing (GitHub, GitLab, etc.).&lt;/p&gt;

&lt;p&gt;That way you can ensure the code that defines your infrastructure can be repeated across your different application environments, and be able to view history of code changes.&lt;/p&gt;

&lt;p&gt;It also acts as a single source of truth that everyone in your team can depend on for what the application state looks like.&lt;/p&gt;

&lt;p&gt;There are several IAC tools that you can use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://aws.amazon.com/cloudformation/"&gt;CloudFormation&lt;/a&gt; is AWS’s home-grown solution exclusively for AWS services&lt;/li&gt;
&lt;li&gt;There are platform-agnostic tools such as &lt;a href="https://www.terraform.io/"&gt;Terraform&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;written in its own proprietary language HashiCorp Configuration Language (HCL)&lt;/li&gt;
&lt;li&gt;I’ve talked about Terraform before in a &lt;a href="https://jdheyburn.co.uk/blog/on-becoming-an-open-source-software-contributor/#background"&gt;previous post&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Or for a language-agnostic, platform-agnostic tool - &lt;a href="https://www.pulumi.com/why-pulumi/"&gt;Pulumi&lt;/a&gt; is the one for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the case of Terraform, it keeps the current state of your infrastructure stack in a file stored in a shared remote location. This is known as the &lt;a href="https://www.terraform.io/docs/state/index.html"&gt;state file&lt;/a&gt;, and it is used by Terraform to understand what resources Terraform is aware of in your platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Patch Baselines in an Enterprise Environment
&lt;/h2&gt;

&lt;p&gt;When managing Terraform in an enterprise environment, it is a best practice to split up the infrastructure on the structure of the teams working on them, known as workspaces. This is something advised by &lt;a href="https://www.terraform.io/docs/cloud/guides/recommended-practices/part1.html#the-recommended-terraform-workspace-structure"&gt;Terraform themselves&lt;/a&gt;. For example, you would have a workspace for billing, and one for networking. That way you can ensure teams can work effectively without treading on each others’ toes.&lt;/p&gt;

&lt;p&gt;The same principle can be applied to a security team that defines security policies, who will also write up patching policies. These policies will be followed by teams working in different workspaces to understand at minimum what patches should be installed on a server, and how quickly to install them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Check out the University of Exeter’s own &lt;a href="https://www.exeter.ac.uk/media/level1/academicserviceswebsite/it/recordsmanagementservice/policydocuments/Patch_Management_Policy_FINAL.pdf"&gt;patching policy&lt;/a&gt; for an example.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example, an enterprise may have a patching policy that states all patches with a severity marked as ‘Critical’ or ‘Important’ must be installed within 14 days of its release. If an out-of-band (OOB) patch is released, then it must be installed within 3 days.&lt;/p&gt;

&lt;p&gt;These folk are the same lot who will also conjure up the &lt;strong&gt;Patch Baselines&lt;/strong&gt; we learned about earlier.&lt;/p&gt;

&lt;p&gt;They may deploy these patch baselines in a different Terraform workspace from yours. If that were the case, then if you wanted to refer back to these in your Terraform workspace, then the &lt;a href="https://www.terraform.io/docs/providers/terraform/d/remote_state.html"&gt;&lt;code&gt;remote_state&lt;/code&gt;&lt;/a&gt; resource is something you might need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# This example references a subnet_id created in another workspace&lt;/span&gt;
&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"terraform_remote_state"&lt;/span&gt; &lt;span class="s2"&gt;"vpc"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"remote"&lt;/span&gt;

  &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;organization&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp"&lt;/span&gt;
    &lt;span class="nx"&gt;workspaces&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"vpc-prod"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_instance"&lt;/span&gt; &lt;span class="s2"&gt;"foo"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;terraform_remote_state&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputs&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subnet_id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;So the above works if all parties are using Terraform workspaces. But what if you’re using Terraform, and the security team (who are managing patch baselines) are using something different like the mentioned CloudFormation or Pulumi.&lt;/p&gt;

&lt;p&gt;If you are creating &lt;strong&gt;patch groups&lt;/strong&gt; in Terraform - you won’t be able to reference the &lt;strong&gt;patch baselines&lt;/strong&gt; they’ve created, because they are in a different state file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_patch_group"&lt;/span&gt; &lt;span class="s2"&gt;"front_end_servers"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;baseline_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ssm_patch_baseline&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;front_end_servers&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="c1"&gt;# Terraform does not know about this resource!&lt;/span&gt;
  &lt;span class="nx"&gt;patch_group&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"front_end_servers"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;You could of course replicate the patch baseline they’ve created in your Terraform code, but then that does not scale in an enterprise.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_patch_baseline"&lt;/span&gt; &lt;span class="s2"&gt;"front_end_servers"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"front-end-servers"&lt;/span&gt;

  &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;# variables removed for brevity&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_patch_group"&lt;/span&gt; &lt;span class="s2"&gt;"front_end_servers"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;baseline_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ssm_patch_baseline&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;front_end_servers&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="c1"&gt;# Not good!&lt;/span&gt;
  &lt;span class="nx"&gt;patch_group&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"front_end_servers"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This is what we want to avoid.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Sources For Patch Baselines
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.terraform.io/docs/configuration/data-sources.html"&gt;Data sources&lt;/a&gt; in Terraform allow you to pull resources in your platform that may exist in a separate state file to yours, or even a completely different IAC tool to Terraform, such as CloudFormation. What we need is a data source component for the &lt;code&gt;aws_ssm_patch_baseline&lt;/code&gt; resource.&lt;/p&gt;

&lt;p&gt;With a &lt;a href="https://github.com/terraform-providers/terraform-provider-aws/pull/9486"&gt;pull request&lt;/a&gt; I made to the &lt;a href="https://github.com/terraform-providers/terraform-provider-aws"&gt;terraform-aws-provider&lt;/a&gt; project - I added the ability to pull in &lt;strong&gt;patch baseline&lt;/strong&gt; resources that exist in the AWS account you are targeting, meaning if the enterprise security team had deployed a patch baseline via a different stack, then you can reference that in your &lt;strong&gt;patch group&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# This resource is defined outside of the current working Terraform state&lt;/span&gt;
&lt;span class="c1"&gt;# So we are making a call to retrieve the ID of the resource in AWS&lt;/span&gt;
&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_patch_baseline"&lt;/span&gt; &lt;span class="s2"&gt;"front_end_servers"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;owner&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Self"&lt;/span&gt;
  &lt;span class="nx"&gt;name_prefix&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"FrontEndServers"&lt;/span&gt;
  &lt;span class="nx"&gt;operating_system&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"CENTOS"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_patch_group"&lt;/span&gt; &lt;span class="s2"&gt;"front_end_servers"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;baseline_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_ssm_patch_baseline&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;front_end_servers&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;patch_group&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"front_end_servers"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_instance"&lt;/span&gt; &lt;span class="s2"&gt;"front_end_server"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;# variables removed for brevity&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"Patch Group"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ssm_patch_group&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;front_end_servers&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The documentation for its usage can be found &lt;a href="https://www.terraform.io/docs/providers/aws/d/ssm_patch_baseline.html"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Another scenario might be where you want to reuse the &lt;strong&gt;patch baselines&lt;/strong&gt; that AWS has created in your account. One such baseline is &lt;code&gt;AWS-WindowsPredefinedPatchBaseline-OS-Applications&lt;/code&gt;, which patches both the Windows OS, and selected Microsoft applications installed on the Windows server.&lt;/p&gt;

&lt;p&gt;Currently this is not the default baseline for Windows. So if you want to have this baseline assigned to a patch group, you could do something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_patch_baseline"&lt;/span&gt; &lt;span class="s2"&gt;"windows_predefined_os_and_apps"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;owner&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS"&lt;/span&gt;
  &lt;span class="nx"&gt;name_prefix&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS-WindowsPredefinedPatchBaseline-OS-Applications"&lt;/span&gt;
  &lt;span class="nx"&gt;operating_system&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"WINDOWS"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_patch_group"&lt;/span&gt; &lt;span class="s2"&gt;"active_directory"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;baseline_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_ssm_patch_baseline&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;windows_predefined_os_and_apps&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;patch_group&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"active_directory"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_instance"&lt;/span&gt; &lt;span class="s2"&gt;"active_directory"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;# variables removed for brevity&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"Patch Group"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ssm_patch_group&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;active_directory&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



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

&lt;p&gt;This is a relatively short post - but it was just a quick introduction to AWS Patch Manager. I hope you find it helpful in applying your enterprise patching strategy to your product teams!&lt;/p&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>ssm</category>
    </item>
    <item>
      <title>Reverse Proxy Multiple Domains Using Caddy 2</title>
      <dc:creator>Joseph Heyburn</dc:creator>
      <pubDate>Wed, 10 Jun 2020 00:00:00 +0000</pubDate>
      <link>https://dev.to/jdheyburn/reverse-proxy-multiple-domains-using-caddy-2-3497</link>
      <guid>https://dev.to/jdheyburn/reverse-proxy-multiple-domains-using-caddy-2-3497</guid>
      <description>&lt;p&gt;During lockdown, I’ve spent a bit of time improving our home network. The bigger picture of which I’ll write about in a future post. But for now, I came across some challenges with running &lt;a href="https://caddyserver.com/" rel="noopener noreferrer"&gt;Caddy 2&lt;/a&gt; as a reverse proxy for multiple domains used internally.&lt;/p&gt;

&lt;p&gt;If you’ve stumbled across this looking for the end config file for Caddy, then you can skip there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;A few months back I kitted out my home with some &lt;a href="https://www.ui.com/products/#unifi" rel="noopener noreferrer"&gt;Ubiquiti UniFi&lt;/a&gt; gear to fix our crappy Wifi at home, following inspiration from &lt;a href="https://www.troyhunt.com/ubiquiti-all-the-things-how-i-finally-fixed-my-dodgy-wifi/" rel="noopener noreferrer"&gt;Troy Hunt&lt;/a&gt; and &lt;a href="https://scotthelme.co.uk/my-ubiquiti-home-network/" rel="noopener noreferrer"&gt;Scott Helme&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In order to administrate UniFi devices, you’ll need the &lt;a href="https://www.ui.com/unifi/unifi-cloud-key/" rel="noopener noreferrer"&gt;UniFi Cloud Key&lt;/a&gt; which runs the Controller software to do just that. Although if you have a spare Raspberry Pi lying around, you can download the &lt;a href="https://www.ui.com/download/unifi/" rel="noopener noreferrer"&gt;software&lt;/a&gt; for free and run it on there - this is what I did.&lt;/p&gt;

&lt;p&gt;I’ve also wanted to protect my home network with a self-hosted DNS server, such as &lt;a href="https://pi-hole.net/" rel="noopener noreferrer"&gt;PiHole&lt;/a&gt;. I won’t go into depth about how that was done, but you can follow &lt;a href="https://scotthelme.co.uk/securing-dns-across-all-of-my-devices-with-pihole-dns-over-https-1-1-1-1/" rel="noopener noreferrer"&gt;Scott Helme’s guide&lt;/a&gt; on how you can set the same up.&lt;/p&gt;

&lt;p&gt;Both of these services can be accessed through web browsers at the IP address and ports where they are being hosted, such as &lt;code&gt;http://192.168.1.10:8093/admin/&lt;/code&gt; in the case of PiHole. Having to remember the IP address and the port can be a pain. We can front these services with a rememberable domain name which points to these services - of which I’ve written about in a &lt;a href="https://jdheyburn.co.uk/blog/who-goes-blogging-2-custom-domain/" rel="noopener noreferrer"&gt;previous post&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Securing with HTTPS
&lt;/h3&gt;

&lt;p&gt;The web is evolving, and there is no reason why we should access services via insecure HTTP, that includes services that are only running on an internal network such as a home network. Web browsers nowadays give you a warning when you are connecting to website over an unencrypted connection.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Freverse-proxy-multiple-domains-using-caddy-2%2Finsecure-pihole.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Freverse-proxy-multiple-domains-using-caddy-2%2Finsecure-pihole.png" alt="Insecure PiHole connection"&gt;&lt;/a&gt;Simply accessing over HTTP is not an option, when browsers present us with a huge warning message&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;a href="https://caddyserver.com/" rel="noopener noreferrer"&gt;Caddy&lt;/a&gt; is a web server similar to Apache, nginx, et al., but it is different in that it enables HTTPS by default and upgrades requests from HTTP to HTTPS. Managing certificates for HTTPS is a pain - so Caddy does that too, so long as you can prove you own the domain you are hosting requests at. We can use Caddy in a reverse proxy mode, allowing us to access services at endpoints such as &lt;code&gt;https://pihole.domain.local&lt;/code&gt; in our browsers and forward them to the corresponding IP address hosting the service.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A &lt;a href="https://www.cloudflare.com/learning/cdn/glossary/reverse-proxy/" rel="noopener noreferrer"&gt;reverse proxy&lt;/a&gt; is a service that simply forwards client requests onto the server on the clients behalf.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Proving Domain Ownership
&lt;/h2&gt;

&lt;p&gt;Caddy uses &lt;a href="https://letsencrypt.org/" rel="noopener noreferrer"&gt;Let’s Encrypt&lt;/a&gt; (LE) to provide certificates for domains. Since domains can be exposed publicly, we will have to prove ownership of the domain to have LE issue certificates on our behalf - so we’ll have to purchase the domain from a registrar. I talked about how to do this for this website &lt;a href="https://jdheyburn.co.uk/blog/who-goes-blogging-2-custom-domain/#acquire-a-domain" rel="noopener noreferrer"&gt;in the past&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;LE supports several &lt;a href="https://letsencrypt.org/docs/challenge-types/" rel="noopener noreferrer"&gt;challenge methods&lt;/a&gt; in order to prove you own the domain. This helps mitigates attacks by adversaries by claiming they own a domain such as &lt;code&gt;natwest.co.uk&lt;/code&gt; - allowing them to create phishing attacks and steal banking information.&lt;/p&gt;

&lt;p&gt;Since my network is only visible internally for the moment (i.e. the domain will only resolve to an IP address on my network) - I cannot use HTTP or TLS since these require the domain to resolve to a public IP address to a web server hosting a challenge file requested by LE. Therefore the only option I have is DNS challenge, where a randomly string generated by LE is placed into the &lt;a href="https://www.cloudflare.com/learning/dns/dns-records/dns-txt-record/" rel="noopener noreferrer"&gt;TXT record&lt;/a&gt; of a DNS record to confirm ownership.&lt;/p&gt;
&lt;h2&gt;
  
  
  Building Our Caddy
&lt;/h2&gt;

&lt;p&gt;For this exercise I’ll be using the latest version, Caddy 2, which allows for plugins to be built into the binary depending on your use case - including &lt;a href="https://caddyserver.com/docs/automatic-https#dns-challenge" rel="noopener noreferrer"&gt;DNS challenge&lt;/a&gt;. This plugin isn’t included by default, so we’ll need to build our own Caddy binary. The tool to do this is called &lt;a href="https://github.com/caddyserver/xcaddy" rel="noopener noreferrer"&gt;xcaddy&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;UPDATE 2020-09-30&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Looks like Caddy now comes with a &lt;a href="https://caddyserver.com/download" rel="noopener noreferrer"&gt;nice web interface&lt;/a&gt; for downloading a Caddy binary with whatever plugins you desire. I just tested out the &lt;code&gt;Linux arm 7&lt;/code&gt; platform with just the &lt;code&gt;github.com/caddy-dns/cloudflare&lt;/code&gt; plugin, and it worked as intended.&lt;/p&gt;

&lt;p&gt;Once you've got the binary downloaded, copy it to the Pi then skip to Caddy Configuration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To build using xcaddy, you need to make sure you have &lt;a href="https://golang.org/doc/install" rel="noopener noreferrer"&gt;Go installed&lt;/a&gt; on your machine.&lt;/p&gt;

&lt;p&gt;Note that I am building Caddy on my laptop, but running it on a Pi, so I will have to specify the architecture that Pi is running on so that Go can correctly build it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Download xcaddy&lt;/span&gt;
go get &lt;span class="nt"&gt;-u&lt;/span&gt; github.com/caddyserver/xcaddy/cmd/xcaddy

&lt;span class="c"&gt;# Build custom Caddy binary for Raspberry Pi&lt;/span&gt;
&lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;linux &lt;span class="nv"&gt;GOARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;arm &lt;span class="nv"&gt;GOARM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;7 xcaddy build &lt;span class="nt"&gt;--with&lt;/span&gt; github.com/caddy-dns/cloudflare

&lt;span class="c"&gt;# Copy the new binary across to the Pi&lt;/span&gt;
scp caddy pi:/home/pi/caddy/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Caddy Configuration
&lt;/h2&gt;

&lt;p&gt;The configuration I’m using can be seen below. Some things to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I’m using Cloudflare as the DNS name servers for the domain, even though I purchased my domain from namecheap

&lt;ul&gt;
&lt;li&gt;This repeats an &lt;a href="https://jdheyburn.co.uk/blog/who-goes-blogging-2-custom-domain/#adding-our-cdn-layer" rel="noopener noreferrer"&gt;exercise I’ve done previously&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;I’ve done this for two reasons:&lt;/li&gt;
&lt;li&gt;Caddy at the time of writing does not have a namecheap DNS challenge plugin&lt;/li&gt;
&lt;li&gt;It’s a proven method I know already&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;CLOUDFLARE_API_TOKEN&lt;/code&gt; is required to have Caddy set the TXT record DNS challenge received from LE

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://support.cloudflare.com/hc/en-us/articles/200167836-Managing-API-Tokens-and-Keys" rel="noopener noreferrer"&gt;Guide for the same&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Caddy is reverse proxying traffic to services running locally on the Pi&lt;/li&gt;
&lt;li&gt;Caddy is not verifying the certificate being hosted by the UniFi Controller (&lt;code&gt;insecure_skip_verify = true&lt;/code&gt;)

&lt;ul&gt;
&lt;li&gt;The controller self-signs a certificate, and the reverse proxy has no means of establishing a chain of trust to verify the certificate&lt;/li&gt;
&lt;li&gt;It’s not a best practice to not verify the chain of trust, however I’m happy to accept the risk for now&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://caddyserver.com/docs/json/" rel="noopener noreferrer"&gt;Click here&lt;/a&gt; to see documentation on Caddy JSON config files.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;



&lt;p&gt;Remember that the domain names aren’t actually publicly accessible. At a basic level we can update the &lt;code&gt;/etc/hosts&lt;/code&gt; file of the machine we’re running on to add a record telling our machine how to resolve the domain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"echo &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;192.168.1.10 pihole.joannet.casa&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;192.168.1.10 unifi.joannet.casa&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &amp;gt;&amp;gt; /etc/hosts"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, we’re already using PiHole as our own DNS server right? We can add the records there instead.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Freverse-proxy-multiple-domains-using-caddy-2%2Fpihole-dns-records.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Freverse-proxy-multiple-domains-using-caddy-2%2Fpihole-dns-records.png" alt="Adding domain records to DNS server"&gt;&lt;/a&gt;PiHole let’s you specify where local domain names should resolve to&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;The IP addresses you see above are pointing to the host running Caddy, the Raspberry Pi.&lt;/p&gt;
&lt;h2&gt;
  
  
  Verifying Caddy
&lt;/h2&gt;

&lt;p&gt;Once the config file is built, you can perform a test run to confirm everything is working by executing this command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; ./caddy run &lt;span class="nt"&gt;--config&lt;/span&gt; config.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;We need to execute using &lt;code&gt;sudo&lt;/code&gt; so that we can expose the service to restricted ports 80 and 443 (HTTP and HTTPS respectively).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Freverse-proxy-multiple-domains-using-caddy-2%2Fproxied-pihole.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Freverse-proxy-multiple-domains-using-caddy-2%2Fproxied-pihole.png" alt="PiHole appearing in browser through a domain name"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Freverse-proxy-multiple-domains-using-caddy-2%2Fproxied-unifi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Freverse-proxy-multiple-domains-using-caddy-2%2Fproxied-unifi.png" alt="UniFi Controller appearing in browser through a domain name"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now we have a memorable domain name fronting the service, and Firefox is happy that we’re encrypting the connection too. The certificate being produced in seen below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Freverse-proxy-multiple-domains-using-caddy-2%2Fpihole-certificate.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Freverse-proxy-multiple-domains-using-caddy-2%2Fpihole-certificate.png" alt="Certificate used by PiHole"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Enabling Caddy Service
&lt;/h2&gt;

&lt;p&gt;Since we’re not using the standard Caddy installation method, we will need to specify a service unit file so that Caddy starts up at the same time as the host - which is what PiHole and UniFi are doing currently.&lt;/p&gt;

&lt;p&gt;First check to see if there is a stale service there already.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /etc/systemd/system/caddy.service
lrwxrwxrwx 1 root root 9 Jun 4 09:14 /etc/systemd/system/caddy.service -&amp;gt; /dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you get the above then remove the symlink so that we can create a file there.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; /etc/systemd/system/caddy.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then populate the same file with the below, remembering the change the location of the Caddy config file to where it exists on your machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;Unit]
&lt;span class="nv"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Caddy Reverse Proxy
&lt;span class="nv"&gt;Wants&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;network-online.target
&lt;span class="nv"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;network.target network-online.target

&lt;span class="o"&gt;[&lt;/span&gt;Service]
&lt;span class="nv"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/local/bin/caddy run &lt;span class="nt"&gt;--config&lt;/span&gt; /home/jdheyburn/homelab/caddy/config.json
&lt;span class="nv"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;on-abort

&lt;span class="o"&gt;[&lt;/span&gt;Install]
&lt;span class="nv"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;multi-user.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finalise the new service with the two commands, enabling it on host startup and starting the service right now.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;caddy.service
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start caddy.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;For now I have all the above running bare-metal on one Pi instance, which produces a huge single point of failure in my network. In the future I’d like to see how converting these to Docker containers and having them distributed on multiple Pis would increase the resiliency of these services.&lt;/p&gt;

&lt;p&gt;Until then, these basic but essential services are being hosted at easy to remember domains, transported over an encrypted connection, for me to easily administer the network for when it gets more complex over time.&lt;/p&gt;

</description>
      <category>caddy</category>
      <category>rpi</category>
      <category>pihole</category>
    </item>
    <item>
      <title>Three Steps to Improve Hugo's RSS Feeds</title>
      <dc:creator>Joseph Heyburn</dc:creator>
      <pubDate>Wed, 10 Jun 2020 00:00:00 +0000</pubDate>
      <link>https://dev.to/jdheyburn/three-steps-to-improve-hugo-s-rss-feeds-58ob</link>
      <guid>https://dev.to/jdheyburn/three-steps-to-improve-hugo-s-rss-feeds-58ob</guid>
      <description>&lt;p&gt;&lt;em&gt;This was originally posted on my &lt;a href="https://jdheyburn.co.uk/blog/who-goes-blogging-6-three-steps-to-improve-hugos-rss-feeds/" rel="noopener noreferrer"&gt;personal blog&lt;/a&gt; from my Who Goes Blogging series - documenting my journey on setting up my Hugo-based portfolio website.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This walkthrough will be especially helpful if you have your own Hugo website, and want to cross-post across to dev.to using the &lt;a href="https://dev.to/settings/publishing-from-rss"&gt;Publishing from RSS&lt;/a&gt; feature.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I “fixed” the default RSS template used by Hugo to show the full article content, along with images, and also talk about how to have social media cards appear in the RSS items too.&lt;/p&gt;

&lt;p&gt;I uploaded my completed RSS file to &lt;a href="https://gist.github.com/jdheyburn/a0a2c678f8f9795088b2779ec6af9920" rel="noopener noreferrer"&gt;GitHub Gist&lt;/a&gt; 🚀&lt;/p&gt;

&lt;h2&gt;
  
  
  What is RSS?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/RSS" rel="noopener noreferrer"&gt;RSS&lt;/a&gt; is a great way to “subscribe” to websites to ensure that you don’t miss content from them. The standard for how it is generated has been the same for decades now - however its still the best supported and most accepted way to receive updates. It is simply a standardised XML file that allows consumers such as RSS aggregators to parse the content of the post - for which there are many to choose from (RIP &lt;a href="https://en.wikipedia.org/wiki/Google_Reader" rel="noopener noreferrer"&gt;Google Reader&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Fwho-goes-blogging-6-three-steps-to-improve-hugos-rss-feeds%2Finoreader-complete.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Fwho-goes-blogging-6-three-steps-to-improve-hugos-rss-feeds%2Finoreader-complete.png" alt="inoreader as an RSS feed aggregator"&gt;&lt;/a&gt;My RSS aggregator of choice currently is &lt;a href="https://www.inoreader.com/" rel="noopener noreferrer"&gt;inoreader&lt;/a&gt; - I’ll be using this in my examples&lt;/p&gt;

&lt;p&gt;Hugo &lt;a href="https://gohugo.io/templates/rss/" rel="noopener noreferrer"&gt;generates RSS XML&lt;/a&gt; files from a template that will loop over your content and expose this at an endpoint for RSS aggregators to subscribe to and periodically check for updates against. Hugo generates a bunch of RSS XML files at different sections of the website, allowing consumers to subscribe to the section that interests them most - you can even subscribe to tags if that XML is being generated!&lt;/p&gt;

&lt;p&gt;Usually there is site top-level available at &lt;code&gt;/index.xml&lt;/code&gt; - in the case of my site that would be &lt;a href="https://jdheyburn.co.uk/index.xml" rel="noopener noreferrer"&gt;https://jdheyburn.co.uk/index.xml&lt;/a&gt;. The source code for the template file Hugo uses to generate this is embedded in Hugo at &lt;a href="https://github.com/gohugoio/hugo/blob/master/tpl/tplimpl/embedded/templates/_default/rss.xml" rel="noopener noreferrer"&gt;this location&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Some (but not all) websites only include the first paragraph of their post in an RSS update (aka summary), along with a message after that paragraph requesting the reader to visit the site in a browser to view the rest of the content.&lt;/p&gt;

&lt;p&gt;This is a common feature of WordPress blogs - and it is done to have you load the full website and along with it, all the code for providing the owner with user analytics, and advertising - if they have it. So really they’re getting the benefit of being able to publish updates via a standardised approach (RSS), to then lure you onto the website.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Fwho-goes-blogging-6-three-steps-to-improve-hugos-rss-feeds%2Frss-post-show-more-content.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Fwho-goes-blogging-6-three-steps-to-improve-hugos-rss-feeds%2Frss-post-show-more-content.png" alt="An RSS post loading incompletely"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hugo &lt;em&gt;kind of&lt;/em&gt; does this through the highlighted line in the &lt;a href="https://github.com/gohugoio/hugo/blob/master/tpl/tplimpl/embedded/templates/_default/rss.xml" rel="noopener noreferrer"&gt;templated RSS XML file&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Summary&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;.Summary&lt;/code&gt; will do what was just described, print out the first paragraph of your post. However unlike WordPress, it doesn’t actually say if there is more content. So users may not load your full website, instead thinking you’ve produced a very short article!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Fwho-goes-blogging-6-three-steps-to-improve-hugos-rss-feeds%2Fhugo-incomplete-post.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Fwho-goes-blogging-6-three-steps-to-improve-hugos-rss-feeds%2Fhugo-incomplete-post.png" alt="Hugo producing a summarised RSS post"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’m not against the practice of previewing the post, if a blog provides revenue for the author(s) then you will need to force readers to view the full website for analytics and advertising (businesses gotta make money!).&lt;/p&gt;

&lt;p&gt;Although I’m sure the initial purpose of RSS was only meant to be used to provide subscribers a pure text version of your content. At the end of the day, I believe the author should give the reader choice on mediums of consumption - and they accept the limitations that come from RSS.&lt;/p&gt;

&lt;p&gt;So now, I’ll be making several changes to the Hugo template RSS XML file to produce the full site content, along with some other enhancements.&lt;/p&gt;
&lt;h2&gt;
  
  
  Rendering Full Site Content
&lt;/h2&gt;

&lt;p&gt;Before we start editing files, we need to produce our own copy of the template RSS XML file. This is embedded directly within Hugo itself and not packaged in with your theme.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There’s every chance your theme is generating one for you. Run the below command to see if there is anything for you to copy from.&lt;br&gt;
&lt;code&gt;find themes/ -type f -name '*.xml'&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We’ve discussed template lookup orders before in &lt;a href="https://jdheyburn.co.uk/blog/who-goes-blogging-5-updates-galore/#template-lookup-order-primer" rel="noopener noreferrer"&gt;previous posts&lt;/a&gt; - we can overwrite the default of any file used in generating the website by placing it in your local project root.&lt;/p&gt;

&lt;p&gt;We can start to make modifications to the &lt;code&gt;rss.xml&lt;/code&gt; file by copying the &lt;a href="https://github.com/gohugoio/hugo/blob/master/tpl/tplimpl/embedded/templates/_default/rss.xml" rel="noopener noreferrer"&gt;default&lt;/a&gt; into the directory where Hugo will read it from.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Assuming you are in project root&lt;/span&gt;
wget https://raw.githubusercontent.com/gohugoio/hugo/master/tpl/tplimpl/embedded/templates/_default/rss.xml &lt;span class="nt"&gt;-O&lt;/span&gt; layouts/_default/rss.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We know that line 35 containing &lt;code&gt;.Summary&lt;/code&gt; is causing the issue here. All we need to do is change it to &lt;code&gt;.Content&lt;/code&gt; - then Hugo will print out the entire content of your post. See the highlighted line below for the change.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Permalink&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;pubDate&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Format&lt;/span&gt; &lt;span class="s"&gt;"Mon, 02 Jan 2006 15:04:05 -0700"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;safeHTML&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;pubDate&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Site&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Author&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;}}{{&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Site&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Author&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="p"&gt;({{&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;}}){{&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;guid&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Permalink&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;guid&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
 &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
 &lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;rss&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Let’s now view the changes in the RSS reader.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Fwho-goes-blogging-6-three-steps-to-improve-hugos-rss-feeds%2Fhugo-complete-post.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Fwho-goes-blogging-6-three-steps-to-improve-hugos-rss-feeds%2Fhugo-complete-post.png" alt="Hugo RSS post displaying all content"&gt;&lt;/a&gt;We can now see much more content is being produced - and with proper HTML this time! &lt;/p&gt;
&lt;h2&gt;
  
  
  Rendering images in RSS posts
&lt;/h2&gt;

&lt;p&gt;Depending on how you are retrieving images - you may find that they are not displaying, and that the image alterative (&lt;code&gt;alt&lt;/code&gt;) text is being displayed instead.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Fwho-goes-blogging-6-three-steps-to-improve-hugos-rss-feeds%2Fhugo-missing-images-post.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Fwho-goes-blogging-6-three-steps-to-improve-hugos-rss-feeds%2Fhugo-missing-images-post.png" alt="Hugo RSS post with missing images, alt text displayed instead"&gt;&lt;/a&gt;Note the highlighted alt texts of images&lt;br&gt;
&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you’re not already providing an &lt;code&gt;alt&lt;/code&gt; field to your &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; blocks then &lt;strong&gt;you absolutely need to&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;They help boost accessibility to your website should the user have a text-to-voice application reading your website.&lt;/p&gt;

&lt;p&gt;They also provide a helpful description to your image in case it cannot load - which happens more often than you think!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Why it is breaking 😤
&lt;/h3&gt;

&lt;p&gt;In &lt;a href="https://jdheyburn.co.uk/blog/who-goes-blogging-4-content-structure-and-refactoring/" rel="noopener noreferrer"&gt;part 4&lt;/a&gt; of this series, we looked at placing resources for a post in the same directory as the content markdown file (also known as &lt;a href="https://gohugo.io/content-management/page-bundles/#leaf-bundles" rel="noopener noreferrer"&gt;leaf bundles&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;That means for a given directory tree structure:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;tree content/blog
content/blog
└── some-blog-post
├── image-file-name.png
└── index.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We would use the following shortcode to generate the &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; block in &lt;code&gt;index.md&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;figure&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"image-file-name.png"&lt;/span&gt; &lt;span class="n"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Here is an image!"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This produces the block:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;figure&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"image-file-name.png"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Here is an image!"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/figure&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Since this contains no preceding forward-slash &lt;code&gt;/&lt;/code&gt; in &lt;code&gt;src&lt;/code&gt;, this becomes a &lt;em&gt;relative path&lt;/em&gt;. This means the webserver will look for the image at the path relative to the current page.&lt;/p&gt;

&lt;p&gt;So when we navigate to &lt;code&gt;blog/some-blog-post/&lt;/code&gt; in our web browser (as dictated from the above tree structure), your browser will request the image at &lt;code&gt;blog/some-blog-post/image-file-name.png&lt;/code&gt; for you.&lt;/p&gt;

&lt;p&gt;Why is this relevant? This same &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; block is being rendered in the description of your RSS XML file, including the relative image location. RSS is very simple and renders only complete URLs (e.g. &lt;code&gt;https://jdheyburn.co.uk/blog/some-blog-post/image-file-name.png&lt;/code&gt;) - so when we just specify the relative path, it cannot find the image and thus prints out the &lt;code&gt;alt&lt;/code&gt; text.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; if you specify images from a complete path such as &lt;code&gt;/images/image-file-name.png&lt;/code&gt; under &lt;code&gt;static/images&lt;/code&gt;, then this is not an issue for you.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Fix image rendering
&lt;/h3&gt;

&lt;p&gt;The fix for this involves some Go magic. We need to prepend the permalink for the current post that’s being printed out to anything that contains a relative path &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; block.&lt;/p&gt;

&lt;p&gt;We need to go back to the same line where we added &lt;code&gt;.Content&lt;/code&gt; and change it to:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;replaceRE&lt;/span&gt; &lt;span class="s"&gt;"img src=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;(.*?)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;printf&lt;/span&gt; &lt;span class="s"&gt;"%s%s%s"&lt;/span&gt; &lt;span class="s"&gt;"img src=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Permalink&lt;/span&gt; &lt;span class="s"&gt;"$1&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This calls the Hugo function &lt;a href="https://gohugo.io/functions/replacere/" rel="noopener noreferrer"&gt;replaceRE&lt;/a&gt; which enables us to perform a find-and-replace using &lt;a href="https://www.regular-expressions.info/tutorial.html" rel="noopener noreferrer"&gt;regular expressions (regex)&lt;/a&gt;, and it takes three inputs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PATTERN&lt;/code&gt; is the regex pattern used to find the text we want to remove

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;"img src=\"(.*?)\""&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;REPLACEMENT&lt;/code&gt; is the text that we want to replace the &lt;code&gt;PATTERN&lt;/code&gt; with

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;(printf "%s%s%s" "img src=\"" .Permalink "$1\"")&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;INPUT&lt;/code&gt; is the string we want to perform the find-and-replace on

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.Content&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the example above, we’re placing a &lt;a href="https://www.regular-expressions.info/brackets.html" rel="noopener noreferrer"&gt;capturing group&lt;/a&gt; in the &lt;code&gt;PATTERN&lt;/code&gt; so that it will capture the contents inside &lt;code&gt;src&lt;/code&gt; and save them so that we can refer to it in the &lt;code&gt;REPLACEMENT&lt;/code&gt;, while removing &lt;code&gt;img src=""&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;REPLACEMENT&lt;/code&gt; is calling another Hugo function called &lt;a href="https://gohugo.io/functions/printf/" rel="noopener noreferrer"&gt;printf&lt;/a&gt; to construct the replacement text. We’re pretty much just injecting the page &lt;code&gt;.Permalink&lt;/code&gt; prior to the captured text from the &lt;code&gt;PATTERN&lt;/code&gt; - so that RSS can then display the complete URL. While adding back the &lt;code&gt;img src=""&lt;/code&gt; that was removed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;INPUT&lt;/code&gt; will be the page &lt;code&gt;.Content&lt;/code&gt;, what we were printing out before.&lt;/p&gt;

&lt;p&gt;Once we’ve made the change, images are now rendered properly!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Fwho-goes-blogging-6-three-steps-to-improve-hugos-rss-feeds%2Fhugo-images-post.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Fwho-goes-blogging-6-three-steps-to-improve-hugos-rss-feeds%2Fhugo-images-post.png" alt="Hugo RSS post with images being correctly rendered"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;UPDATE&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Upon posting this, I noticed I needed to do the same thing for hyperlinks referencing headings with the same post. For instance the markdown below which renders a hyperlink to a heading…&lt;/p&gt;


&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;images to display in RSS content&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;#rendering-images-in-rss-posts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Will render this in RSS:&lt;/p&gt;


&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://jdheyburn.co.uk/#rendering-images-in-rss-posts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Which does not take the reader to the right location at all. What we need to do is repeat the regex used above in &lt;code&gt;rss.xml&lt;/code&gt; to catch these and replace them with the complete URL. See below for the snippet.&lt;/p&gt;


&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;replaceRE&lt;/span&gt; &lt;span class="s"&gt;"a href=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;(#.*?)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;printf&lt;/span&gt; &lt;span class="s"&gt;"%s%s%s"&lt;/span&gt; &lt;span class="s"&gt;"a href=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Permalink&lt;/span&gt; &lt;span class="s"&gt;"$1&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;replaceRE&lt;/span&gt; &lt;span class="s"&gt;"img src=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;(.*?)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;printf&lt;/span&gt; &lt;span class="s"&gt;"%s%s%s"&lt;/span&gt; &lt;span class="s"&gt;"img src=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Permalink&lt;/span&gt; &lt;span class="s"&gt;"$1&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Adding cards for posts
&lt;/h2&gt;

&lt;p&gt;Not dissimilar to &lt;a href="https://barkersocial.com/social-cards/" rel="noopener noreferrer"&gt;social media cards&lt;/a&gt;, you can also have cards appear for your RSS posts in order to make them more attractive for readers to click on!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Fwho-goes-blogging-6-three-steps-to-improve-hugos-rss-feeds%2Finoreader-enclosure-card-view.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Fwho-goes-blogging-6-three-steps-to-improve-hugos-rss-feeds%2Finoreader-enclosure-card-view.png" alt="inoreader displaying images for each post when in card view"&gt;&lt;/a&gt;While in card view for inoreader, we can see featured images being displayed&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;You can do these in RSS via the &lt;a href="https://www.w3schools.com/xml/rss_tag_enclosure.asp" rel="noopener noreferrer"&gt;enclosure&lt;/a&gt; tag…&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;enclosure&lt;/span&gt; &lt;span class="na"&gt;url=&lt;/span&gt;&lt;span class="s"&gt;"image-location.png"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/jpg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/enclosure&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If you have a look at our &lt;code&gt;layouts/_default/rss.xml&lt;/code&gt; file, we don’t have this defined. We could go and add it in now, but I would like a unified social media image to be used across all platforms. This will enable me to focus my effort on generating a single social media card image that is shared across all methods of consumption.&lt;/p&gt;

&lt;p&gt;Based on the above, my theme (hugo-coder), renders the tags required for social media cards through the &lt;a href="https://github.com/gohugoio/hugo/blob/master/tpl/tplimpl/embedded/templates/twitter_cards.html" rel="noopener noreferrer"&gt;internal template&lt;/a&gt; for &lt;a href="https://gohugo.io/templates/internal/#use-the-twitter-cards-template" rel="noopener noreferrer"&gt;twitter_cards&lt;/a&gt;. A snippet of the logic that determines this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;images&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"twitter:card"&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"summary_large_image"&lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"twitter:image"&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"{{ index . 0 | absURL }}"&lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;images&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resources&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ByType&lt;/span&gt; &lt;span class="s"&gt;"image"&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;featured&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;images&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetMatch&lt;/span&gt; &lt;span class="s"&gt;"*feature*"&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;not&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;featured&lt;/span&gt; &lt;span class="p"&gt;}}{{&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;featured&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;images&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetMatch&lt;/span&gt; &lt;span class="s"&gt;"{*cover*,*thumbnail*}"&lt;/span&gt; &lt;span class="p"&gt;}}{{&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;featured&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"twitter:card"&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"summary_large_image"&lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"twitter:image"&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"{{ $featured.Permalink }}"&lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Site&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;images&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"twitter:card"&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"summary_large_image"&lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"twitter:image"&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"{{ index . 0 | absURL }}"&lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="o"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The hierarchy of images used to render the social media card goes like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The first entry in the list of &lt;code&gt;images&lt;/code&gt; found in the post front matter&lt;/li&gt;
&lt;li&gt;Any image in the post leaf bundle containing &lt;code&gt;feature&lt;/code&gt; in its name&lt;/li&gt;
&lt;li&gt;Any image in the post leaf bundle containing either &lt;code&gt;cover&lt;/code&gt; or &lt;code&gt;thumbnail&lt;/code&gt; in its name&lt;/li&gt;
&lt;li&gt;The default image defined at the site level params&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Adding enclosure tags
&lt;/h3&gt;

&lt;p&gt;We can effectively copy and paste the code being used for generating Twitter cards and modify it for enclosure tags. The snippet of code ultimately ends up looking like this 👇&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;pagePermalink&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Permalink&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;images&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;enclosure&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"{{ printf "&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="s"&gt;" $pagePermalink $img }}"&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"image/jpg"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;enclosure&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;images&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resources&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ByType&lt;/span&gt; &lt;span class="s"&gt;"image"&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;featured&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;images&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetMatch&lt;/span&gt; &lt;span class="s"&gt;"*feature*"&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;not&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;featured&lt;/span&gt; &lt;span class="p"&gt;}}{{&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;featured&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;images&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetMatch&lt;/span&gt; &lt;span class="s"&gt;"{*cover*,*thumbnail*}"&lt;/span&gt; &lt;span class="p"&gt;}}{{&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;featured&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;enclosure&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"{{ $featured.Permalink }}"&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"image/jpg"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;enclosure&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Site&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;images&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;enclosure&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"{{ index . 0 | absURL }}"&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"image/jpg"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;enclosure&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Walking through what this is doing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lines 1-4 will use the image defined in the &lt;code&gt;images&lt;/code&gt; in the post front matter, if it exists

&lt;ul&gt;
&lt;li&gt;I had to define &lt;code&gt;{{-/* $pagePermalink := .Permalink */-}}&lt;/code&gt; on line 1 to make &lt;code&gt;.Permalink&lt;/code&gt; available inside the &lt;code&gt;with&lt;/code&gt; block at line 4&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Line 7 will find an image containing &lt;code&gt;feature&lt;/code&gt; in its name, at the post leaf bundle section, if an image is not found previously&lt;/li&gt;
&lt;li&gt;Line 8 will revert to an image containing either &lt;code&gt;cover&lt;/code&gt; or &lt;code&gt;thumbnail&lt;/code&gt;, if an image is not found previously&lt;/li&gt;
&lt;li&gt;Lines 12-13 will default to the image defined at the site level params&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With lessons learnt trying to get images to display in RSS content, I am rendering the full URL of the image to ensure RSS is always able to retrieve it.&lt;/p&gt;

&lt;p&gt;I’m going to follow lines 12-13 above and have a default card defined at the site level parameters. In my &lt;code&gt;config.toml&lt;/code&gt; I’ll need to add the following…&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[params]&lt;/span&gt;
&lt;span class="py"&gt;images&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"images/jdheyburn_co_uk_card.png"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;…while also ensuring that I’ve placed the image under &lt;code&gt;static/images/jdheyburn_co_uk_card.png&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Once all that is done and deployed - we will have an RSS post card that looks like this 🙌&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Fwho-goes-blogging-6-three-steps-to-improve-hugos-rss-feeds%2Fhugo-enclosure-card-view.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Fwho-goes-blogging-6-three-steps-to-improve-hugos-rss-feeds%2Fhugo-enclosure-card-view.png" alt="inoreader displaying an image for a Hugo post"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;We can now advertise our RSS subscription URL to readers. My theme &lt;code&gt;hugo-coder&lt;/code&gt; supports this as a social button on the home page. We just need to add the below block to the &lt;code&gt;config.toml&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[[params.social]]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"RSS"&lt;/span&gt;
&lt;span class="py"&gt;icon&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"fas fa-rss"&lt;/span&gt;
&lt;span class="py"&gt;weight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;
&lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://jdheyburn.co.uk/index.xml"&lt;/span&gt;
&lt;span class="py"&gt;rel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"alternate"&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"application/rss+xml"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Once done - our homepage will have the button displayed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Fwho-goes-blogging-6-three-steps-to-improve-hugos-rss-feeds%2Frss-subscription-button.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjdheyburn.co.uk%2Fblog%2Fwho-goes-blogging-6-three-steps-to-improve-hugos-rss-feeds%2Frss-subscription-button.png" alt="RSS subscription button displayed on the homepage"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Summarising changes to RSS XML
&lt;/h2&gt;

&lt;p&gt;You can view the gist below to see my complete RSS XML file after all the above has been added. If you wish to use it, you’ll need to place it in &lt;code&gt;layouts/_default/rss.xml&lt;/code&gt; in your project directory.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;



&lt;p&gt;&lt;a href="https://gist.github.com/jdheyburn/a0a2c678f8f9795088b2779ec6af9920/revisions" rel="noopener noreferrer"&gt;Click here&lt;/a&gt; for a &lt;code&gt;git diff&lt;/code&gt; view on the changes I made from the default RSS XML file.&lt;/p&gt;

&lt;p&gt;Thanks for reading! 💯&lt;/p&gt;

</description>
      <category>hugo</category>
      <category>rss</category>
      <category>go</category>
    </item>
    <item>
      <title>Extending Gotests for Strict Error Tests</title>
      <dc:creator>Joseph Heyburn</dc:creator>
      <pubDate>Tue, 07 May 2019 14:01:26 +0000</pubDate>
      <link>https://dev.to/jdheyburn/extending-gotests-for-strict-error-tests-4j96</link>
      <guid>https://dev.to/jdheyburn/extending-gotests-for-strict-error-tests-4j96</guid>
      <description>&lt;p&gt;&lt;em&gt;This is my first post on dev.to, X-posted from my new personal blog which can be found &lt;a href="https://jdheyburn.co.uk"&gt;here&lt;/a&gt;. Hopefully I'll have dev.to publish from RSS &lt;br&gt;
feeds once I've worked it out!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Happy to receive any feedback you may have!&lt;/em&gt; 😃&lt;/p&gt;


&lt;h1&gt;
  
  
  Strict Error Tests in Java
&lt;/h1&gt;

&lt;p&gt;I love confirming the stability of my code through writing tests and practicing Test-driven development (TDD).  For Java, JUnit was my preferred testing framework of choice. When writing tests to confirm an exception had been thrown, I used the optional parameter &lt;code&gt;expected&lt;/code&gt; for the annotation &lt;code&gt;@Test&lt;/code&gt;, however I quickly found that this solution would not work for methods where I raised the same exception class multiple times for different error messages, and testing on those messages. &lt;/p&gt;

&lt;p&gt;This is commonly found in writing a validation method such as the one below, which will take in a name of a dog and return a boolean if it is valid.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;validateDogName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;dogName&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;DogValidationException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;containsSymbols&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dogName&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DogValidationException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Dogs cannot have symbols in their name!"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dogName&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DogValidationException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Who has a name for a dog that long?!"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;For this method, just using &lt;code&gt;@Test(expected = DogValidationException.class)&lt;/code&gt; on our test method is not sufficient; how can we determine that the exception was raised for a dogName.length breach and not for containing symbols?&lt;/p&gt;

&lt;p&gt;In order for me to resolve this, I came across the &lt;code&gt;ExpectedException&lt;/code&gt; class for JUnit on &lt;a href="https://www.baeldung.com/junit-assert-exception"&gt;Baeldung&lt;/a&gt; which enables us to specify the error message expected. Here it is applied to the test case for this method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Rule&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ExpectedException&lt;/span&gt; &lt;span class="n"&gt;exceptionRule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ExpectedException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;none&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;shouldHandleDogNameWithSymbols&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;exceptionRule&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DogValidationException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;exceptionRule&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;expectMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Dogs cannot have symbols in their name!"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;validateDogName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"GoodestBoy#1"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h1&gt;
  
  
  Applying to Golang
&lt;/h1&gt;

&lt;p&gt;Back to Golang, there is a built-in library aptly named &lt;code&gt;testing&lt;/code&gt; which enables us to assert on test conditions. When combined with &lt;a href="https://github.com/cweill/gotests"&gt;Gotests&lt;/a&gt; - a tool for generating Go tests from your code - writing tests could not be easier! I love how this is bundled in with the Go extension for VSCode, my text editor of choice (for now...).&lt;/p&gt;

&lt;p&gt;Converting the above Java &lt;code&gt;validateDogName&lt;/code&gt; method to Golang will produce something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;validateDogName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;containsSymbols&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dog cannot have symbols in their name"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"who has a name for a dog that long"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;If you have a Go method that returns the &lt;code&gt;error&lt;/code&gt; interface, then gotests will generate a test that look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Test_validateDogName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt;    &lt;span class="n"&gt;args&lt;/span&gt;
        &lt;span class="n"&gt;want&lt;/span&gt;    &lt;span class="kt"&gt;bool&lt;/span&gt;
        &lt;span class="n"&gt;wantErr&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;}{&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Test error was thrown for dog name with symbols"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"GoodestBoy#1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;wantErr&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;validateDogName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wantErr&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"validateDogName() error = %v, wantErr %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wantErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;want&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"validateDogName() = %v, want %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;From the above we are limited to what error we can assert for, here &lt;em&gt;any&lt;/em&gt; error returned will pass the test. This is equivalent to using &lt;code&gt;@Test(expected=Exception.class)&lt;/code&gt; in JUnit! But there is another way...&lt;/p&gt;

&lt;h2&gt;
  
  
  Modifying the Generated Test
&lt;/h2&gt;

&lt;p&gt;We only need to make a few simple changes to the generated test to give us the ability to assert on test error message...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;Test_validateDogName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;T&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;struct&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;    &lt;span class="n"&gt;string&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt;    &lt;span class="n"&gt;args&lt;/span&gt;
        &lt;span class="n"&gt;want&lt;/span&gt;    &lt;span class="n"&gt;bool&lt;/span&gt;
        &lt;span class="n"&gt;wantErr&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;
    &lt;span class="o"&gt;}{&lt;/span&gt;
        &lt;span class="nl"&gt;name:&lt;/span&gt; &lt;span class="s"&gt;"Test error was thrown for dog name with symbols"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;args:&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nl"&gt;name:&lt;/span&gt; &lt;span class="s"&gt;"GoodestBoy#1"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;},&lt;/span&gt;
        &lt;span class="nl"&gt;want:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;wantErr:&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;New&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dog cannot have symbols in their name"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;range&lt;/span&gt; &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Run&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;T&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;validateDogName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;wantErr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;reflect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DeepEqual&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;wantErr&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Errorf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"validateDogName() error = %v, wantErr %v"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;wantErr&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;want&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Errorf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"validateDogName() = %v, want %v"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;want&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;})&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;From the above there are three changes, let's go over them individually:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;wantErr error&lt;/code&gt; 

&lt;ul&gt;
&lt;li&gt;we are changing this from &lt;code&gt;bool&lt;/code&gt; so that we can make a comparison against the error returned from the function&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wantErr: errors.New("dog cannot have symbols in their name"),&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;this is the error struct that we are expecting&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;if tt.wantErr != nil &amp;amp;&amp;amp; !reflect.DeepEqual(err, tt.wantErr) {&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;check to make sure the test is expected an error, if so then compare it against the returned error&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Point 3 provides additional support if there was a test case that did not expect an error. Note how &lt;code&gt;wantErr&lt;/code&gt; is omitted entirely from the test case below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Should return true for valid dog name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Benedict Cumberland the Sausage Dog"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h2&gt;
  
  
  Customising Gotests Generated Test
&lt;/h2&gt;

&lt;p&gt;Gotests gives us the ability to provide our own templates for generating tests, and can easily be integrated into your text editor of choice. I'll show you how this can be done in VSCode.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Check out gotests and copy the templates directory to a place of your choosing&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;git clone https://github.com/cweill/gotests.git&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cp -R gotests/internal/render/templates ~/scratch/gotests&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Overwrite the contents of function.tmpl with &lt;a href="https://gist.github.com/jdheyburn/978e7b84dc9c197bcdd41afece2edab5"&gt;the contents of this Gist&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Add the following setting to VSCode's settings.json&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;"go.generateTestsFlags": ["--template_dir=~/scratch/templates"]&lt;/code&gt; &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once you have done that, future tests will now generate with stricter error testing! 🎉&lt;/p&gt;

&lt;h1&gt;
  
  
  Closing
&lt;/h1&gt;

&lt;p&gt;I understand that the recommendations above will make your code more fragile, as the code is subject to any changing of the error message of say a downstream library. However for myself, I prefer to write tests that are strict and minimalise the chance of other errors contaminating tests.&lt;/p&gt;

&lt;p&gt;I also understand that GoodestBoy#1 is probably a valid name for a dog! 🐶&lt;/p&gt;

</description>
      <category>go</category>
      <category>java</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
