<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://www.macksmind.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.macksmind.io/" rel="alternate" type="text/html" /><updated>2026-03-06T10:23:05+00:00</updated><id>https://www.macksmind.io/feed.xml</id><title type="html">MacksMind</title><entry><title type="html">Opinionated Rails Generator</title><link href="https://www.macksmind.io/2020/08/11/opinionated-rails-generator.html" rel="alternate" type="text/html" title="Opinionated Rails Generator" /><published>2020-08-11T19:15:42+00:00</published><updated>2026-03-06T10:22:05+00:00</updated><id>https://www.macksmind.io/2020/08/11/opinionated-rails-generator</id><content type="html" xml:base="https://www.macksmind.io/2020/08/11/opinionated-rails-generator.html"><![CDATA[<p><img src="/assets/images/rails_welcome.png" alt="Yay! You're on Rails!" />
Recently I made long overdue updates to a Rails app generator I’ve been using on and off since 2009.
The choices have changed over the years, but they aren’t shocking.
Nothing you couldn’t do yourself, but it may save you a few minutes.
Depending on your needs, check out the <a href="https://github.com/MacksMind/opinionated-rails-generator">Generator</a>
or the <a href="https://github.com/MacksMind/opinionated-rails">Output</a>.
And please let me know if you see something you think is outdated or an anti-pattern.</p>

<h3 id="generates-a-rails-6-application-with-these-choices">Generates a Rails 6 application with these choices:</h3>

<ul>
  <li><a href="https://www.postgresql.org/">PostgreSQL</a> database</li>
  <li><a href="https://v5.getbootstrap.com/">Bootstrap V5</a> front-end toolkit</li>
  <li><a href="https://github.com/heartcombo/devise">Devise</a> authentication</li>
  <li><a href="https://github.com/varvet/pundit">Pundit</a> authorization</li>
  <li><a href="http://slim-lang.com/">Slim</a> templating engine</li>
  <li><a href="https://rspec.info/">RSpec</a> &amp; <a href="https://cucumber.io/">Cucumber</a> testing</li>
  <li><a href="https://github.com/flyerhzm/bullet">Bullet</a> N+1 detection</li>
  <li><a href="https://github.com/thoughtbot/factory_bot">FactoryBot</a> test data</li>
  <li><a href="https://github.com/kaminari/kaminari">Kaminari</a> pagination</li>
  <li><a href="https://github.com/activerecord-hackery/ransack">Ransack</a> search</li>
  <li><a href="https://github.com/kpumuk/meta-tags">MetaTags</a> SEO</li>
  <li><a href="http://pry.github.io/">Pry</a> console</li>
  <li><a href="https://en.gravatar.com/">Gravatar</a> with fallback to <a href="https://github.com/ksz2k/letter_avatar">LetterAvatar</a></li>
  <li><a href="https://rubocop.org/">RuboCop</a> &amp; <a href="https://eslint.org/">ESLint</a> linters, with <a href="https://prettier.io/">Prettier</a> formatting</li>
  <li>Model <a href="https://github.com/ctran/annotate_models">annotation</a></li>
  <li>Code <a href="https://github.com/colszowka/simplecov">coverage</a></li>
  <li>Ready for cloud deploy at <a href="https://www.heroku.com/">Heroku</a></li>
</ul>]]></content><author><name>Mack Earnhardt</name></author><category term="ruby" /><category term="rails" /><summary type="html"><![CDATA[Recently I made long overdue updates to a Rails app generator I’ve been using on and off since 2009. The choices have changed over the years, but they aren’t shocking. Nothing you couldn’t do yourself, but it may save you a few minutes. Depending on your needs, check out the Generator or the Output. And please let me know if you see something you think is outdated or an anti-pattern.]]></summary></entry><entry><title type="html">Deploy Static Website to Amazon S3</title><link href="https://www.macksmind.io/2020/07/20/deploy-static-website-to-amazon-s3.html" rel="alternate" type="text/html" title="Deploy Static Website to Amazon S3" /><published>2020-07-20T22:43:42+00:00</published><updated>2026-03-06T10:22:05+00:00</updated><id>https://www.macksmind.io/2020/07/20/deploy-static-website-to-amazon-s3</id><content type="html" xml:base="https://www.macksmind.io/2020/07/20/deploy-static-website-to-amazon-s3.html"><![CDATA[<blockquote>
  <p><strong>⚠️ Update (Out of Date):</strong> This post reflects an older CloudFormation-based setup and is kept for historical reference.
For new projects, prefer <strong>AWS CDK</strong> so your infrastructure is best practices based, easier to test, and simpler to evolve over time.
Start here: <a href="https://docs.aws.amazon.com/cdk/v2/guide/home.html">AWS CDK Developer Guide</a>.</p>
</blockquote>

<p><strong>TL;DR</strong> —
CloudFormation template that autodeploys MacksMind.io.
Download from <a href="https://gist.github.com/MacksMind/c1dbc34d92aed1db892d23e758aa4637">GitHub</a>.</p>
<h2 id="why-cloudformation">Why CloudFormation?</h2>
<p>Setting up static deploy to S3 usually involves many steps in the AWS UI, then using <code class="language-plaintext highlighter-rouge">aws s3 sync</code> locally to deploy changes.
Here I demonstrate how to automate <code class="language-plaintext highlighter-rouge">jekyll build</code> in CodePipeline/CodeBuild, and also how to create all the resources in a single CloudFormation template.
It creates between 12 and 20 resources depending on your selections.</p>

<p>Yes it’s overkill for a Jekyll site, but it’s also a starting point for related use cases that only need a single pipeline from a single GitHub branch.
What follows is section by section commentary to help you with that adaptation.</p>
<h2 id="parameters">Parameters</h2>
<p>Template sharing &amp; reuse is all about parameters. If you’re deploying a <a href="https://jekyllrb.com/">Jekyll</a> site, you might not have to change a thing. Some of the less obvious bits are:</p>
<ul>
  <li>GitHubSecret
    <ul>
      <li>Create an OAuth token for GitHub using steps 1-6 of <a href="https://docs.aws.amazon.com/codepipeline/latest/userguide/GitHub-create-personal-token-CLI.html">these instructions</a></li>
      <li>Store the token in <a href="https://aws.amazon.com/secrets-manager/getting-started/">AWS Secrets Manager</a> as “Other type of secrets” without automatic rotation</li>
      <li>Construct a parameter resembling <code class="language-plaintext highlighter-rouge">{{resolve:secretsmanager:macksmind.io:SecretString:github-token}}</code>, in which <code class="language-plaintext highlighter-rouge">macksmind.io</code> is the <em>secret name</em>, and <code class="language-plaintext highlighter-rouge">github-token</code> is the <em>key</em> pointing to the token</li>
      <li>Of course you <strong>can</strong> paste the token directly into the parameter if you remove <code class="language-plaintext highlighter-rouge">AllowedPattern</code>, but you <strong>shouldn’t</strong></li>
    </ul>
  </li>
  <li>ChatbotSlackArn
    <ul>
      <li>To receive Slack notifications when deploys start and finish, link to Slack using <a href="https://docs.aws.amazon.com/chatbot/latest/adminguide/getting-started.html">these instructions</a></li>
      <li>Once the channel is configured, paste the ARN into the parameter</li>
    </ul>
  </li>
</ul>

<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="na">AWSTemplateFormatVersion</span><span class="pi">:</span> <span class="s">2010-09-09</span>
<span class="na">Parameters</span><span class="pi">:</span>
  <span class="na">GitHubOwner</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">String</span>
    <span class="na">Description</span><span class="pi">:</span> <span class="s">GitHub repo owner</span>
    <span class="na">MinLength</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">GitHubRepo</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">String</span>
    <span class="na">Description</span><span class="pi">:</span> <span class="s">GitHub repo name</span>
    <span class="na">MinLength</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">GitHubBranch</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">String</span>
    <span class="na">Default</span><span class="pi">:</span> <span class="s">master</span>
    <span class="na">Description</span><span class="pi">:</span> <span class="s">GitHub branch name</span>
    <span class="na">MinLength</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">GitHubSecret</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">String</span>
    <span class="na">Description</span><span class="pi">:</span> <span class="s">Reference to AWS Secrets Manager</span>
    <span class="na">AllowedPattern</span><span class="pi">:</span> <span class="s2">"</span><span class="se">\\</span><span class="s">{</span><span class="se">\\</span><span class="s">{resolve:secretsmanager:.+:SecretString:.+</span><span class="se">\\</span><span class="s">}</span><span class="se">\\</span><span class="s">}"</span>
  <span class="na">Subdomain</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">String</span>
    <span class="na">Default</span><span class="pi">:</span> <span class="s">www</span>
    <span class="na">Description</span><span class="pi">:</span> <span class="s">Subdomain of URL (optional)</span>
  <span class="na">RootDomain</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">String</span>
    <span class="na">Description</span><span class="pi">:</span> <span class="s">Root domain of URL</span>
  <span class="na">IndexPage</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">String</span>
    <span class="na">Default</span><span class="pi">:</span> <span class="s">index.html</span>
    <span class="na">Description</span><span class="pi">:</span> <span class="s">Default page in any directory</span>
    <span class="na">MinLength</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">ErrorPage</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">String</span>
    <span class="na">Default</span><span class="pi">:</span> <span class="s">404.html</span>
    <span class="na">Description</span><span class="pi">:</span> <span class="s">Page not found path (optional, but highly recommended)</span>
  <span class="na">Route53</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">String</span>
    <span class="na">Description</span><span class="pi">:</span> <span class="s">Automatically configure DNS in Route53?</span>
    <span class="na">AllowedValues</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="kc">true</span>
    <span class="pi">-</span> <span class="kc">false</span>
    <span class="na">Default</span><span class="pi">:</span> <span class="kc">false</span>
  <span class="na">RedirectRoot</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">String</span>
    <span class="na">Description</span><span class="pi">:</span> <span class="s">Redirect RootDomain to Subdomain?</span>
    <span class="na">AllowedValues</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="kc">true</span>
    <span class="pi">-</span> <span class="kc">false</span>
    <span class="na">Default</span><span class="pi">:</span> <span class="kc">false</span>
  <span class="na">JekyllEnv</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">String</span>
    <span class="na">Description</span><span class="pi">:</span> <span class="s">Jekyll build environment</span>
    <span class="na">Default</span><span class="pi">:</span> <span class="s">production</span>
    <span class="na">AllowedValues</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">production</span>
    <span class="pi">-</span> <span class="s">development</span>
  <span class="na">ChatbotSlackArn</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">String</span>
    <span class="na">Description</span><span class="pi">:</span> <span class="s">AWS Chatbot Channel ARN (optional)</span></code></pre></figure>

<h2 id="metadata">Metadata</h2>
<p>Controls parameter display in CloudFormation UI. Without this, parameters display alphabetically. 🙁</p>

<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="na">Metadata</span><span class="pi">:</span>
  <span class="na">AWS::CloudFormation::Interface</span><span class="pi">:</span>
    <span class="na">ParameterGroups</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">Label</span><span class="pi">:</span>
        <span class="na">default</span><span class="pi">:</span> <span class="s">GitHub Configuration</span>
      <span class="na">Parameters</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">GitHubOwner</span>
      <span class="pi">-</span> <span class="s">GitHubRepo</span>
      <span class="pi">-</span> <span class="s">GitHubBranch</span>
      <span class="pi">-</span> <span class="s">GitHubSecret</span>
    <span class="pi">-</span> <span class="na">Label</span><span class="pi">:</span>
        <span class="na">default</span><span class="pi">:</span> <span class="s">Website Info</span>
      <span class="na">Parameters</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">Subdomain</span>
      <span class="pi">-</span> <span class="s">RootDomain</span>
      <span class="pi">-</span> <span class="s">IndexPage</span>
      <span class="pi">-</span> <span class="s">ErrorPage</span>
    <span class="pi">-</span> <span class="na">Label</span><span class="pi">:</span>
        <span class="na">default</span><span class="pi">:</span> <span class="s">DNS Details</span>
      <span class="na">Parameters</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">Route53</span>
      <span class="pi">-</span> <span class="s">RedirectRoot</span></code></pre></figure>

<h2 id="conditions">Conditions</h2>
<ul>
  <li>Conditions and parameters are separate namespaces, permitting a Boolean such as <code class="language-plaintext highlighter-rouge">Route53</code> to match the String parameter it’s derived from</li>
  <li>Note the condition chaining using <code class="language-plaintext highlighter-rouge">Fn::And</code></li>
</ul>

<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="na">Conditions</span><span class="pi">:</span>
  <span class="na">HasSubdomain</span><span class="pi">:</span>
    <span class="na">Fn::Not</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">Fn::Equals</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s1">'</span><span class="s">'</span>
      <span class="pi">-</span> <span class="na">Ref</span><span class="pi">:</span> <span class="s">Subdomain</span>
  <span class="na">HasErrorPage</span><span class="pi">:</span>
    <span class="na">Fn::Not</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">Fn::Equals</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s1">'</span><span class="s">'</span>
      <span class="pi">-</span> <span class="na">Ref</span><span class="pi">:</span> <span class="s">ErrorPage</span>
  <span class="na">RedirectRoot</span><span class="pi">:</span>
    <span class="na">Fn::And</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">Condition</span><span class="pi">:</span> <span class="s">HasSubdomain</span>
    <span class="pi">-</span> <span class="na">Fn::Equals</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s1">'</span><span class="s">true'</span>
      <span class="pi">-</span> <span class="na">Ref</span><span class="pi">:</span> <span class="s">RedirectRoot</span>
  <span class="na">Route53</span><span class="pi">:</span>
    <span class="na">Fn::Equals</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s1">'</span><span class="s">true'</span>
    <span class="pi">-</span> <span class="na">Ref</span><span class="pi">:</span> <span class="s">Route53</span>
  <span class="na">Route53Redirect</span><span class="pi">:</span>
    <span class="na">Fn::And</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">Condition</span><span class="pi">:</span> <span class="s">Route53</span>
    <span class="pi">-</span> <span class="na">Condition</span><span class="pi">:</span> <span class="s">RedirectRoot</span>
  <span class="na">Chatbot</span><span class="pi">:</span>
    <span class="na">Fn::Not</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">Fn::Equals</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s1">'</span><span class="s">'</span>
      <span class="pi">-</span> <span class="na">Ref</span><span class="pi">:</span> <span class="s">ChatbotSlackArn</span></code></pre></figure>

<h2 id="certificate">Certificate</h2>
<h3 id="manual-step-alert">Manual Step Alert!!</h3>
<ul>
  <li>Certificate validation via DNS requires creation of a cryptic CNAME in the root domain</li>
  <li>Once creation of your stack is under way, visit the Route53 console to view the pending certificate</li>
  <li>Expand the certificate to view the CNAME</li>
  <li>Create the CNAME in your DNS <em>or</em> if your’re using Route53, AWS will create it for you with the push of a button</li>
</ul>

<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="na">Resources</span><span class="pi">:</span>
  <span class="na">Certificate</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">AWS::CertificateManager::Certificate</span>
    <span class="na">Properties</span><span class="pi">:</span>
      <span class="na">DomainName</span><span class="pi">:</span>
        <span class="na">Fn::If</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">HasSubdomain</span>
        <span class="pi">-</span> <span class="na">Fn::Sub</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${Subdomain}.${RootDomain}"</span>
        <span class="pi">-</span> <span class="na">Ref</span><span class="pi">:</span> <span class="s">RootDomain</span>
      <span class="na">DomainValidationOptions</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">DomainName</span><span class="pi">:</span>
          <span class="na">Fn::If</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s">HasSubdomain</span>
          <span class="pi">-</span> <span class="na">Fn::Sub</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${Subdomain}.${RootDomain}"</span>
          <span class="pi">-</span> <span class="na">Ref</span><span class="pi">:</span> <span class="s">RootDomain</span>
        <span class="na">ValidationDomain</span><span class="pi">:</span>
          <span class="na">Ref</span><span class="pi">:</span> <span class="s">RootDomain</span>
      <span class="na">ValidationMethod</span><span class="pi">:</span> <span class="s">DNS</span></code></pre></figure>

<h2 id="deploy-bucket">Deploy Bucket</h2>
<ul>
  <li><code class="language-plaintext highlighter-rouge">WebsiteConfiguration</code> defines handling for requests that don’t have an exact match</li>
  <li><code class="language-plaintext highlighter-rouge">AWS::NoValue</code> essentially unsets <code class="language-plaintext highlighter-rouge">ErrorDocument</code>, but be warned the default 404 is cryptic</li>
  <li><code class="language-plaintext highlighter-rouge">DeployBucketPolicy</code> bravely lets all the world read, but not list</li>
</ul>

<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml">  <span class="na">DeployBucket</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">AWS::S3::Bucket</span>
    <span class="na">Properties</span><span class="pi">:</span>
      <span class="na">WebsiteConfiguration</span><span class="pi">:</span>
        <span class="na">IndexDocument</span><span class="pi">:</span>
          <span class="na">Ref</span><span class="pi">:</span> <span class="s">IndexPage</span>
        <span class="na">ErrorDocument</span><span class="pi">:</span>
          <span class="na">Fn::If</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s">HasErrorPage</span>
          <span class="pi">-</span> <span class="na">Ref</span><span class="pi">:</span> <span class="s">ErrorPage</span>
          <span class="pi">-</span> <span class="na">Ref</span><span class="pi">:</span> <span class="s">AWS::NoValue</span>
  <span class="na">DeployBucketPolicy</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">AWS::S3::BucketPolicy</span>
    <span class="na">Properties</span><span class="pi">:</span>
      <span class="na">Bucket</span><span class="pi">:</span>
        <span class="na">Ref</span><span class="pi">:</span> <span class="s">DeployBucket</span>
      <span class="na">PolicyDocument</span><span class="pi">:</span>
        <span class="na">Statement</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">Action</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s">s3:GetObject</span>
          <span class="na">Effect</span><span class="pi">:</span> <span class="s">Allow</span>
          <span class="na">Principal</span><span class="pi">:</span> <span class="s2">"</span><span class="s">*"</span>
          <span class="na">Resource</span><span class="pi">:</span>
            <span class="na">Fn::Sub</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${DeployBucket.Arn}/*"</span></code></pre></figure>

<h2 id="cloudfront-distribution">CloudFront Distribution</h2>
<ul>
  <li><code class="language-plaintext highlighter-rouge">Compress</code> turns on automatic compression</li>
  <li><code class="language-plaintext highlighter-rouge">MinTTL</code> sets the cache to 24 hours even though browsers see <code class="language-plaintext highlighter-rouge">cache-control: no-cache</code></li>
  <li><code class="language-plaintext highlighter-rouge">ViewerProtocolPolicy</code> handles https redirection while <code class="language-plaintext highlighter-rouge">OriginProtocolPolicy</code> avoids attempting https with S3</li>
  <li><code class="language-plaintext highlighter-rouge">DomainName</code> uses the public hostname of the bucket, because <code class="language-plaintext highlighter-rouge">WebsiteConfiguration</code> has no effect on S3 access using AWS internals</li>
</ul>

<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml">  <span class="na">Distribution</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">AWS::CloudFront::Distribution</span>
    <span class="na">Properties</span><span class="pi">:</span>
      <span class="na">DistributionConfig</span><span class="pi">:</span>
        <span class="na">Aliases</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">Fn::If</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s">HasSubdomain</span>
          <span class="pi">-</span> <span class="na">Fn::Sub</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${Subdomain}.${RootDomain}"</span>
          <span class="pi">-</span> <span class="na">Ref</span><span class="pi">:</span> <span class="s">RootDomain</span>
        <span class="na">DefaultCacheBehavior</span><span class="pi">:</span>
          <span class="na">AllowedMethods</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s">GET</span>
          <span class="pi">-</span> <span class="s">HEAD</span>
          <span class="na">Compress</span><span class="pi">:</span> <span class="kc">true</span>
          <span class="na">ForwardedValues</span><span class="pi">:</span>
            <span class="na">QueryString</span><span class="pi">:</span> <span class="kc">false</span>
          <span class="na">MinTTL</span><span class="pi">:</span> <span class="m">86400</span>
          <span class="na">TargetOriginId</span><span class="pi">:</span> <span class="s">S3Origin</span>
          <span class="na">ViewerProtocolPolicy</span><span class="pi">:</span> <span class="s">redirect-to-https</span>
        <span class="na">Enabled</span><span class="pi">:</span> <span class="kc">true</span>
        <span class="na">Origins</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">DomainName</span><span class="pi">:</span>
            <span class="na">Fn::Sub</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${DeployBucket}.s3-website-${AWS::Region}.amazonaws.com"</span>
          <span class="na">Id</span><span class="pi">:</span> <span class="s">S3Origin</span>
          <span class="na">CustomOriginConfig</span><span class="pi">:</span>
            <span class="na">OriginProtocolPolicy</span><span class="pi">:</span> <span class="s">http-only</span>
        <span class="na">HttpVersion</span><span class="pi">:</span> <span class="s">http2</span>
        <span class="na">PriceClass</span><span class="pi">:</span> <span class="s">PriceClass_100</span>
        <span class="na">ViewerCertificate</span><span class="pi">:</span>
          <span class="na">AcmCertificateArn</span><span class="pi">:</span>
            <span class="na">Ref</span><span class="pi">:</span> <span class="s">Certificate</span>
          <span class="na">MinimumProtocolVersion</span><span class="pi">:</span> <span class="s">TLSv1.2_2018</span>
          <span class="na">SslSupportMethod</span><span class="pi">:</span> <span class="s">sni-only</span></code></pre></figure>

<h2 id="dns-recordsets">DNS RecordSets</h2>
<ul>
  <li><code class="language-plaintext highlighter-rouge">Condition: Route53</code> makes these resources optional</li>
  <li>A &amp; AAAA to cover IPv4 and IPv6</li>
  <li>Using Route53 aliases instead of CNAME for a shorter lookup
    <ul>
      <li>If your DNS is <em>not</em> at Route53, you’ll need CNAMEs pointing to the CloudFront <code class="language-plaintext highlighter-rouge">DomainName</code></li>
    </ul>
  </li>
</ul>

<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml">  <span class="na">DistributionIP4</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">AWS::Route53::RecordSet</span>
    <span class="na">Condition</span><span class="pi">:</span> <span class="s">Route53</span>
    <span class="na">Properties</span><span class="pi">:</span>
      <span class="na">AliasTarget</span><span class="pi">:</span>
        <span class="na">DNSName</span><span class="pi">:</span>
          <span class="na">Fn::GetAtt</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s">Distribution</span>
          <span class="pi">-</span> <span class="s">DomainName</span>
        <span class="na">HostedZoneId</span><span class="pi">:</span> <span class="s">Z2FDTNDATAQYW2</span>
      <span class="na">HostedZoneName</span><span class="pi">:</span>
        <span class="na">Fn::Sub</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${RootDomain}."</span>
      <span class="na">Name</span><span class="pi">:</span>
        <span class="na">Fn::If</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">HasSubdomain</span>
        <span class="pi">-</span> <span class="na">Fn::Sub</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${Subdomain}.${RootDomain}"</span>
        <span class="pi">-</span> <span class="na">Ref</span><span class="pi">:</span> <span class="s">RootDomain</span>
      <span class="na">Type</span><span class="pi">:</span> <span class="s">A</span>
  <span class="na">DistributionIP6</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">AWS::Route53::RecordSet</span>
    <span class="na">Condition</span><span class="pi">:</span> <span class="s">Route53</span>
    <span class="na">Properties</span><span class="pi">:</span>
      <span class="na">AliasTarget</span><span class="pi">:</span>
        <span class="na">DNSName</span><span class="pi">:</span>
          <span class="na">Fn::GetAtt</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s">Distribution</span>
          <span class="pi">-</span> <span class="s">DomainName</span>
        <span class="na">HostedZoneId</span><span class="pi">:</span> <span class="s">Z2FDTNDATAQYW2</span>
      <span class="na">HostedZoneName</span><span class="pi">:</span>
        <span class="na">Fn::Sub</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${RootDomain}."</span>
      <span class="na">Name</span><span class="pi">:</span>
        <span class="na">Fn::If</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">HasSubdomain</span>
        <span class="pi">-</span> <span class="na">Fn::Sub</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${Subdomain}.${RootDomain}"</span>
        <span class="pi">-</span> <span class="na">Ref</span><span class="pi">:</span> <span class="s">RootDomain</span>
      <span class="na">Type</span><span class="pi">:</span> <span class="s">AAAA</span></code></pre></figure>

<h2 id="codepipeline">CodePipeline</h2>
<p>The magic starts here.</p>
<ul>
  <li>PollForSourceChanges turns off polling since we define a Webhook below</li>
</ul>

<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml">  <span class="na">Pipeline</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">AWS::CodePipeline::Pipeline</span>
    <span class="na">Properties</span><span class="pi">:</span>
      <span class="na">ArtifactStore</span><span class="pi">:</span>
        <span class="na">Location</span><span class="pi">:</span>
          <span class="na">Ref</span><span class="pi">:</span> <span class="s">PipelineBucket</span>
        <span class="na">Type</span><span class="pi">:</span> <span class="s">S3</span>
      <span class="na">RoleArn</span><span class="pi">:</span>
        <span class="na">Fn::GetAtt</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">PipelineRole</span>
        <span class="pi">-</span> <span class="s">Arn</span>
      <span class="na">Stages</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">Actions</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">ActionTypeId</span><span class="pi">:</span>
            <span class="na">Category</span><span class="pi">:</span> <span class="s">Source</span>
            <span class="na">Owner</span><span class="pi">:</span> <span class="s">ThirdParty</span>
            <span class="na">Provider</span><span class="pi">:</span> <span class="s">GitHub</span>
            <span class="na">Version</span><span class="pi">:</span> <span class="m">1</span>
          <span class="na">Configuration</span><span class="pi">:</span>
            <span class="na">Owner</span><span class="pi">:</span>
              <span class="na">Ref</span><span class="pi">:</span> <span class="s">GitHubOwner</span>
            <span class="na">Repo</span><span class="pi">:</span>
              <span class="na">Ref</span><span class="pi">:</span> <span class="s">GitHubRepo</span>
            <span class="na">Branch</span><span class="pi">:</span>
              <span class="na">Ref</span><span class="pi">:</span> <span class="s">GitHubBranch</span>
            <span class="na">OAuthToken</span><span class="pi">:</span>
              <span class="na">Ref</span><span class="pi">:</span> <span class="s">GitHubSecret</span>
            <span class="na">PollForSourceChanges</span><span class="pi">:</span> <span class="kc">false</span>
          <span class="na">Name</span><span class="pi">:</span> <span class="s">GitHubCommit</span>
          <span class="na">OutputArtifacts</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">Name</span><span class="pi">:</span> <span class="s">SourcePipe</span>
        <span class="na">Name</span><span class="pi">:</span> <span class="s">Source</span>
      <span class="pi">-</span> <span class="na">Actions</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">Name</span><span class="pi">:</span> <span class="s">JekyllDeploy</span>
          <span class="na">ActionTypeId</span><span class="pi">:</span>
            <span class="na">Category</span><span class="pi">:</span> <span class="s">Build</span>
            <span class="na">Owner</span><span class="pi">:</span> <span class="s">AWS</span>
            <span class="na">Provider</span><span class="pi">:</span> <span class="s">CodeBuild</span>
            <span class="na">Version</span><span class="pi">:</span> <span class="m">1</span>
          <span class="na">Configuration</span><span class="pi">:</span>
            <span class="na">ProjectName</span><span class="pi">:</span>
              <span class="na">Ref</span><span class="pi">:</span> <span class="s">Project</span>
          <span class="na">InputArtifacts</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">Name</span><span class="pi">:</span> <span class="s">SourcePipe</span>
        <span class="na">Name</span><span class="pi">:</span> <span class="s">Build</span></code></pre></figure>

<h2 id="pipeline-bucket">Pipeline Bucket</h2>
<ul>
  <li>CodePipeline stores the code here for CodeBuild</li>
  <li>CodeBuild also caches here as you’ll see below</li>
</ul>

<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml">  <span class="na">PipelineBucket</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">AWS::S3::Bucket</span>
    <span class="na">Properties</span><span class="pi">:</span>
      <span class="na">PublicAccessBlockConfiguration</span><span class="pi">:</span>
        <span class="na">BlockPublicAcls</span><span class="pi">:</span> <span class="kc">true</span>
        <span class="na">BlockPublicPolicy</span><span class="pi">:</span> <span class="kc">true</span>
        <span class="na">IgnorePublicAcls</span><span class="pi">:</span> <span class="kc">true</span>
        <span class="na">RestrictPublicBuckets</span><span class="pi">:</span> <span class="kc">true</span></code></pre></figure>

<h2 id="pipeline-role--policy">Pipeline Role &amp; Policy</h2>
<p>All the perms needed to orchestrate the build</p>

<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml">  <span class="na">PipelineRole</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">AWS::IAM::Role</span>
    <span class="na">Properties</span><span class="pi">:</span>
      <span class="na">AssumeRolePolicyDocument</span><span class="pi">:</span>
        <span class="na">Statement</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">Action</span><span class="pi">:</span> <span class="s">sts:AssumeRole</span>
          <span class="na">Effect</span><span class="pi">:</span> <span class="s">Allow</span>
          <span class="na">Principal</span><span class="pi">:</span>
            <span class="na">Service</span><span class="pi">:</span> <span class="s">codepipeline.amazonaws.com</span>
        <span class="na">Version</span><span class="pi">:</span> <span class="s">2012-10-17</span>
  <span class="na">PipelineRoleDefaultPolicy</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">AWS::IAM::Policy</span>
    <span class="na">Properties</span><span class="pi">:</span>
      <span class="na">PolicyDocument</span><span class="pi">:</span>
        <span class="na">Statement</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">Action</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s">s3:GetObject*</span>
          <span class="pi">-</span> <span class="s">s3:GetBucket*</span>
          <span class="pi">-</span> <span class="s">s3:List*</span>
          <span class="pi">-</span> <span class="s">s3:DeleteObject*</span>
          <span class="pi">-</span> <span class="s">s3:PutObject*</span>
          <span class="pi">-</span> <span class="s">s3:Abort*</span>
          <span class="na">Effect</span><span class="pi">:</span> <span class="s">Allow</span>
          <span class="na">Resource</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">Fn::GetAtt</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="s">PipelineBucket</span>
            <span class="pi">-</span> <span class="s">Arn</span>
          <span class="pi">-</span> <span class="na">Fn::Sub</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${PipelineBucket.Arn}/*"</span>
        <span class="pi">-</span> <span class="na">Action</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s">codebuild:BatchGetBuilds</span>
          <span class="pi">-</span> <span class="s">codebuild:StartBuild</span>
          <span class="pi">-</span> <span class="s">codebuild:StopBuild</span>
          <span class="na">Effect</span><span class="pi">:</span> <span class="s">Allow</span>
          <span class="na">Resource</span><span class="pi">:</span>
            <span class="na">Fn::GetAtt</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="s">Project</span>
            <span class="pi">-</span> <span class="s">Arn</span>
        <span class="na">Version</span><span class="pi">:</span> <span class="s">2012-10-17</span>
      <span class="na">PolicyName</span><span class="pi">:</span> <span class="s">PipelineRoleDefaultPolicy</span>
      <span class="na">Roles</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">Ref</span><span class="pi">:</span> <span class="s">PipelineRole</span></code></pre></figure>

<h2 id="webhook">Webhook</h2>
<p>Automatically manages webhooks from your GitHub repo.</p>

<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml">  <span class="na">PipelineWebhook</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">AWS::CodePipeline::Webhook</span>
    <span class="na">Properties</span><span class="pi">:</span>
      <span class="na">Authentication</span><span class="pi">:</span> <span class="s">GITHUB_HMAC</span>
      <span class="na">AuthenticationConfiguration</span><span class="pi">:</span>
        <span class="na">SecretToken</span><span class="pi">:</span>
          <span class="na">Ref</span><span class="pi">:</span> <span class="s">GitHubSecret</span>
      <span class="na">Filters</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">JsonPath</span><span class="pi">:</span> <span class="s2">"</span><span class="s">$.ref"</span>
        <span class="na">MatchEquals</span><span class="pi">:</span> <span class="s">refs/heads/{Branch}</span>
      <span class="na">TargetAction</span><span class="pi">:</span> <span class="s">GitHubCommit</span>
      <span class="na">TargetPipeline</span><span class="pi">:</span>
        <span class="na">Ref</span><span class="pi">:</span> <span class="s">Pipeline</span>
      <span class="na">TargetPipelineVersion</span><span class="pi">:</span>
        <span class="na">Fn::GetAtt</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">Pipeline</span>
        <span class="pi">-</span> <span class="s">Version</span>
      <span class="na">RegisterWithThirdParty</span><span class="pi">:</span> <span class="kc">true</span></code></pre></figure>

<h2 id="codebuild-project">CodeBuild Project</h2>
<p>The magic continues here.</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">Cache</code> property, <code class="language-plaintext highlighter-rouge">bundle config set path</code>, and <code class="language-plaintext highlighter-rouge">cache</code> key in <code class="language-plaintext highlighter-rouge">BuildSpec</code> avoid unneeded gem installs
    <ul>
      <li><code class="language-plaintext highlighter-rouge">node_modules</code> and <code class="language-plaintext highlighter-rouge">npm install</code> would work in a similar way</li>
      <li>Simulate <code class="language-plaintext highlighter-rouge">rm -rf node_modules</code> by deleting the cache from the Pipeline Bucket</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">--cache-control='no-cache'</code> is set during <code class="language-plaintext highlighter-rouge">aws s3 sync</code></li>
  <li>` aws cloudfront create-invalidation` refreshes the cache</li>
</ul>

<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml">  <span class="na">Project</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">AWS::CodeBuild::Project</span>
    <span class="na">Properties</span><span class="pi">:</span>
      <span class="na">Name</span><span class="pi">:</span>
        <span class="na">Ref</span><span class="pi">:</span> <span class="s">AWS::StackName</span>
      <span class="na">Artifacts</span><span class="pi">:</span>
        <span class="na">Type</span><span class="pi">:</span> <span class="s">CODEPIPELINE</span>
      <span class="na">Cache</span><span class="pi">:</span>
        <span class="na">Location</span><span class="pi">:</span>
          <span class="na">Fn::Sub</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${PipelineBucket}/build_cache"</span>
        <span class="na">Type</span><span class="pi">:</span> <span class="s">S3</span>
      <span class="na">Environment</span><span class="pi">:</span>
        <span class="na">ComputeType</span><span class="pi">:</span> <span class="s">BUILD_GENERAL1_SMALL</span>
        <span class="na">Image</span><span class="pi">:</span> <span class="s">aws/codebuild/standard:4.0</span>
        <span class="na">Type</span><span class="pi">:</span> <span class="s">LINUX_CONTAINER</span>
      <span class="na">ServiceRole</span><span class="pi">:</span>
        <span class="na">Fn::GetAtt</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">ProjectRole</span>
        <span class="pi">-</span> <span class="s">Arn</span>
      <span class="na">Source</span><span class="pi">:</span>
        <span class="na">BuildSpec</span><span class="pi">:</span>
          <span class="na">Fn::Sub</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">version: 0.2</span>
            <span class="s">phases:</span>
              <span class="s">install:</span>
                <span class="s">runtime-versions:</span>
                  <span class="s">ruby: 2.7</span>
                <span class="s">commands:</span>
                  <span class="s">- mkdir -p _bundle_cache</span>
                  <span class="s">- bundle config set path _bundle_cache</span>
                  <span class="s">- bundle config set without 'development test'</span>
                  <span class="s">- bundle install</span>
              <span class="s">build:</span>
                <span class="s">commands:</span>
                  <span class="s">- JEKYLL_ENV=${JekyllEnv} bundle exec jekyll build</span>
              <span class="s">post_build:</span>
                <span class="s">commands:</span>
                  <span class="s">- aws s3 sync --cache-control='no-cache' _site s3://${DeployBucket}/ --delete</span>
                  <span class="s">- aws cloudfront create-invalidation --distribution-id ${Distribution} --paths '/*'</span>
            <span class="s">cache:</span>
              <span class="s">paths:</span>
              <span class="s">- '_bundle_cache/**/*'</span>
        <span class="na">Type</span><span class="pi">:</span> <span class="s">CODEPIPELINE</span></code></pre></figure>

<h2 id="codebuild-role--policy">CodeBuild Role &amp; Policy</h2>
<p>Lots of perms for all the CodeBuild things.</p>

<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml">  <span class="na">ProjectRole</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">AWS::IAM::Role</span>
    <span class="na">Properties</span><span class="pi">:</span>
      <span class="na">AssumeRolePolicyDocument</span><span class="pi">:</span>
        <span class="na">Statement</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">Action</span><span class="pi">:</span> <span class="s">sts:AssumeRole</span>
          <span class="na">Effect</span><span class="pi">:</span> <span class="s">Allow</span>
          <span class="na">Principal</span><span class="pi">:</span>
            <span class="na">Service</span><span class="pi">:</span> <span class="s">codebuild.amazonaws.com</span>
        <span class="na">Version</span><span class="pi">:</span> <span class="s">2012-10-17</span>
  <span class="na">ProjectRoleDefaultPolicy</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">AWS::IAM::Policy</span>
    <span class="na">Properties</span><span class="pi">:</span>
      <span class="na">PolicyDocument</span><span class="pi">:</span>
        <span class="na">Statement</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">Action</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s">s3:GetObject*</span>
          <span class="pi">-</span> <span class="s">s3:GetBucket*</span>
          <span class="pi">-</span> <span class="s">s3:List*</span>
          <span class="pi">-</span> <span class="s">s3:DeleteObject*</span>
          <span class="pi">-</span> <span class="s">s3:PutObject*</span>
          <span class="pi">-</span> <span class="s">s3:Abort*</span>
          <span class="na">Effect</span><span class="pi">:</span> <span class="s">Allow</span>
          <span class="na">Resource</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">Fn::GetAtt</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="s">PipelineBucket</span>
            <span class="pi">-</span> <span class="s">Arn</span>
          <span class="pi">-</span> <span class="na">Fn::Sub</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${PipelineBucket.Arn}/*"</span>
        <span class="pi">-</span> <span class="na">Action</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s">s3:List*</span>
          <span class="pi">-</span> <span class="s">s3:DeleteObject*</span>
          <span class="pi">-</span> <span class="s">s3:PutObject*</span>
          <span class="pi">-</span> <span class="s">s3:Abort*</span>
          <span class="na">Effect</span><span class="pi">:</span> <span class="s">Allow</span>
          <span class="na">Resource</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">Fn::GetAtt</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="s">DeployBucket</span>
            <span class="pi">-</span> <span class="s">Arn</span>
          <span class="pi">-</span> <span class="na">Fn::Sub</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${DeployBucket.Arn}/*"</span>
        <span class="pi">-</span> <span class="na">Action</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s">logs:CreateLogGroup</span>
          <span class="pi">-</span> <span class="s">logs:CreateLogStream</span>
          <span class="pi">-</span> <span class="s">logs:PutLogEvents</span>
          <span class="na">Effect</span><span class="pi">:</span> <span class="s">Allow</span>
          <span class="na">Resource</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">Fn::Sub</span><span class="pi">:</span> <span class="s">arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${Project}</span>
          <span class="pi">-</span> <span class="na">Fn::Sub</span><span class="pi">:</span> <span class="s">arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${Project}:*</span>
        <span class="pi">-</span> <span class="na">Action</span><span class="pi">:</span> <span class="s">cloudfront:CreateInvalidation</span>
          <span class="na">Effect</span><span class="pi">:</span> <span class="s">Allow</span>
          <span class="na">Resource</span><span class="pi">:</span>
            <span class="na">Fn::Sub</span><span class="pi">:</span> <span class="s">arn:${AWS::Partition}:cloudfront::${AWS::AccountId}:distribution/${Distribution}</span>
        <span class="na">Version</span><span class="pi">:</span> <span class="s">2012-10-17</span>
      <span class="na">PolicyName</span><span class="pi">:</span> <span class="s">ProjectRoleDefaultPolicy</span>
      <span class="na">Roles</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">Ref</span><span class="pi">:</span> <span class="s">ProjectRole</span></code></pre></figure>

<h2 id="slack-notification">Slack Notification</h2>
<ul>
  <li>AWS sends status updates and a link to the build</li>
  <li>Love to see that ✅</li>
</ul>

<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml">  <span class="na">ProjectNotification</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">AWS::CodeStarNotifications::NotificationRule</span>
    <span class="na">Condition</span><span class="pi">:</span> <span class="s">Chatbot</span>
    <span class="na">Properties</span><span class="pi">:</span>
      <span class="na">DetailType</span><span class="pi">:</span> <span class="s">FULL</span>
      <span class="na">EventTypeIds</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">codebuild-project-build-state-failed</span>
      <span class="pi">-</span> <span class="s">codebuild-project-build-state-succeeded</span>
      <span class="pi">-</span> <span class="s">codebuild-project-build-state-in-progress</span>
      <span class="pi">-</span> <span class="s">codebuild-project-build-state-stopped</span>
      <span class="na">Name</span><span class="pi">:</span>
        <span class="na">Fn::Sub</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${AWS::StackName}-notification"</span>
      <span class="na">Resource</span><span class="pi">:</span>
        <span class="na">Fn::GetAtt</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">Project</span>
        <span class="pi">-</span> <span class="s">Arn</span>
      <span class="na">Targets</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">TargetAddress</span><span class="pi">:</span>
          <span class="na">Ref</span><span class="pi">:</span> <span class="s">ChatbotSlackArn</span>
        <span class="na">TargetType</span><span class="pi">:</span> <span class="s">AWSChatbotSlack</span></code></pre></figure>

<h2 id="redirect-root-domain">Redirect Root Domain</h2>
<p>A few key changes allow redirection to the primary hostname.</p>
<ul>
  <li>This Certificate <em>also</em> needs a CNAME for validation</li>
  <li><code class="language-plaintext highlighter-rouge">RedirectAllRequestsTo</code> in S3 is the key difference from the deploy bucket</li>
  <li>https redirect is handled by S3 instead of CloudFront for fewer round trips</li>
  <li>Oddly the bucket doesn’t need to be public ¯\_(ツ)_/¯</li>
</ul>

<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml">  <span class="na">CertificateRedirect</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">AWS::CertificateManager::Certificate</span>
    <span class="na">Condition</span><span class="pi">:</span> <span class="s">RedirectRoot</span>
    <span class="na">Properties</span><span class="pi">:</span>
      <span class="na">DomainName</span><span class="pi">:</span>
        <span class="na">Ref</span><span class="pi">:</span> <span class="s">RootDomain</span>
      <span class="na">DomainValidationOptions</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">DomainName</span><span class="pi">:</span>
          <span class="na">Ref</span><span class="pi">:</span> <span class="s">RootDomain</span>
        <span class="na">ValidationDomain</span><span class="pi">:</span>
          <span class="na">Ref</span><span class="pi">:</span> <span class="s">RootDomain</span>
      <span class="na">ValidationMethod</span><span class="pi">:</span> <span class="s">DNS</span>
  <span class="na">DeployBucketRedirect</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">AWS::S3::Bucket</span>
    <span class="na">Condition</span><span class="pi">:</span> <span class="s">RedirectRoot</span>
    <span class="na">Properties</span><span class="pi">:</span>
      <span class="na">PublicAccessBlockConfiguration</span><span class="pi">:</span>
        <span class="na">BlockPublicAcls</span><span class="pi">:</span> <span class="kc">true</span>
        <span class="na">BlockPublicPolicy</span><span class="pi">:</span> <span class="kc">true</span>
        <span class="na">IgnorePublicAcls</span><span class="pi">:</span> <span class="kc">true</span>
        <span class="na">RestrictPublicBuckets</span><span class="pi">:</span> <span class="kc">true</span>
      <span class="na">WebsiteConfiguration</span><span class="pi">:</span>
        <span class="na">RedirectAllRequestsTo</span><span class="pi">:</span>
          <span class="na">HostName</span><span class="pi">:</span>
            <span class="na">Fn::Sub</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${Subdomain}.${RootDomain}"</span>
          <span class="na">Protocol</span><span class="pi">:</span> <span class="s">https</span>
  <span class="na">DistributionRedirect</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">AWS::CloudFront::Distribution</span>
    <span class="na">Condition</span><span class="pi">:</span> <span class="s">RedirectRoot</span>
    <span class="na">Properties</span><span class="pi">:</span>
      <span class="na">DistributionConfig</span><span class="pi">:</span>
        <span class="na">Aliases</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">Ref</span><span class="pi">:</span> <span class="s">RootDomain</span>
        <span class="na">DefaultCacheBehavior</span><span class="pi">:</span>
          <span class="na">AllowedMethods</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s">GET</span>
          <span class="pi">-</span> <span class="s">HEAD</span>
          <span class="na">ForwardedValues</span><span class="pi">:</span>
            <span class="na">QueryString</span><span class="pi">:</span> <span class="kc">false</span>
          <span class="na">TargetOriginId</span><span class="pi">:</span> <span class="s">S3OriginRedirect</span>
          <span class="na">ViewerProtocolPolicy</span><span class="pi">:</span> <span class="s">allow-all</span>
        <span class="na">Enabled</span><span class="pi">:</span> <span class="kc">true</span>
        <span class="na">Origins</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">DomainName</span><span class="pi">:</span>
            <span class="na">Fn::Sub</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${DeployBucketRedirect}.s3-website-${AWS::Region}.amazonaws.com"</span>
          <span class="na">Id</span><span class="pi">:</span> <span class="s">S3OriginRedirect</span>
          <span class="na">CustomOriginConfig</span><span class="pi">:</span>
            <span class="na">OriginProtocolPolicy</span><span class="pi">:</span> <span class="s">http-only</span>
        <span class="na">HttpVersion</span><span class="pi">:</span> <span class="s">http2</span>
        <span class="na">PriceClass</span><span class="pi">:</span> <span class="s">PriceClass_100</span>
        <span class="na">ViewerCertificate</span><span class="pi">:</span>
          <span class="na">AcmCertificateArn</span><span class="pi">:</span>
            <span class="na">Ref</span><span class="pi">:</span> <span class="s">CertificateRedirect</span>
          <span class="na">MinimumProtocolVersion</span><span class="pi">:</span> <span class="s">TLSv1.2_2018</span>
          <span class="na">SslSupportMethod</span><span class="pi">:</span> <span class="s">sni-only</span>
  <span class="na">DistributionIP4Redirect</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">AWS::Route53::RecordSet</span>
    <span class="na">Condition</span><span class="pi">:</span> <span class="s">Route53Redirect</span>
    <span class="na">Properties</span><span class="pi">:</span>
      <span class="na">AliasTarget</span><span class="pi">:</span>
        <span class="na">DNSName</span><span class="pi">:</span>
          <span class="na">Fn::GetAtt</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s">DistributionRedirect</span>
          <span class="pi">-</span> <span class="s">DomainName</span>
        <span class="na">HostedZoneId</span><span class="pi">:</span> <span class="s">Z2FDTNDATAQYW2</span>
      <span class="na">HostedZoneName</span><span class="pi">:</span>
        <span class="na">Fn::Sub</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${RootDomain}."</span>
      <span class="na">Name</span><span class="pi">:</span>
        <span class="na">Ref</span><span class="pi">:</span> <span class="s">RootDomain</span>
      <span class="na">Type</span><span class="pi">:</span> <span class="s">A</span>
  <span class="na">DistributionIP6Redirect</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">AWS::Route53::RecordSet</span>
    <span class="na">Condition</span><span class="pi">:</span> <span class="s">Route53Redirect</span>
    <span class="na">Properties</span><span class="pi">:</span>
      <span class="na">AliasTarget</span><span class="pi">:</span>
        <span class="na">DNSName</span><span class="pi">:</span>
          <span class="na">Fn::GetAtt</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s">DistributionRedirect</span>
          <span class="pi">-</span> <span class="s">DomainName</span>
        <span class="na">HostedZoneId</span><span class="pi">:</span> <span class="s">Z2FDTNDATAQYW2</span>
      <span class="na">HostedZoneName</span><span class="pi">:</span>
        <span class="na">Fn::Sub</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${RootDomain}."</span>
      <span class="na">Name</span><span class="pi">:</span>
        <span class="na">Ref</span><span class="pi">:</span> <span class="s">RootDomain</span>
      <span class="na">Type</span><span class="pi">:</span> <span class="s">AAAA</span></code></pre></figure>]]></content><author><name>Mack Earnhardt</name></author><category term="aws" /><summary type="html"><![CDATA[⚠️ Update (Out of Date): This post reflects an older CloudFormation-based setup and is kept for historical reference. For new projects, prefer AWS CDK so your infrastructure is best practices based, easier to test, and simpler to evolve over time. Start here: AWS CDK Developer Guide.]]></summary></entry><entry><title type="html">Respawn: Live to Play Another Day</title><link href="https://www.macksmind.io/2020/07/18/welcome-to-the-new-macksmind.html" rel="alternate" type="text/html" title="Respawn: Live to Play Another Day" /><published>2020-07-18T17:01:11+00:00</published><updated>2026-03-06T10:22:05+00:00</updated><id>https://www.macksmind.io/2020/07/18/welcome-to-the-new-macksmind</id><content type="html" xml:base="https://www.macksmind.io/2020/07/18/welcome-to-the-new-macksmind.html"><![CDATA[<p>Once upon a time, there was a blog.</p>

<p>It got an OK amount of traffic from people who were facing similar challenges, but I moved on to other things and let it decay. Then at some point it went away.</p>

<p>As I contemplate changes I’d like to make in my life, I realize I miss sharing solutions.</p>

<p>It’s not fancy, but it’s a start.</p>

<p>-M</p>]]></content><author><name>Mack Earnhardt</name></author><summary type="html"><![CDATA[Once upon a time, there was a blog.]]></summary></entry></feed>