<?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>Forem: Ian Knighton</title>
    <description>The latest articles on Forem by Ian Knighton (@ianknighton).</description>
    <link>https://forem.com/ianknighton</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%2F140866%2F4bbf9c54-cd10-453c-9613-8c41c5542367.jpeg</url>
      <title>Forem: Ian Knighton</title>
      <link>https://forem.com/ianknighton</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/ianknighton"/>
    <language>en</language>
    <item>
      <title>I Completely Bombed So You Don't Have To...</title>
      <dc:creator>Ian Knighton</dc:creator>
      <pubDate>Wed, 04 Feb 2026 16:00:02 +0000</pubDate>
      <link>https://forem.com/ianknighton/i-completely-bombed-so-you-dont-have-to-3nka</link>
      <guid>https://forem.com/ianknighton/i-completely-bombed-so-you-dont-have-to-3nka</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvxjojslbxa6re876uayk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvxjojslbxa6re876uayk.png" alt="I Completely Bombed So You Don't Have To..." width="495" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(tl:dr; I created a&lt;/em&gt; &lt;a href="https://github.com/IanKnighton/ideal-garbanzo?ref=ianknighton.com" rel="noopener noreferrer"&gt;&lt;em&gt;repo&lt;/em&gt;&lt;/a&gt; &lt;em&gt;that has a boiler plate for deploying to GKE. I've documented the code pretty well. The rest of this is just me reflecting on it because I think it's important.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I just got home from teaching a USSF Grassroots course. One of the topics that came up is what to do when things go wrong. I was telling a story about when I did my National "D" license, the training session I had worked on with teams at home completely fell apart when working with a top U12 girls team in Boise. Under normal circumstances, I would just pivot to something else. However, this was an assessment and someone was watching me so I panicked.&lt;/p&gt;

&lt;p&gt;Anyways, that happened today during a coding assessment and in an effort to maybe prove to myself that I do actually know what I'm doing, I'm writing it down and committing it to code.&lt;/p&gt;

&lt;p&gt;Maybe it will bail me out next time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Comfort
&lt;/h2&gt;

&lt;p&gt;When you've done something successfully and repeatedly, you naturally forget the amount of work that it took to get there. You forget the shell scripts and weird hacks that it took before you wrote the Terraform module. You forget the small nuances involved in that module.&lt;/p&gt;

&lt;p&gt;Two months since last touching it is a dangerous zone. You still have the muscle memory, but you've forgotten all of the nuances. You feel confident and comfortable in it because you've done it so many times, but you've forgotten all of the configuration it takes beforehand to make it all work.&lt;/p&gt;

&lt;p&gt;So you focus on something different that you think you're going to struggle with and end up tripping before you even get to the football.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Project
&lt;/h2&gt;

&lt;p&gt;The goal was to create an application that could be deployed to Kubernetes, deploy it, and build out a CI/CD pipeline for it. This is a simple task I've done 100+ times. In preparation, I focused on remembering how to create a Kubernetes cluster from scratch and overlooked the fact that I do not have a templated authentication handler that is repeatable as it is all locked up in private repos for a company I no longer work for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;A two hour video in which 45+ minutes is me fighting with an OpenID configuration issue.&lt;/p&gt;

&lt;p&gt;What was the issue?&lt;/p&gt;

&lt;p&gt;I transposed two words in one piece of configuration and in the spiraling around that transposed it in other places. Creating a lasagna of mistakes that made the easy part hard and changed the headspace I was in.&lt;/p&gt;

&lt;p&gt;A couple hours separated from it, I know that I wouldn't have been hung up on it. I could have taken a beat, caught my breath, and took a deeper look.&lt;/p&gt;

&lt;p&gt;Something about the eyes of that clock watching me though, I just couldn't help but panic.&lt;/p&gt;

&lt;p&gt;Anyways, there's a &lt;a href="https://github.com/IanKnighton/ideal-garbanzo?ref=ianknighton.com" rel="noopener noreferrer"&gt;repository&lt;/a&gt; in GitHub with a clean example that is well documented. I wish I would have had it this morning, but you can only hope that someone will see you trying to chase down a problem and not laugh at you too hard.&lt;/p&gt;

&lt;p&gt;I know there's a mixed audience who will care about one side of this or the other, so I tried to cover both.&lt;/p&gt;

</description>
      <category>github</category>
      <category>terraform</category>
      <category>aws</category>
    </item>
    <item>
      <title>Using GitHub to Manage GitHub (fin)</title>
      <dc:creator>Ian Knighton</dc:creator>
      <pubDate>Tue, 03 Feb 2026 16:00:36 +0000</pubDate>
      <link>https://forem.com/ianknighton/using-github-to-manage-github-fin-48ip</link>
      <guid>https://forem.com/ianknighton/using-github-to-manage-github-fin-48ip</guid>
      <description>&lt;p&gt;In &lt;a href="https://ianknighton.com/using-github-to-manage-your-github/" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt;, we did the basic setup and started creating/importing repositories.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://ianknighton.com/using-github-to-manage-github-part-deux/" rel="noopener noreferrer"&gt;Part 2&lt;/a&gt;, we migrated the state to a remote storage and made it so the Terraform could be ran anywhere.&lt;/p&gt;

&lt;p&gt;For Part 3, let's add a deployment process so you can properly gate and version control your changes. This is pretty overkill for your personal GitHub, but it's crucial when you're working in production/enterprise environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Credentials
&lt;/h2&gt;

&lt;p&gt;In the previous two posts, we've relied on CLI's installed on your machine to make things work. The AWS CLI provided credentials to your AWS account and the GitHub CLI handled the same for your GitHub account. Obviously, we can't rely on that when we're running on machines that are not owned by us. Wouldn't really want to anyways.&lt;/p&gt;

&lt;p&gt;So we'll need to create and save the appropriate credentials for both services.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: you can probably do this through Terraform using&lt;/em&gt; &lt;a href="https://ianknighton.com/using-ephemeral-resources-in-terraform/" rel="noopener noreferrer"&gt;&lt;em&gt;ephemeral resources&lt;/em&gt;&lt;/a&gt;&lt;em&gt;, but that's a bit heavy for all of this.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub PAT
&lt;/h3&gt;

&lt;p&gt;While logged into GitHub in your browser, go to &lt;strong&gt;Settings&lt;/strong&gt; &amp;gt; &lt;strong&gt;Developer Settings&lt;/strong&gt; &amp;gt; &lt;strong&gt;Personal Access Tokens&lt;/strong&gt; &amp;gt; &lt;strong&gt;Fine-grained Personal Access Tokens&lt;/strong&gt; (or just click this &lt;a href="https://github.com/settings/personal-access-tokens?ref=ianknighton.com" rel="noopener noreferrer"&gt;link&lt;/a&gt;) and create a new token.&lt;/p&gt;

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

&lt;p&gt;You will only need this token to have Administration Read and Write permissions. Which, I use the word &lt;em&gt;only&lt;/em&gt; in square quotes a bit. I strongly recommend setting this with a short expiration date so you have to rotate it regularly and it expires when you're not using it. It's a burden worth having when something has admin rights.&lt;/p&gt;

&lt;p&gt;Once you have that token, you'll go to the &lt;strong&gt;Repository Settings&lt;/strong&gt; &amp;gt; &lt;strong&gt;Secrets and Variables&lt;/strong&gt; &amp;gt; &lt;strong&gt;Actions&lt;/strong&gt; &amp;gt; &lt;strong&gt;New Repository Secret&lt;/strong&gt; and save the token with the name &lt;code&gt;GH_PAT&lt;/code&gt;. This will be used later when we setup the workflow.&lt;/p&gt;

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

&lt;p&gt;We're set there, now let's work on the AWS credentials.&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS Credentials
&lt;/h3&gt;

&lt;p&gt;Honestly, I was going to try and write this all up, but there's a lot to it and I can't really beat the &lt;a href="https://aws.amazon.com/blogs/security/use-iam-roles-to-connect-github-actions-to-actions-in-aws/?ref=ianknighton.com" rel="noopener noreferrer"&gt;AWS Docs&lt;/a&gt; on the topic. My contribution here will be that you should give the service account the &lt;code&gt;AmazonEC2FullAccess&lt;/code&gt; role. This will allow it to read and write to the Terraform State we created in Part 2.&lt;/p&gt;

&lt;p&gt;Another note that I will add is that I found the Trusted Entity page a little different than the current docs, so here's how I set mine up. It's pretty straightforward, but I thought it worth calling out.&lt;/p&gt;

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

&lt;p&gt;You can be more fine-grained than that, but for this example we'll be a little more permissive. Especially considering the use case.&lt;/p&gt;

&lt;p&gt;Once you have the role created, you'll want to copy the ARN of the role and add it to the &lt;strong&gt;Actions Secrets&lt;/strong&gt; as &lt;code&gt;AWS_ROLE_ARN&lt;/code&gt;. While this isn't strictly necessary, I like to keep things like that stored as a secret. It makes it easier to swap out down the road and if you have multiple people working on a project/multiple ARNs for roles, you can make sure that copy pasta is scoped correctly or it breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Actions Workflow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Structuring
&lt;/h3&gt;

&lt;p&gt;GitHub Actions Workflows live in the &lt;code&gt;.github/workflows&lt;/code&gt; path. You can create that with:&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;# Makes the directory&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; .github/workflows

&lt;span class="c"&gt;# Creates the deploy workflow&lt;/span&gt;
&lt;span class="nb"&gt;touch&lt;/span&gt; .github/workflows/deploy.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also like to move all of my &lt;code&gt;*.tf&lt;/code&gt; files into a directory called &lt;code&gt;.tf&lt;/code&gt; so it cleans up the root of the repository.&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;# Makes the directory&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; .tf

&lt;span class="c"&gt;# Moves all of the files&lt;/span&gt;
&lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;.tf ./.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's worth running a &lt;code&gt;terraform init&lt;/code&gt; and a &lt;code&gt;terraform plan&lt;/code&gt; in that directory to make sure that everything is still good. Just a bit of a sanity check before you start pushing things up.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Actual Workflow
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;(Note: as I'm writing this, GitHub Actions is having "service degradation" so I'm somewhat air-coding. I'll come back and update if I find out a missed anything.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The full workflow &lt;a href="https://github.com/IanKnighton/ideal-garbanzo/blob/main/.github/workflows/deploy.yml" rel="noopener noreferrer"&gt;lives here&lt;/a&gt; as a reference, but I want to call out a few sections and what they do and why they're there.&lt;/p&gt;

&lt;h4&gt;
  
  
  Credentials Handling
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GH_PAT }}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Here we tell the whole workflow to use the PAT we created as its token. This helps us limit the token and makes it possible to adjust scope as we need.&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;# Configure AWS Credentials&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;Configure AWS Credentials&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/configure-aws-credentials@v5.1.1&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
        &lt;span class="na"&gt;role-to-assume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_ROLE_ARN }}&lt;/span&gt;
        &lt;span class="na"&gt;role-session-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.actor }}-github-actions&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This step makes sure that we've defined the role that the actor will use. It's possible that we may have other roles we want different pipelines to use, so that's where having that stored as a secret works for us.&lt;/p&gt;

&lt;h4&gt;
  
  
  Terraform Plan
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;terraform-plan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Terraform&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Plan'&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'push' || github.event_name == 'pull_request'&lt;/span&gt;
    &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;tfplanExitCode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.tf-plan.outputs.exitcode }}&lt;/span&gt;
    &lt;span class="na"&gt;defaults&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.tf/&lt;/span&gt;
&lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;...&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is quite the action here, but I'll explain what it does in bullets.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check out the repo and all of its code.&lt;/li&gt;
&lt;li&gt;Configures the AWS credentials so the action runner can authenticate with AWS.&lt;/li&gt;
&lt;li&gt;Installs Terraform on the runner.&lt;/li&gt;
&lt;li&gt;Initializes our Terraform configuration&lt;/li&gt;
&lt;li&gt;Checks the formatting of our Terraform configuration.

&lt;ol&gt;
&lt;li&gt;This isn't &lt;strong&gt;necessary&lt;/strong&gt; , but it does make sure you're formatted. It's like linting your other code. &lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt;Runs the &lt;code&gt;terraform plan&lt;/code&gt; and outputs the plan itself as a &lt;code&gt;.tfplan&lt;/code&gt; file. &lt;/li&gt;

&lt;li&gt;That &lt;code&gt;.tfplan&lt;/code&gt; file is then used in the following steps to output the plan as a comment on the Pull Request. This is so, so helpful when you have someone reviewing your code. They can quickly look and see what changes Terraform is making without having to "interpret" the git comparison. &lt;/li&gt;

&lt;/ol&gt;

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

&lt;p&gt;All of this runs when a Pull Request is created. I cannot stress this enough that, especially if you are working on a team, you should &lt;strong&gt;ALWAYS ENABLE BRANCH PROTECTION ON THE DEFAULT BRANCH SO NOTHING GOES TO THAT BRANCH WITHOUT A CODE REVIEW/PULL REQUEST&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Treat your default branch like an absolute treasure.&lt;/p&gt;

&lt;p&gt;It is.&lt;/p&gt;

&lt;h4&gt;
  
  
  Terraform Apply
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;terraform-apply&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Terraform&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Apply'&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.ref == 'refs/heads/main' &amp;amp;&amp;amp; needs.terraform-plan.outputs.tfplanExitCode == &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;terraform-plan&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;defaults&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.tf/&lt;/span&gt;
&lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;...&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You guessed it, it does all of the same things the plan does but it actually makes the changes. This job only runs if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This is a merge or a push to the branch (in our case &lt;code&gt;main&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;terraform-plan&lt;/code&gt; step succeeds. &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  In Closing
&lt;/h2&gt;

&lt;p&gt;At this point, I would hope that everything is in good working order. You can build from here and add things like creating storage buckets for docker containers for each repo, creating each repo it's own roles or credentials, or just adding &lt;strong&gt;BRANCH PROTECTION AS A DEFAULT&lt;/strong&gt; to all of your repositories.&lt;/p&gt;

&lt;p&gt;You can now also work across multiple people with the same experience and have a clean, baked in way to validate infrastructure changes before they are made.&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>github</category>
      <category>terraform</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Using GitHub to Manage GitHub (Part Deux)</title>
      <dc:creator>Ian Knighton</dc:creator>
      <pubDate>Wed, 21 Jan 2026 17:07:22 +0000</pubDate>
      <link>https://forem.com/ianknighton/using-github-to-manage-github-part-deux-138b</link>
      <guid>https://forem.com/ianknighton/using-github-to-manage-github-part-deux-138b</guid>
      <description>&lt;p&gt;In a &lt;a href="https://ianknighton.com/using-github-to-manage-your-github/" rel="noopener noreferrer"&gt;previous post&lt;/a&gt;, I wrote about using GitHub to manage GitHub by creating Terraform Resources. This was a pretty basic configuration and it was limited to running from one computer.   &lt;/p&gt;

&lt;p&gt;We live in a modern era though and we can use cloud services and deployment pipelines to make it so we're never tied to one machine and we can leverage all of the functionalities of source control. Branch protection, security gates, the whole lot.&lt;/p&gt;

&lt;p&gt;In this post, we'll work on setting up a remote state. This means that the Terraform can run from anywhere.&lt;/p&gt;

&lt;p&gt;Two quick notes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You'll need to have an AWS account and the &lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html?ref=ianknighton.com" rel="noopener noreferrer"&gt;AWS CLI&lt;/a&gt; setup for this.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://github.com/IanKnighton/jubilant-octo-spork?ref=ianknighton.com" rel="noopener noreferrer"&gt;source&lt;/a&gt; will be updated to match this. &lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Moving the State to Space
&lt;/h2&gt;

&lt;p&gt;Okay, not really space, yet. Clouds are close to space though.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.tfstate&lt;/code&gt; file is a JSON file (I wrote about it a bit &lt;a href="https://ianknighton.com/using-ephemeral-resources-in-terraform/" rel="noopener noreferrer"&gt;here&lt;/a&gt;) that stores the current known configuration of the resources in the cloud. By default, the state file is stored on your machine in the same directory that the &lt;code&gt;main.tf&lt;/code&gt; (or wherever you have your providers configured) are stored.&lt;/p&gt;

&lt;p&gt;For a lot of use cases, this is okay, but as you move closer to production/enterprise environments you'll want to be able to make changes from anywhere and have the same experience. You'll also want to do this across multiple services and places as you expand your footprint.&lt;/p&gt;

&lt;p&gt;Terraform has this idea of a "remote state" which tells the configuration to store its state file somewhere else. Once stored, everything that runs that Terraform will use that remote state. This means that if you and I have the appropriate credentials, we can both work with the same Terraform and have the changes be consistent.&lt;/p&gt;

&lt;p&gt;In this example, we'll create an Amazon S3 bucket. I've been working in AWS a lot lately because it's the only cloud I don't have a ton of experience in so I'm trying to use it on my own in a bunch of cases.&lt;/p&gt;

&lt;p&gt;Anyways, let's use Terraform to create an S3 bucket.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Up Terraform
&lt;/h3&gt;

&lt;p&gt;First, we'll update the &lt;code&gt;main.tf&lt;/code&gt; to include the &lt;code&gt;hashicorp/aws&lt;/code&gt; and &lt;code&gt;hashicorp/random&lt;/code&gt; providers.&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;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;github&lt;/span&gt; &lt;span class="p"&gt;=&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;"integrations/github"&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;"6.9.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="p"&gt;=&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;"hashicorp/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; 6.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;random&lt;/span&gt; &lt;span class="p"&gt;=&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;"hashicorp/random"&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; 3.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="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"github"&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="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;github_organization&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_region&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"random"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;# Configuration options&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the changes that we made, you'll want to run a &lt;code&gt;terraform init&lt;/code&gt; to make sure you have all of the necessary providers and then a &lt;code&gt;terraform plan&lt;/code&gt;. If you run a plan right now, there should be no changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a Bucket
&lt;/h3&gt;

&lt;p&gt;Second, we'll add an S3 bucket. In Terraform file names don't matter, but as I mentioned before I like to follow a naming convention of &lt;code&gt;{PROVIDER}-{RESOURCE_TYPE}-{RESOURCE_NAME}&lt;/code&gt;. So in this case, I'll call the file &lt;code&gt;aws-s3-terraform-state.tf&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;It's worth noting that S3 bucket names have to be globally unique, so I use the &lt;code&gt;random&lt;/code&gt; provider to make sure we have a unique name.&lt;/em&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;"random_string"&lt;/span&gt; &lt;span class="s2"&gt;"terraform_state_suffix"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
  &lt;span class="nx"&gt;special&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nx"&gt;upper&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Between the region and the random, we should have a unique name.&lt;/span&gt;
&lt;span class="c1"&gt;# The region in the name also helps keep things organized at a larger scale.&lt;/span&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;"terraform_state"&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;"${var.aws_region}-terraform-state-${random_string.terraform_state_suffix.result}"&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="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform_state"&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"state"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Versioning is optional, but it's nice to have.&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_versioning"&lt;/span&gt; &lt;span class="s2"&gt;"terraform_state_versioning"&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="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;terraform_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;versioning_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Enabled"&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'll notice that I reference the variable &lt;code&gt;aws_region&lt;/code&gt; to the file there. This makes sure that we're creating resources across the same region. In most cases the default is &lt;code&gt;us-east-1&lt;/code&gt;. I tend to use &lt;code&gt;us-west-2&lt;/code&gt; since it's closer to me. I also do that since most people put production resources in &lt;code&gt;us-east-1&lt;/code&gt; and if I'm bouncing between projects it's an easy way to make sure &lt;strong&gt;my&lt;/strong&gt; resources are going to the right place.&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;variable&lt;/span&gt; &lt;span class="s2"&gt;"aws_region"&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;"The AWS region to deploy resources in"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-west-2"&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'll run a &lt;code&gt;terraform plan&lt;/code&gt; to see what new resources are being created.&lt;/p&gt;

&lt;p&gt;The plan output should look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Terraform will perform the following actions:

  &lt;span class="c"&gt;# aws_s3_bucket.terraform_state will be created&lt;/span&gt;
  + resource &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"terraform_state"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;[&lt;/span&gt;...]
      + region &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-west-2"&lt;/span&gt;
        &lt;span class="o"&gt;[&lt;/span&gt;...]
      + tags &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
          + &lt;span class="s2"&gt;"Environment"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"state"&lt;/span&gt;
          + &lt;span class="s2"&gt;"Name"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform_state"&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="c"&gt;# aws_s3_bucket_versioning.terraform_state_versioning will be created&lt;/span&gt;
  + resource &lt;span class="s2"&gt;"aws_s3_bucket_versioning"&lt;/span&gt; &lt;span class="s2"&gt;"terraform_state_versioning"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;[&lt;/span&gt;...]
      + region &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-west-2"&lt;/span&gt;
        &lt;span class="o"&gt;[&lt;/span&gt;...]
    &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="c"&gt;# random_string.terraform_state_suffix will be created&lt;/span&gt;
  + resource &lt;span class="s2"&gt;"random_string"&lt;/span&gt; &lt;span class="s2"&gt;"terraform_state_suffix"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;[&lt;/span&gt;...]
      + length &lt;span class="o"&gt;=&lt;/span&gt; 6
      + lower &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
        &lt;span class="o"&gt;[&lt;/span&gt;...]
      + special &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;
      + upper &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

Plan: 3 to add, 0 to change, 0 to destroy.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Example terraform plan output.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you don't see the plan, odds are good there is something incorrect in your AWS configuration.&lt;/p&gt;

&lt;p&gt;Assuming we're happy with everything, we'll run &lt;code&gt;terraform apply&lt;/code&gt; and the resources will be created.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

random_string.terraform_state_suffix: Creating...
random_string.terraform_state_suffix: Creation complete after 0s [id=yt942l]
aws_s3_bucket.terraform_state: Creating...
aws_s3_bucket.terraform_state: Creation complete after 2s [id=us-west-2-terraform-state-yt942l]
aws_s3_bucket_versioning.terraform_state_versioning: Creating...
aws_s3_bucket_versioning.terraform_state_versioning: Creation complete after 1s [id=us-west-2-terraform-state-yt942l]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that the bucket name in this case is: &lt;code&gt;us-west-2-terraform-state-yt942l&lt;/code&gt;. We'll need that in a second.&lt;/p&gt;

&lt;h3&gt;
  
  
  Migrating State
&lt;/h3&gt;

&lt;p&gt;Finally, with everything configured and created we'll migrate the &lt;code&gt;.tfstate&lt;/code&gt; to the S3 bucket.&lt;/p&gt;

&lt;p&gt;Going back to the &lt;code&gt;main.tf&lt;/code&gt; file, we'll add a &lt;code&gt;backend&lt;/code&gt; to the configuration.&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;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&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;backend&lt;/span&gt; &lt;span class="s2"&gt;"s3"&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;"{BUCKET_NAME}"&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;"{STORAGE_KEY}/terraform.tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"{REGION}"&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;Here's a breakdown of what we'll need here.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The bucket name we grabbed from the &lt;code&gt;terraform apply&lt;/code&gt; step.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;key&lt;/code&gt; we'll want to use. 

&lt;ol&gt;
&lt;li&gt;This is a little complicated to explain, but it's where in the bucket the state will be stored. In a lot of cases, you can just use the repo name. My preference is to use a GUID. That makes it so you can use this bucket across multiple Terraform configurations without the states being confused. &lt;/li&gt;
&lt;li&gt;On MacOS/Linux you can run the command &lt;code&gt;uuidgen&lt;/code&gt; to create a new GUID. &lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt;The &lt;code&gt;region&lt;/code&gt; the bucket is in. 

&lt;ol&gt;
&lt;li&gt;It is worth noting that this cannot be a variable so you have to make sure it matches. It's silly that you can't use variables here, but it is what it is. &lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;In my case, the complete file looks 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;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;github&lt;/span&gt; &lt;span class="p"&gt;=&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;"integrations/github"&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;"6.9.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="p"&gt;=&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;"hashicorp/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; 6.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;random&lt;/span&gt; &lt;span class="p"&gt;=&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;"hashicorp/random"&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; 3.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="s2"&gt;"s3"&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;"us-west-2-terraform-state-yt942l"&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;"AEAAA801-1091-4E6F-B125-5F79B8B8ED0E/terraform.tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-west-2"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"github"&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="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;github_organization&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_region&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"random"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;# Configuration options&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;With everything changed, we'll run &lt;code&gt;terraform init&lt;/code&gt; again. This will prompt Terraform to re-evaluate the configuration and it will catch that the &lt;code&gt;backend&lt;/code&gt; has been added. It will then ask you if you want to migrate your state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Initializing the backend...
Do you want to copy existing state to the new backend?
  Pre-existing state was found &lt;span class="k"&gt;while &lt;/span&gt;migrating the previous &lt;span class="s2"&gt;"local"&lt;/span&gt; backend to the
  newly configured &lt;span class="s2"&gt;"s3"&lt;/span&gt; backend. No existing state was found &lt;span class="k"&gt;in &lt;/span&gt;the newly
  configured &lt;span class="s2"&gt;"s3"&lt;/span&gt; backend. Do you want to copy this state to the new &lt;span class="s2"&gt;"s3"&lt;/span&gt;
  backend? Enter &lt;span class="s2"&gt;"yes"&lt;/span&gt; to copy and &lt;span class="s2"&gt;"no"&lt;/span&gt; to start with an empty state.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll want to make sure to enter &lt;code&gt;yes&lt;/code&gt; here to migrate the state. To verify, you can check your S3 bucket in the AWS console and see that the new path and file have been created.&lt;/p&gt;

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

&lt;p&gt;From this point forward, you can run your Terraform from anywhere that you have access to the repository without worrying about the local state file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;This was a lot of words and not a lot of code. In the next installment, we'll add a release pipeline to this whole process. That's really going to be focused on enterprise/production environments, but it's a good look at using Terraform at scale and an introduction to GitHub Actions.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>aws</category>
      <category>github</category>
      <category>s3</category>
    </item>
    <item>
      <title>Using GitHub to Manage Your GitHub</title>
      <dc:creator>Ian Knighton</dc:creator>
      <pubDate>Tue, 20 Jan 2026 17:37:39 +0000</pubDate>
      <link>https://forem.com/ianknighton/using-github-to-manage-your-github-4kfl</link>
      <guid>https://forem.com/ianknighton/using-github-to-manage-your-github-4kfl</guid>
      <description>&lt;p&gt;Let me come out of the gate and say that this idea comes from a talk at &lt;a href="https://www.youtube.com/watch?v=hVHLcb7kCXQ&amp;amp;ref=ianknighton.com" rel="noopener noreferrer"&gt;GitHub Universe 2022&lt;/a&gt;. They go pretty deep into it, so I thought I would show a basic implementation I use as a primer for someone to build off of.&lt;/p&gt;

&lt;p&gt;Also, as always, here's the &lt;a href="https://github.com/IanKnighton/jubilant-octo-spork?ref=ianknighton.com" rel="noopener noreferrer"&gt;actual source&lt;/a&gt; if you don't want to read the whole thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  But, why?
&lt;/h2&gt;

&lt;p&gt;I'll probably say this until I'm blue in the face, but if it can be code and in source control...it should be in code and source control. Everything becomes repeatable, trackable, and standardize-able.&lt;/p&gt;

&lt;p&gt;Let's look at this in a small scenario:&lt;/p&gt;

&lt;p&gt;You've been working on a small project and it's reached a point you're going to bring in a couple people to help you. As it's just been you, things like merging directly to &lt;code&gt;main&lt;/code&gt; has been fine, but now that there are multiple people working on the project it's not really sustainable to not use branches. Since you're managing the project, you probably want a pull request for every feature and the ability to look at them before they go into &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's pretty simple click-ops.&lt;/p&gt;

&lt;p&gt;Things go well and now you need to start working on splitting off a service. You've made the decision to move this to a new repository to separate out the concerns. Now you have to click-ops two repos to give your team access and turn on the branch protection and do any other configuration you've done on the main repository.&lt;/p&gt;

&lt;p&gt;Times that by four or five and I think you can see how tedious that becomes. Especially if you decide to change your policy and you then have to go change it in multiple places and make sure it's correct across all of the repositories.&lt;/p&gt;

&lt;p&gt;And then you bring in new members to the team when someone leaves...&lt;/p&gt;

&lt;p&gt;You get the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Okay, how?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Creating the main.tf
&lt;/h3&gt;

&lt;p&gt;We start simple...&lt;/p&gt;

&lt;p&gt;We'll need a &lt;code&gt;main.tf&lt;/code&gt; that has the GitHub provider configured and a &lt;code&gt;variables.tf&lt;/code&gt; that sets our default organization.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform {
  required_providers {
    github = {
      source = "integrations/github"
      version = "6.9.0"
    }
  }
}

provider "github" {
  owner = var.github_organization
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;main.tf file that adds the GitHub provider and configuration.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;variable "github_organization" {
  description = "The GitHub organization where the repository will be created"
  type = string
  default = "{YOUR_ORG}"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;variables.tf file that sets the default organization for the GitHub provider.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At this point, you should be able to run &lt;code&gt;terraform init&lt;/code&gt; and validate that you're able to complete that process. We've made no changes, but nothing going forward will work until you've ran the &lt;code&gt;init&lt;/code&gt; and allowed Terraform to pull in the providers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a Repository Resource
&lt;/h3&gt;

&lt;p&gt;Now that we're set, let's start by creating a resource for an existing repository. I like using the pattern &lt;code&gt;github-repository-{REPOSITORY_NAME}.tf&lt;/code&gt; when creating these resources.&lt;/p&gt;

&lt;p&gt;As an example, the code for this blog post is saved in a repo called &lt;code&gt;jubilant-octo-spork&lt;/code&gt;, so the file name would be &lt;code&gt;github-repository-jubilant-octo-spork.tf&lt;/code&gt;. The related resource inside of that file would look 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;resource "github_repository" "jubilant_octo_spork" {
  name = "jubilant-octo-spork"
  description = "An example of using Terraform in GitHub to manage your GitHub"
  visibility = "public"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Example terraform configuration for GitHub repository.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Note that the resource name uses &lt;code&gt;_&lt;/code&gt; instead of &lt;code&gt;-&lt;/code&gt; as the separator. This is just to conform with Terraform standards and makes life a little easier when you're working with resources.&lt;/p&gt;

&lt;h3&gt;
  
  
  Importing an Existing Repository
&lt;/h3&gt;

&lt;p&gt;If you were to run &lt;code&gt;terraform plan&lt;/code&gt; at this phase, the output will tell you that it wants to create a repository. That's all well and good, but in this case this repo already exists so we don't want to do that. In order to fix that, we'll &lt;code&gt;import&lt;/code&gt; the existing repos information into that resource we just created.&lt;/p&gt;

&lt;p&gt;The command looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform import github_repository.{RESOURCE_NAME} {REPOSITORY-NAME}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So in our example case, it would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform import github_repository.jubilant_octo_spork jubilant-octo-spork

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

&lt;/div&gt;



&lt;p&gt;This will run for a second as it uses the GitHub API to pull in the information and match it to the resource. From here forward, the repository itself is now part of the Terraform state and thus is being controlled by Terraform.&lt;/p&gt;

&lt;h3&gt;
  
  
  Plan Validation
&lt;/h3&gt;

&lt;p&gt;To make sure we don't accidentally cause any issues, run a &lt;code&gt;terraform plan&lt;/code&gt; to see what changes Terraform thinks need to be made in order to make the local state file match the actual resource.&lt;/p&gt;

&lt;p&gt;Again, using the example case, the plan output looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  # github_repository.jubilant_octo_spork will be updated in-place
  ~ resource "github_repository" "jubilant_octo_spork" {
      - has_downloads = true -&amp;gt; null
      - has_issues = true -&amp;gt; null
      - has_projects = true -&amp;gt; null
      - has_wiki = true -&amp;gt; null
        id = "jubilant-octo-spork"
        name = "jubilant-octo-spork"
        #
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this is telling us is that in GitHub downloads, issues, projects, and wikis are all turned on but we don't have that in our current terraform configuration. In order to correct this, we'll just update our resource to make sure all of those settings match.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "github_repository" "jubilant_octo_spork" {
  name = "jubilant-octo-spork"
  description = "An example of using Terraform in GitHub to manage your GitHub"
  has_downloads = true
  has_issues = true
  has_projects = true
  has_wiki = true
  visibility = "public"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running the &lt;code&gt;terraform plan&lt;/code&gt; again should give us a result that no changes are needed to be made.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No changes. Your infrastructure matches the configuration.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Committing It
&lt;/h3&gt;

&lt;p&gt;From here, there's a lot of rinse and repeat. Add the file and commit to the repo you're working in, push it into source control, and now you have version control around your GitHub resources. This can be applied to all GitHub resources outlined in the &lt;a href="https://registry.terraform.io/providers/integrations/github/latest/docs?ref=ianknighton.com" rel="noopener noreferrer"&gt;Terraform Registry documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;p&gt;In our current setup, this terraform is only manageable from the machine it was created on.&lt;/p&gt;

&lt;p&gt;In your personal environment, that will work fine.&lt;/p&gt;

&lt;p&gt;In a production/enterprise environment, you need to do something different.&lt;/p&gt;

&lt;p&gt;This involves a remote state and maybe even a deployment pipeline. Considering as this has already gone a bit long, I'll come back and put together a post on how to do that step.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>github</category>
    </item>
    <item>
      <title>Sharing Skipboi</title>
      <dc:creator>Ian Knighton</dc:creator>
      <pubDate>Mon, 20 Oct 2025 15:00:09 +0000</pubDate>
      <link>https://forem.com/ianknighton/sharing-skipboi-4akj</link>
      <guid>https://forem.com/ianknighton/sharing-skipboi-4akj</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;At least 40 hours of my week every week is spent in various terminal windows and listening to music on Apple Music. It's the nature of the beast.&lt;/p&gt;

&lt;p&gt;I have two external keyboards (a mechanical and an ergonomic), two laptops with slightly different keyboard form factors, and for funsies a case on my iPad that has an third layout. This isn't to brag, but I think it's important to know.&lt;/p&gt;

&lt;p&gt;No matter how good my playlists are (they're great), sometimes there's a song or two that you want to skip. Sometimes you need to hop on a virtual meeting and you need to pause. All of the aforementioned keyboards have a key for this, but the layouts are different enough that I can't perform those tasks without stopping and looking at the keyboard to remember where "that" button is on "this" keyboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Spark
&lt;/h2&gt;

&lt;p&gt;On a particularly bad playlist run, I found myself skipping every other or every third song. This is more annoying than skipping every other song because it forces a bounce back. Every time I performed this action, I had to leave the "flow" or sometimes the VS Code window I was in to perform the operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/IanKnighton/skipboi?ref=ianknighton.com" rel="noopener noreferrer"&gt;Skipboi&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a CLI tool written in swift that allows you to manipulate functionality around Apple Music without ever leaving your terminal window.&lt;/p&gt;

&lt;p&gt;Play, pause, skip forward, skip backward, enable/disable shuffle, enable/disable repeat (all and one), mute, unmute, volume up, and volume down are all accounted for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Process
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Starting
&lt;/h3&gt;

&lt;p&gt;Similar to &lt;a href="https://ianknighton.com/introducing-institutionalized/" rel="noopener noreferrer"&gt;institutionalized&lt;/a&gt;, I started this with the "jump start your project with Copilot" option when creating the new repo. As I've done this a couple of times, I've gotten a little better about fitting the prompt into the 500 character limit and covering the requirements I have for my "MVP."&lt;/p&gt;

&lt;p&gt;In this case, I realized that I didn't even have swift installed on my machine. I guess I could have let it ride just to see what happened, but I installed Xcode, pulled the branch, and verified the functionality.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building
&lt;/h3&gt;

&lt;p&gt;I won't rehash the same processes I used before, but I will use this opportunity to explain an edge case I hit that shows where the wall is for agentic coding and why the "driver in the loop" is so important.&lt;/p&gt;

&lt;p&gt;This was my first time implementing &lt;a href="https://brew.sh/?ref=ianknighton.com" rel="noopener noreferrer"&gt;Homebrew&lt;/a&gt; in a project. The goal was to make this sit in line with the other tools I used in a way that &lt;code&gt;institutionalized&lt;/code&gt; (at the time) didn't. I understood some of the core functionality, but I decided to leverage the agent to see how far it would get.&lt;/p&gt;

&lt;h4&gt;
  
  
  What It Did Well
&lt;/h4&gt;

&lt;p&gt;It was able to successfully scaffold the Homebrew configuration and document out what I needed to do in order to complete the process. It gave me usage instructions and the whole lot. The instructions were easy to follow and, much to my surprise, worked right out of that gate as it said on the tin.&lt;/p&gt;

&lt;p&gt;In that same documentation, it had a section called "possible future enhancements" with some suggestions as to what could be added to make the user experience better.&lt;/p&gt;

&lt;h4&gt;
  
  
  Where It Fell Apart
&lt;/h4&gt;

&lt;p&gt;One of those suggestions (and an observation I made in the process) was that it required the user to have Xcode installed in order for Homebrew to compile the app. While this isn't a huge deal, it did throw up a potential barrier to a non "power" user that may be trying to use it. The recommendation was to use "&lt;a href="https://docs.brew.sh/Bottles?ref=ianknighton.com" rel="noopener noreferrer"&gt;bottles&lt;/a&gt;" which are pre-compiled binaries that can be installed anywhere as long as there is a binary for it.&lt;/p&gt;

&lt;p&gt;The first pass went okay. It took some coaxing to help the Copilot agent connect what it had already done to what it thought it should do, but we were able to eventually get there. That first pass only covered two versions of macOS, but didn't include the Tahoe which is installed on both of my machines. This meant that I couldn't really test the bottles process and it would default to the previous, somewhat clunky process.&lt;/p&gt;

&lt;p&gt;So the next logical iteration was to add bottles for all currently supported versions of macOS. Through this process there was a lot more coaxing to connect the dots. Once all of the dots were connected, the changes were merged, and the CI issues were fixed it was time to test.&lt;/p&gt;

&lt;p&gt;In testing, I found an issue where the &lt;code&gt;v&lt;/code&gt; in the version number was being dropped one time and not the rest. This meant that &lt;code&gt;v1.2.1&lt;/code&gt; was getting a broken url because the path looked like &lt;code&gt;.../v1.2.1/skipboi-1.2.1.*&lt;/code&gt; which there was no release for. Because this was my first adventure in Homebrew, I asked the Copilot agent to investigate.&lt;/p&gt;

&lt;p&gt;I got to see the most legendary AI crashout I've ever seen.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: maybe I didn't use "crashout" correctly there. I coach a high school team and I'm afraid to ask the rules...I just go on context clues.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Sometimes the agents will do this thing where you ask it to solve a problem, it thinks it has a solution, it tries to process that solution, realizes it's not a solution, and then goes into this loop. It feels like it's talking to itself. A weird computer version of "&lt;a href="https://en.wikipedia.org/wiki/Rubber_duck_debugging?ref=ianknighton.com" rel="noopener noreferrer"&gt;rubberducking&lt;/a&gt;" where it just spirals on potential solutions and talks itself out of the solution.&lt;/p&gt;

&lt;p&gt;Eventually, after some time watching it spin out, it comes back and says "I don't know how to fix this, you're going to have to troubleshoot this." A fun admission from our future robot overlord that we're all somewhat fallible at times.&lt;/p&gt;

&lt;p&gt;To fix this, I dropped the &lt;code&gt;v&lt;/code&gt; from the version number all together. This worked, but I still wanted to see if I could prompt my way through it. To go at it another angle, I created an issue in GitHub and assigned it to the Copilot agent there. I gave it a ton of information and let it go. That agent's solution was to actually gut the entire process in a way that didn't make sense.&lt;/p&gt;

&lt;p&gt;I merged it, it broke everything, and so I reverted it and we're back working. It was worth the "git-fu" to see where the edges are of the LLM's and their ability to actually troubleshoot code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Closing
&lt;/h2&gt;

&lt;p&gt;Again, I want to acknowledge that AI is a complicated matter. I've talked about this elsewhere, but I know that at some point someone who hasn't read anything else I've written is going to come at me about how I'm ruining the planet or putting people out of jobs.&lt;/p&gt;

&lt;p&gt;That said, the current implementation of this application handles everything &lt;em&gt;I think&lt;/em&gt; you could want to do while listening to music. All music functionality as well as the ability to toggle audio settings. I'm working on a way to handle AirPod controls, but it's messy and I don't like it.&lt;/p&gt;

&lt;p&gt;If you're a sicko like me and live in a macOS terminal and listen to Apple Music, give it a go.&lt;/p&gt;

&lt;p&gt;Besos!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>githubcopilot</category>
      <category>github</category>
      <category>chatgpt</category>
    </item>
    <item>
      <title>Introducing Institutionalized</title>
      <dc:creator>Ian Knighton</dc:creator>
      <pubDate>Mon, 29 Sep 2025 15:30:25 +0000</pubDate>
      <link>https://forem.com/ianknighton/introducing-institutionalized-1b65</link>
      <guid>https://forem.com/ianknighton/introducing-institutionalized-1b65</guid>
      <description>&lt;p&gt;&lt;em&gt;A couple weeks back, I wrote a&lt;/em&gt; &lt;a href="https://ianknighton.com/i-vibe-coded-ama/" rel="noopener noreferrer"&gt;&lt;em&gt;post&lt;/em&gt;&lt;/a&gt; &lt;em&gt;about vibe coding. It was something simple and stupid and it was an experiment in using the Copilot chat window in VS Code. It was fun, but the more I thought about it, the more I felt like I needed to really push it.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;In the last year I've probably made a few thousand &lt;code&gt;git&lt;/code&gt; commits. I think anyone working on software and using &lt;code&gt;git&lt;/code&gt; as their source control, probably has.&lt;/p&gt;

&lt;p&gt;In the last year, we also started rolling out more AI tools at work with an emphasis on using them.&lt;/p&gt;

&lt;p&gt;Combine all that with a &lt;a href="https://github.com/kardolus/chatgpt-cli?ref=ianknighton.com" rel="noopener noreferrer"&gt;ChatGPT CLI&lt;/a&gt; made by &lt;a href="https://github.com/kardolus?ref=ianknighton.com" rel="noopener noreferrer"&gt;kardolus&lt;/a&gt; (including their &lt;a href="https://github.com/kardolus/prompts?ref=ianknighton.com" rel="noopener noreferrer"&gt;prompt&lt;/a&gt; repository), I had come up with a Rube Goldberg machine of a command that would look at &lt;code&gt;git status&lt;/code&gt;, check the diffs, and write a pull request description which would then pipe into the GitHub CLI.&lt;/p&gt;

&lt;p&gt;It worked about 40% of the time and I eventually just kind of abandoned the setup...but not the idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Spark
&lt;/h2&gt;

&lt;p&gt;In the aforementioned experiment with agentic coding, I came across a way to have the agent create commit messages and PR descriptions. It was actually kind of a neat feature since they were very descriptive and, most importantly, didn't take a whole lot of work.&lt;/p&gt;

&lt;p&gt;That re-ignited my drive for a tool that would let me make all of the code changes, but not make me do the boring part of writing descriptive commit messages and adequate pull request descriptions.&lt;/p&gt;

&lt;p&gt;So I took some time over the weekend and started mucking about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/IanKnighton/institutionalized?ref=ianknighton.com" rel="noopener noreferrer"&gt;institutionalized&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's a CLI written in Go that will leverage different LLM API's to create your commit messages and pull request descriptions. It's relatively easy to install, relatively easy to configure, and has logic setup to handle three different LLM providers.&lt;/p&gt;

&lt;p&gt;The command is maybe a little wordy, but I couldn't stop thinking about that time that Mike wanted just one Pepsi.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Process
&lt;/h2&gt;

&lt;p&gt;I think this is the real meat of everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Starting
&lt;/h3&gt;

&lt;p&gt;I created the repository and used the "jump start your project with Copilot" option when you create a new repository. I threw in a prompt with the basic requirements and let it go.&lt;/p&gt;

&lt;p&gt;Thirty minutes or so I had a pull request waiting for me that had a base program that connected to the OpenAI API and was able to create a commit message and complete the commit.&lt;/p&gt;

&lt;p&gt;From a project management standpoint, this was an MVP. I felt like "yeah, this will work from here, but I want some more options."&lt;/p&gt;

&lt;h3&gt;
  
  
  Building
&lt;/h3&gt;

&lt;p&gt;From there, I used a combination of a couple different ways to go about doing this:&lt;/p&gt;

&lt;h4&gt;
  
  
  GitHub Issues Assigned to Copilot
&lt;/h4&gt;

&lt;p&gt;The premise is pretty simple (if you've been around software engineering and project management for a while). You write a user story with your requirements, acceptance criteria, and any other notes. You assign it to Copilot and sometime late you get a pull request to review. Once complete, you can go back and check the logic it used and the steps it went through to get there. If it's all above board, you merge the pull request and you're there.&lt;/p&gt;

&lt;p&gt;While this is by far away the easiest, it also feels like the most "you need to know what you're looking at" factor. I'm not super proficient in Go, which is why I used it instead of my beloved .Net. I wanted to have to really dive in and make sure that things made sense and that I could understand them. I had to really review them. If you were doing this without a solid foundation in programming, you're kind of flying blind that it works as expected and has any degree of maintainability.&lt;/p&gt;

&lt;h4&gt;
  
  
  Copilot Chat in VS Code
&lt;/h4&gt;

&lt;p&gt;Again, it's pretty simple. Just like other GPTs, you tell it what you want and it will work through iteration with you. Start broad or start narrow and it will just go. Along the way it will prompt you for any commands it needs to run and check with you to make sure you want to keep going. When all is said and done it will explain to you what it did and in some propose some next steps for you.&lt;/p&gt;

&lt;p&gt;This feels more like actually coding. You're closer to the metal as you watch the process go and you can interject when you see things start to go sideways. The visibility is pretty awesome.&lt;/p&gt;

&lt;p&gt;What I found difficult was that it (somewhat regularly) can get stuck in a loop. In another case I was working with it to write tests and it was stuck in a loop trying to change a property name from camel-case to pascal-case. I would stop the agent, re-prompt it to solve the problem, and it would get stuck in the loop again.&lt;/p&gt;

&lt;p&gt;The other issue I ran into was context. When you use Copilot to work though an issue, it maintains its own context as it iterates. If you need it to fix something, it can check what it did and the original prompt and work from there. When using the chat, it seems to lose context more often. You can find it doing things like forgetting names for variables or re-doing the same thing twice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Completion
&lt;/h3&gt;

&lt;p&gt;Moving back and forth between the two, I was able to iterate and expand to a point that I hit everything I had originally wanted in the tool. A lot of these were from observations while iterating and a lot of these were just "nice to haves" that I kind of knew would be there.&lt;/p&gt;

&lt;p&gt;I had Copilot help me create some CI workflows and it's ready for prime time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Concerns
&lt;/h2&gt;

&lt;p&gt;I think the environmental, socio-economic, psychological, and educational issues with using AI tools across the board has been documented by people way smarter than me. However, as I said previously, this is the reality of the industry I'm in. I'll set all of that aside for now and try to make peace with myself on those other fronts.&lt;/p&gt;

&lt;p&gt;I do have concerns about the actual usage of these tools though.&lt;/p&gt;

&lt;p&gt;The first is that I think there's a level of comfort it creates that can be misleading. In cases of data security or logic, I've found it can get itself very twisted. So it may "work," but it wouldn't pass a basic test of logic or security. It would also struggle to scale and undoing those decisions would be costly to fix. If you don't have a base understanding of the tools/languages it is using, it can be incredibly difficult to troubleshoot on your own and if you try to have the tooling do it, there can be further obfuscation.&lt;/p&gt;

&lt;p&gt;The second is the lack of visibility it can create. If you use GitHub Issues, the commits and pull requests show they were performed by the AI agent. If you use the VS Code plugin, the same changes appear to come from you. The implication here is that you may lose visibility into what/who did what to your codebase. This could become an issue when working in a team and you're trying to chase down an issue or a change. You could find yourself in a loop of introducing a change that no one knows how to fix and the AI just loops trying to fix it. There has to be a very solid "driver in the loop" to keep it honest and validate everything happening.&lt;/p&gt;

&lt;p&gt;The third is, and I said this before, it's a junior engineer at best. You give it a well defined task and it complete it. You review it heavily, peer test, and send it if it's all above board. Sometimes you ask for fixes and it makes the problem worse and you have to guide it back. This means the window for junior engineers is closing, which is bad for an aging industry in the middle of a jobs crisis.&lt;/p&gt;

&lt;p&gt;The final is the actual cost. I can't actually nail down what the cost is for this functionality in GitHub. It looks like there is no cost to the right accounts until November. This has the potential to really bite you if you're not keeping an eye on your billing and budgets in GitHub.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Closing
&lt;/h2&gt;

&lt;p&gt;In the right hands, I think these tools are pretty handy. There are cases that I've used it to work through issues that I've been struggling with for months.&lt;/p&gt;

&lt;p&gt;A GitHub Action introduced a breaking change and didn't really document the work-around that makes sense for my environment? Ask the chat agent to take a swing at fixing it.&lt;/p&gt;

&lt;p&gt;That Action needs to be updated across 34 Workflows? Ask the agent to write a script to update all of them.&lt;/p&gt;

&lt;p&gt;I believe it also creates a nicer way to boilerplate your projects.&lt;/p&gt;

&lt;p&gt;I want to build a data tool that has a .Net 8 backend, a PostgeSQL database, an Angular frontend, and I want to use &lt;code&gt;docker-compose&lt;/code&gt; to run the whole thing? Prompt upon repository creation and start from there. I can build from there without the tedium of scaffolding out everything on my own and trying to remember where that one change is you need to make in the &lt;code&gt;Dockerfile&lt;/code&gt; to use the right version of NPM.&lt;/p&gt;

&lt;p&gt;Will I keep using it? Probably. It's the nature of the industry right now and I have a backlog of things that need to be done without the time to do them. It also makes the barrier to starting new ideas (this is all a case in point) much lower.&lt;/p&gt;

&lt;p&gt;Anyways, I hope the overlords eventually read this post and decide to take pity on me.&lt;/p&gt;

&lt;p&gt;Besos robots!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>chatgpt</category>
      <category>githubcopilot</category>
      <category>github</category>
    </item>
    <item>
      <title>I Vibe Coded...AMA</title>
      <dc:creator>Ian Knighton</dc:creator>
      <pubDate>Wed, 17 Sep 2025 17:00:34 +0000</pubDate>
      <link>https://forem.com/ianknighton/i-vibe-codedama-3ehn</link>
      <guid>https://forem.com/ianknighton/i-vibe-codedama-3ehn</guid>
      <description>&lt;p&gt;There is a time and a place to talk about the various levels of damage that AI is causing. There is also a time and a place to acknowledge that we can't let the genie out of the bottle and no matter how much you resist, it will be part of your life henceforth and you better know how it works.&lt;/p&gt;

&lt;p&gt;I've been in the industry long enough to have seen this time and time again.&lt;/p&gt;

&lt;p&gt;That all said, I decided I would take a crack at "vibe coding" a &lt;a href="https://github.com/Knighton-Dev/jsonencoder?ref=ianknighton.com" rel="noopener noreferrer"&gt;tool&lt;/a&gt; that I needed to flesh out some stuff at work and I want to talk about the experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's easy, but not simple.
&lt;/h2&gt;

&lt;p&gt;Yes, if you have the correct tools and IDE and licenses and whatever you can basically say "hey, I need a thing that does a thing" and it will go and it will get you 90% of the way there. That 90% is probably adequate for most people.&lt;/p&gt;

&lt;p&gt;The last 10% can be a bit of a slog.&lt;/p&gt;

&lt;p&gt;In the test case, I needed to be able to validate a JSON string could be encoded and decoded in Go. The base output from the first prompt &lt;em&gt;mostly&lt;/em&gt; did that. It didn't take me long to start finding edge cases.&lt;/p&gt;

&lt;p&gt;What happens if there's a special character?&lt;/p&gt;

&lt;p&gt;What happens if the file is incorrectly formatted?&lt;/p&gt;

&lt;p&gt;Can the output be minified?&lt;/p&gt;

&lt;p&gt;These are all considerations that someone with the amount of experience I have are going to want to resolve as quickly as possible as we know that those cases aren't really that close to the edge. They're things that are going to have to be resolved.&lt;/p&gt;

&lt;p&gt;Again, the tool will you you 90% of the way there. So, because of diminishing returns, you're at 99% of the way there. So yeah, you're probably in a pretty good place, but you're not 100%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scope creep.
&lt;/h2&gt;

&lt;p&gt;So you started with this simple idea and then as you started working and solving little test cases, you started also seeing features that were missing.&lt;/p&gt;

&lt;p&gt;This is scope creep.&lt;/p&gt;

&lt;p&gt;So yeah, I have a tool that will take JSON output JSON. It has some tests and it probably passes the low bar we set for it.&lt;/p&gt;

&lt;p&gt;This is where I think the "vibes" start to kind of fall apart.&lt;/p&gt;

&lt;p&gt;You have to start knowing that you're asking for, what that means, and what the ramifications are to your existing code.&lt;/p&gt;

&lt;p&gt;You want to add base64 encoding? That's doable...but you first need to know how to prompt for that, how to work with the agent to add that functionality, and then how to validate that the testing works in both directions. At that juncture, you have to have an understanding of the language you're working in and where all of the knock-ons are.&lt;/p&gt;

&lt;p&gt;So yes, the AI agent can write the code and write some tests, but you have to be able to validate the code works and that the tests are actually valuable.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's a junior engineer.
&lt;/h2&gt;

&lt;p&gt;The datasets are trained off of public code. Public code isn't "enterprise" and it inherently carries with it some standards that you may not see in an enterprise environment.&lt;/p&gt;

&lt;p&gt;This can be a real issue if you're working on a tool for an enterprise environment.&lt;/p&gt;

&lt;p&gt;We walked it through all of the tests and all of the functionality and we've made sure that it's all valid.&lt;/p&gt;

&lt;p&gt;Did it update the documentation?&lt;/p&gt;

&lt;p&gt;Did it add the necessary CI/CD steps?&lt;/p&gt;

&lt;p&gt;Are there any comments in the code that explains what it's doing?&lt;/p&gt;

&lt;p&gt;Again...it gets you there...but you have to know all of these other things are there and what to check for on them.&lt;/p&gt;

&lt;h2&gt;
  
  
  In summary.
&lt;/h2&gt;

&lt;p&gt;I'm all for democratizing creativity in any way possible. I don't like the environmental toll that this is taking and the lack of any regulation around it, but I also realize that's a fight that I can't sway in either way.&lt;/p&gt;

&lt;p&gt;The ultimate questions are these:&lt;/p&gt;

&lt;p&gt;Will it get the job done?&lt;/p&gt;

&lt;p&gt;Will I use it going forward?&lt;/p&gt;

&lt;p&gt;To answer the first...I think I've kind of already done that. I think if you know how to ask the questions and what questions to ask, you can probably get to 99% and just stay at 99%. That's the case for a lot of development work. The thing is that &lt;strong&gt;you&lt;/strong&gt; need to know how to ask those questions and what questions to ask.&lt;/p&gt;

&lt;p&gt;The second question is...probably? Honestly, I don't really know. It was nice to kind of offload some of this, but I still had to be engaged with the window in order to permit it to run tests and things. I still had to handle all of the source control and then test it on my own to find the cases that the tests may have missed. I probably could have made it to the same place just using Copilot in VS Code, but I was able to do this asynchronously, which I guess was nice.&lt;/p&gt;

&lt;p&gt;There...now I've talked about vibe coding and, &lt;em&gt;eventually&lt;/em&gt;, this post will be on LinkedIn which will check some set of arbitrary boxes for the modern software engineer.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>discuss</category>
      <category>programming</category>
    </item>
    <item>
      <title>Using Ephemeral Resources in Terraform</title>
      <dc:creator>Ian Knighton</dc:creator>
      <pubDate>Wed, 30 Apr 2025 23:30:24 +0000</pubDate>
      <link>https://forem.com/ianknighton/using-ephemeral-resources-in-terraform-1km7</link>
      <guid>https://forem.com/ianknighton/using-ephemeral-resources-in-terraform-1km7</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1558932130-308a12148740%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDF8fGVwaGVtZXJhbHxlbnwwfHx8fDE3NDYwNTA1MjR8MA%26ixlib%3Drb-4.0.3%26q%3D80%26w%3D2000" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1558932130-308a12148740%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDF8fGVwaGVtZXJhbHxlbnwwfHx8fDE3NDYwNTA1MjR8MA%26ixlib%3Drb-4.0.3%26q%3D80%26w%3D2000" alt="Using Ephemeral Resources in Terraform" width="2000" height="1333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Last year, I did a workshop at B-Sides in Idaho Falls on secrets management in Terraform. I had a pretty solid outline and felt confident in my ability to "do it live." What I hadn't counted on was a provider in an underlying module being deprecated that week and completely blowing the whole thing to bits.&lt;/p&gt;

&lt;p&gt;It was rough.&lt;/p&gt;

&lt;p&gt;Maybe this small discovery in the registry with a quick implementation is my shot at redemption.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(As always, the solution is up on&lt;/em&gt; &lt;a href="https://github.com/IanKnighton/special-dollop?ref=ianknighton.com" rel="noopener noreferrer"&gt;&lt;em&gt;GitHub&lt;/em&gt;&lt;/a&gt; &lt;em&gt;so you can just look at it and not have to read the SEO slop.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Whether you were aware or not, Terraform stores everything in the state file. The state file is just JSON. So while a &lt;code&gt;terraform state show resource.name&lt;/code&gt; may show &lt;code&gt;(sensisitve_data)&lt;/code&gt; when you look at it, the JSON file knows what the value is. It has to. That has always meant locking down your remote states and using local states for anything super critical or sensitive. These both come with their own tradeoffs.&lt;/p&gt;

&lt;p&gt;This is an issue that has always been on my radar. I believe everything can or should be done through code, so that little security wrinkle is something I could never let go. Likewise, I think that you should be able to use tools like Terraform to manage your keys and rotation of those keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;While taking another crack at this idea of rotating keys as code (RKaC?), I discovered a new resource type in the Terraform registry for the &lt;a href="https://registry.terraform.io/providers/hashicorp/random/latest/docs/ephemeral-resources/password?ref=ianknighton.com" rel="noopener noreferrer"&gt;random provider&lt;/a&gt;. The provider has had the ability to create a password, but this idea of an ephemeral password was pretty exciting. This new type provides the ability to create a resource without the sensitive data being stored in the state at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Implementation
&lt;/h2&gt;

&lt;p&gt;I'll skip talking about the &lt;code&gt;main.tf&lt;/code&gt; file because it's pretty dead simple.&lt;/p&gt;

&lt;p&gt;What will talk about though is the &lt;code&gt;resources.tf&lt;/code&gt; file, which is where all the magic happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ephemeral "random_password" "password_update" {
  length = 16
  special = false
  upper = true
  lower = true
  numeric = true
}

resource "google_secret_manager_secret" "ephemeral_secret" {
  secret_id = "my-secret-id" // Replace with your secret ID
  replication {
    auto {}
  }
}

resource "google_secret_manager_secret_version" "ephemeral_secret_version" {
  secret = google_secret_manager_secret.ephemeral_secret.id
  secret_data_wo = ephemeral.random_password.password_update.result
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We create a random password that is ephemeral. This means that it will only be created on the next &lt;code&gt;terraform apply&lt;/code&gt; and will be forgotten/ignored. &lt;/li&gt;
&lt;li&gt;We create a new secret in Google Secret Manager.&lt;/li&gt;
&lt;li&gt;We create a new version of the secret using the result from the &lt;code&gt;random&lt;/code&gt; provider creating the password and sets it to the &lt;code&gt;secret_data_wo&lt;/code&gt; which makes sure that the information is not stored in the state file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On the next &lt;code&gt;terraform apply&lt;/code&gt;, the password will be created and once the apply has completed it will be forgotten and that object itself will be ignored going forward. If you check the state file, you will see that there is no mention of the &lt;code&gt;random_password.password_update&lt;/code&gt; resource and you will see that the &lt;code&gt;secret_data_wo&lt;/code&gt; field for &lt;code&gt;google_secret_manager_secret_version.ephemeral_secret_version&lt;/code&gt; has no value. This means that your state file is completely clean and the only thing that knows the value of the password is your secrets manager.&lt;/p&gt;

&lt;p&gt;Assuming your IAM is configured correctly and you're using LUA and all of those other fun acronyms...&lt;/p&gt;

&lt;h2&gt;
  
  
  A Closing Thought
&lt;/h2&gt;

&lt;p&gt;This is a big move for Terraform. I looked and the GCS Provider has also started adding &lt;a href="https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/using_ephemeral_resources?ref=ianknighton.com" rel="noopener noreferrer"&gt;ephemeral resources&lt;/a&gt; for keys and secrets. The less that information is in the state file, the better.&lt;/p&gt;

&lt;p&gt;That all said, I can not stress enough the nature of this resource is exactly what is says on the tin...ephemeral. If you goof up and need it to come back, it won't, you'll just have to create a new resource. It's a price worth paying, but a fact worth knowing.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>googlecloud</category>
      <category>secretmanagement</category>
    </item>
    <item>
      <title>AWS Nuances in Terraform</title>
      <dc:creator>Ian Knighton</dc:creator>
      <pubDate>Thu, 03 Apr 2025 14:00:35 +0000</pubDate>
      <link>https://forem.com/ianknighton/aws-nuances-in-terraform-186p</link>
      <guid>https://forem.com/ianknighton/aws-nuances-in-terraform-186p</guid>
      <description>&lt;p&gt;In tandem with my recent post on &lt;a href="https://ianknighton.com/reusing-providers-in-terraform/" rel="noopener noreferrer"&gt;Reusing Providers in Terraform&lt;/a&gt;, I used the example of working with the Terraform AWS provider. It may come as a shock, but it's because I was working on implementing some resources in AWS that we hadn't done much with in the past. Through this process, I learned a couple of things...&lt;/p&gt;

&lt;h2&gt;
  
  
  Regions Matter
&lt;/h2&gt;

&lt;p&gt;Unlike other providers that I've used, it appears that regions are incredibly important to AWS. If you have a resource in one region and it's trying to contact or append to a resource in another region, you can run into all kinds of weird errors.&lt;/p&gt;

&lt;p&gt;In one case, we couldn't use a resource in &lt;code&gt;us-east-1&lt;/code&gt; and have it connect to a resource in &lt;code&gt;us-west-1&lt;/code&gt; without it trying to do some redirect magic. So it's all well and good to be able to use the aliased providers, but depending on your dependency tree, it may just be best to put everything in one region.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secrets Live
&lt;/h2&gt;

&lt;p&gt;In other providers, destroying a resource sends it into the void. You say "vaya con dios" and then never think about that thing again.&lt;/p&gt;

&lt;p&gt;This is mostly true in AWS, with the exception of Secrets Manager. When a secret is deleted, it has a default delay of 30 days before it is actually removed. So it's possible to remove the resource, have it no longer in your state or visible through the web console, but be unable to recreate the resource due to an existing resource with the same name.&lt;/p&gt;

&lt;p&gt;It was only upon deleting a secret through the console that I learned about this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resource Linking
&lt;/h2&gt;

&lt;p&gt;The last thing, and this is pretty fringe, was I ran into an issue with using resources in other resources. Specifically in the case of setting IAM policies on a resource.&lt;/p&gt;

&lt;p&gt;This was kind of a silly thing and I was already not positive it would work, but the error handling wasn't great. I kept seeing an error on initialization of terraform that just said "cycle" and two resource names. It took me a second to figure out what it was trying to tell me.&lt;/p&gt;




&lt;p&gt;Do with this what you will.&lt;/p&gt;

&lt;p&gt;Just want to keep this out there for when I forget next week.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
    </item>
    <item>
      <title>Reusing Providers in Terraform</title>
      <dc:creator>Ian Knighton</dc:creator>
      <pubDate>Mon, 24 Mar 2025 13:00:12 +0000</pubDate>
      <link>https://forem.com/ianknighton/reusing-providers-in-terraform-2438</link>
      <guid>https://forem.com/ianknighton/reusing-providers-in-terraform-2438</guid>
      <description>&lt;p&gt;&lt;em&gt;(if you want to skip the BS,&lt;/em&gt;&lt;a href="https://github.com/IanKnighton/multi-provider-example?ref=ianknighton.com" rel="noopener noreferrer"&gt;&lt;em&gt;here's an example&lt;/em&gt;&lt;/a&gt; &lt;em&gt;in my GitHub)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I recently came into a situation where I needed to import multiple S3 buckets from AWS. For the most part, importing the resources is pretty straightforward. Especially when compared to other service providers.&lt;/p&gt;

&lt;p&gt;However, in this case the resources were all over the place as far as regions. This means that you couldn't &lt;em&gt;necessarily&lt;/em&gt; use a default configuration to import and manage the resources. This is a weird limitation in the provider where it &lt;em&gt;seems&lt;/em&gt; that the import can only search the region of the default provider unless told otherwise.&lt;/p&gt;

&lt;p&gt;Anyways, since this is a thing I've ran into more than once, it felt important to document it somewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some Assumptions
&lt;/h2&gt;

&lt;p&gt;I'm operating off the assumption that you have &lt;a href="https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli?ref=ianknighton.com" rel="noopener noreferrer"&gt;Terraform&lt;/a&gt; and (&lt;em&gt;probably&lt;/em&gt;) the &lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html?ref=ianknighton.com" rel="noopener noreferrer"&gt;AWS&lt;/a&gt; CLIs installed.&lt;/p&gt;

&lt;p&gt;I'm also operating on the assumption that you already have at least the &lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs?ref=ianknighton.com" rel="noopener noreferrer"&gt;base configuration&lt;/a&gt; of the AWS provider working with terraform. You'll need a secret key and all of that setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Main.tf
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = "~&amp;gt; 5.0"
    }
  }
}

# The us-east-1 instance
provider "aws" {
  region = "us-east-1"
}

# Create a bucket in us-east-1
resource "aws_s3_bucket" "east_1_example" {
  bucket = "east-1-example"

  tags = {
    Environment = "Dev"
  }
}

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

&lt;/div&gt;



&lt;p&gt;If you've worked in Terraform, a lot of this is straight forward.&lt;/p&gt;

&lt;p&gt;First, we're establishing our required provider as &lt;code&gt;"hashicorp/aws"&lt;/code&gt; so we can import that provider and the correct version.&lt;/p&gt;

&lt;p&gt;The provider requires:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;region&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;access_key&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;secret_key&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Obviously, we &lt;strong&gt;NEVER HARDCODE THE ACCESS OR SECRET KEY INTO THE CONFIG&lt;/strong&gt; so we've passed those in as environment variables and just set the provider's region to &lt;code&gt;us-east-1&lt;/code&gt;. This means that all resources will be created in that region and, importantly, when importing resources you'll only be able to import resources from that region.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alias
&lt;/h2&gt;

&lt;p&gt;This is the thing that took me too long to figure out. &lt;em&gt;In the old days&lt;/em&gt;, you used to configure a second provider name. At least I think that's how it worked. Honestly, it's not something I had to do a lot until recently. Now, we can split a provider using &lt;code&gt;alias&lt;/code&gt; in the provider configuration.&lt;/p&gt;

&lt;p&gt;Here's an updated version of the &lt;code&gt;main.tf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = "~&amp;gt; 5.0"
    }
  }
}

# The us-east-1 instance
provider "aws" {
  alias = "east-1"
  region = "us-east-1"
}

# Create a bucket in us-east-1
resource "aws_s3_bucket" "east_1_example" {
  provider = aws.east-1
  bucket = "east-1-example"

  tags = {
    Environment = "Dev"
  }
}

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

&lt;/div&gt;



&lt;p&gt;This version is pretty much the same. The big change is that we're adding the &lt;code&gt;alias&lt;/code&gt; property to the provider configuration and setting it to &lt;code&gt;east-1&lt;/code&gt;. Essentially, we're giving it another name.&lt;/p&gt;

&lt;p&gt;To use that, we add the &lt;code&gt;provider&lt;/code&gt; property to the storage bucket and specify we want to use the &lt;code&gt;aws.east-1&lt;/code&gt; provider. Again, this is masking (&lt;em&gt;aliasing...wow&lt;/em&gt;) and telling the resource we want to use the &lt;code&gt;aws&lt;/code&gt; provider with the name &lt;code&gt;east-1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In this configuration, we could have a provider with no alias and that would be the default and then we could work from there.&lt;/p&gt;

&lt;p&gt;To that end, let's create a bucket in &lt;code&gt;us-west-1&lt;/code&gt; using an aliased provider.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = "~&amp;gt; 5.0"
    }
  }
}

# The us-east-1 instance
provider "aws" {
  region = "us-east-1"
}

# The us-west-1 instance
provider "aws" {
  alias = "west-1"
  region = "us-west-1"
}

# Create a bucket in us-east-1
resource "aws_s3_bucket" "east_1_example" {
  bucket = "east-1-example"

  tags = {
    Environment = "Dev"
  }
}

# Create a bucket in us-west-1
resource "aws_s3_bucket" "west_1_bucket" {
  provider = aws.west-1
  bucket = "west-1-example"

  tags = {
    Environment = "Dev"
  }
}

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

&lt;/div&gt;



&lt;p&gt;This example will create a bucket in &lt;code&gt;us-east-1&lt;/code&gt; as that's our default and a second in &lt;code&gt;us-west-1&lt;/code&gt; using the aliased provider. In the case of an import, specifying the &lt;code&gt;provider&lt;/code&gt; in the resource &lt;strong&gt;prior&lt;/strong&gt; to import will make sure the &lt;code&gt;terraform import&lt;/code&gt; command looks in the right region for the resource.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;I don't know how many times I've run into this in the last few months. If you made it this far, that's great. I'm sure I'll be back in here in a few months trying to remember how I did this.&lt;/p&gt;

&lt;p&gt;I hope you have a nice day.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>aws</category>
      <category>s3</category>
    </item>
    <item>
      <title>PagerDuty Change Events from Azure Release Pipelines</title>
      <dc:creator>Ian Knighton</dc:creator>
      <pubDate>Thu, 27 Jan 2022 21:27:44 +0000</pubDate>
      <link>https://forem.com/ianknighton/pagerduty-change-events-from-azure-release-pipelines-29k7</link>
      <guid>https://forem.com/ianknighton/pagerduty-change-events-from-azure-release-pipelines-29k7</guid>
      <description>&lt;p&gt;As our team and our architecture has grown, so has the need for better managed and streamlined systems. A big push we've been making in the last few months is Incident Management and On Call Scheduling. At this point I think we've tried every tool/combination of tools and we're finally settling in with PagerDuty. &lt;/p&gt;

&lt;p&gt;For me, one of the big selling features of PagerDuty was the ability to add change events to your services. The forensics of an incident are a lot easier if you can quickly rule a recent release out of the realm of possibility. It's also helpful to know how often and frequently releases are happening to a certain service. &lt;/p&gt;

&lt;p&gt;Unfortunately, Azure DevOps didn't really have anything out of the box to send POST requests to an API when a release finishes. I also couldn't find anything in their marketplace that would fill the need I had. &lt;/p&gt;

&lt;p&gt;So I made &lt;a href="https://marketplace.visualstudio.com/items?itemName=KnightonDev.send-change-event" rel="noopener noreferrer"&gt;something&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;It's a very simple extension for Azure Pipelines and Releases that allows you to send an update to the service in PagerDuty. If you want to configure it to do so, it will send an event message for every state (succeeded, failed, canceled) and most of the fields are customizable to do whatever you need with them. &lt;/p&gt;

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

&lt;p&gt;I know that this is probably a very narrowcast post. I can't help but think this isn't an original idea or need, so I wanted to put it out there to the community in case anyone can find uses for it in their own environment.&lt;/p&gt;

&lt;p&gt;It's also been a &lt;em&gt;hot minute&lt;/em&gt; since I've contributed here in any meaningful way, so consider this a toe dipped back into the water just before my three year anniversary. &lt;/p&gt;

</description>
      <category>pagerduty</category>
      <category>azuredevops</category>
      <category>extensions</category>
    </item>
    <item>
      <title>Hosting a .Net Core App with Nginx and Let's Encrypt</title>
      <dc:creator>Ian Knighton</dc:creator>
      <pubDate>Tue, 07 May 2019 16:50:34 +0000</pubDate>
      <link>https://forem.com/ianknighton/hosting-a-net-core-app-with-nginx-and-let-s-encrypt-1m50</link>
      <guid>https://forem.com/ianknighton/hosting-a-net-core-app-with-nginx-and-let-s-encrypt-1m50</guid>
      <description>&lt;p&gt;&lt;a href="https://dev.to/ianknighton/event-registration-web-app-with-net-core-and-stripe-39ib"&gt;In the last post&lt;/a&gt;, we built a simple .Net Core application to handle payment processing for our event registration. It works like a champ on our local machine, but it doesn't do us any good if no one can access it.&lt;/p&gt;

&lt;p&gt;Today, we'll make it available to the public.&lt;/p&gt;

&lt;p&gt;I mostly started with this helpful article in the &lt;a href="https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/linux-nginx?view=aspnetcore-2.2" rel="noopener noreferrer"&gt;Microsoft Docs&lt;/a&gt;. However, I found some issues that I had to work around.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Quick Update to the App
&lt;/h2&gt;

&lt;p&gt;Using Nginx, we'll need to be able to reverse proxy, so we need to add a little bit of code to our project to handle this. In &lt;code&gt;startup.cs&lt;/code&gt;, we'll add to the &lt;code&gt;Configure&lt;/code&gt; method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseForwardedHeaders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ForwardedHeadersOptions&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ForwardedHeaders&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ForwardedHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;XForwardedFor&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;ForwardedHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;XForwardedProto&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You may need to add to your using statements.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.AspNetCore.Hosting&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.AspNetCore.HttpOverrides&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 can carry on to building out the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up the Server
&lt;/h2&gt;

&lt;p&gt;I started with a &lt;a href="https://www.linode.com/?r=bddbeba21bd71930c65c4a52ab8d6426acd3c1d7" rel="noopener noreferrer"&gt;Linode&lt;/a&gt; "Nanode" server with Ubuntu 18.04 installed. In my case, this page only needs to be available for three months and won't see a ton of traffic, so the small size and smaller cost ($5 a month) really suits what I'm trying to do.&lt;/p&gt;

&lt;p&gt;Go through your normal setup (update, upgrade, secure ssh, etc...) and then we'll install the other pieces we need.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing Dotnet on the Server
&lt;/h3&gt;

&lt;p&gt;Again, I referenced &lt;a href="https://dotnet.microsoft.com/download/linux-package-manager/ubuntu18-04/sdk-current" rel="noopener noreferrer"&gt;Microsofts Docs&lt;/a&gt; for this, but I condensed the commands together for the sake of brevity.&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="go"&gt;wget -q https://packages.microsoft.com/config/ubuntu/18.04/packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
sudo add-apt-repository universe
sudo apt-get install apt-transport-https
sudo apt-get update
sudo apt-get install dotnet-sdk-2.2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Copying Code and Building
&lt;/h3&gt;

&lt;p&gt;Depending on how intense you may want to go with this, you may want to build out a full CI pipeline. In my case, I'm not going to be doing any iterations, so I just copied the code over using &lt;code&gt;scp&lt;/code&gt; and ran it on the server.&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="go"&gt;scp -r ~/path/to/directory ssh@server:~/appname
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That command (once populated with your information) should copy the entire folder from your machine to the server in the SSH users home directory. &lt;/p&gt;

&lt;p&gt;SSH into the server and build/publish the application.&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="go"&gt;dotnet build
dotnet publish --configuration Release
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Publish should move everything into a folder in the &lt;code&gt;bin/Release/netcore2.2/&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;Once that is done and successful, I then use symbolic linking to put it in &lt;code&gt;/var/www&lt;/code&gt;. It's not necessary to do it, but it's an old habit.&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="go"&gt;sudo ln -s ~/appname/bin/Release/netcore2.2/publish /var/www/appname
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This links the folder in the home directory to the &lt;code&gt;/var/www/&lt;/code&gt; directory, so if you do need to make fixes, there's no worry about copying things around.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing and Setting Up CertBot
&lt;/h3&gt;

&lt;p&gt;Certbot is pretty straight forward and it's a beautiful tool I've written a bit about.&lt;/p&gt;

&lt;p&gt;First, install through &lt;code&gt;apt&lt;/code&gt;.&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="go"&gt;sudo apt install -y certbot
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once that is done, we can request a certificate. I prefer to use the &lt;code&gt;standalone&lt;/code&gt; method.&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="go"&gt;sudo certbot certonly --standalone -d yourdomain.com -d www.yourdomain.com
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once that is finished, you should be able to run &lt;code&gt;sudo certbot certificate&lt;/code&gt; and see the location of the certificate on your machine.  It's important to keep this somewhere, we'll need it in the next step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install and Configure Nginx
&lt;/h3&gt;

&lt;p&gt;We have code, we have a certificate, now we just need to make it available to the adoring masses.&lt;/p&gt;

&lt;p&gt;First, install Nginx:&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="go"&gt;sudo apt install nginx
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, make sure you can access it through your firewall. Assuming you're using &lt;code&gt;ufw&lt;/code&gt;, we can just update the permissions.&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="go"&gt;sudo ufw allow 'Nginx Full'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, if you navigate to the IP address of your server, you should see the "welcome to nginx" screen.&lt;/p&gt;

&lt;p&gt;Once you've verified that you're able to access the server, you'll want to make sure your domain is pointed to the IP. This is going to vary wildly based on what your DNS provider is, but I believe in you.&lt;/p&gt;

&lt;p&gt;With a domain, we can now handle setting up Nginx for our site. I'll save the boring details of how all of this works, but here's what we need to do.&lt;/p&gt;

&lt;p&gt;Create a file in &lt;code&gt;/etc/nginx/sites-available/&lt;/code&gt;:&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="go"&gt;sudo nano /etc/nginx/sites-available/yourdomain.conf
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the text editor, copy in this configuration:&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="go"&gt;server { 
&lt;/span&gt;&lt;span class="gp"&gt;    listen 80;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;    server_name yourdomain.com;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;    return 301 https://$&lt;/span&gt;host&lt;span class="nv"&gt;$request_uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt;}

server {
&lt;/span&gt;&lt;span class="gp"&gt;    listen 443;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;    server_name yourdomain.com;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;    ssl_certificate           /etc/letsencrypt/live/yourdomain.com/fullchain.pem;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;    ssl_certificate_key       /etc/letsencrypt/live/yourdomain.com/privkey.pem;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;    ssl on;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;    ssl_session_cache  builtin:1000  shared:SSL:10m;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;    ssl_protocols  TLSv1 TLSv1.1 TLSv1.2;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;    ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;    ssl_prefer_server_ciphers on;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;    gzip  on;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;    gzip_http_version 1.1;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;    gzip_vary on;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;    gzip_comp_level 6;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;    gzip_proxied any;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;    gzip_types text/plain text/html text/css application/json application/javascript application/x-javascript text/javascript text/xml application/xml application/rss+xml application/atom+xml application/rdf+xml;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;    gzip_buffers 16 8k;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;    gzip_disable “MSIE [1-6].(?!.*SV1)”;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;    access_log  /var/log/nginx/yourdomain .access.log;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;
location / {
&lt;/span&gt;&lt;span class="gp"&gt;    proxy_pass            https://localhost:5001;&lt;/span&gt;&lt;span class="w"&gt;        
&lt;/span&gt;&lt;span class="gp"&gt;    proxy_http_version 1.1;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;    proxy_set_header   Upgrade $&lt;/span&gt;http_upgrade&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="gp"&gt;    proxy_set_header   Connection keep-alive;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;    proxy_set_header   Host $&lt;/span&gt;host&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="gp"&gt;    proxy_cache_bypass $&lt;/span&gt;http_upgrade&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="gp"&gt;    proxy_set_header   X-Forwarded-For $&lt;/span&gt;proxy_add_x_forwarded_for&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="gp"&gt;    proxy_set_header   X-Forwarded-Proto $&lt;/span&gt;scheme&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt;    } 
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, I'm being brief here, but this should do everything we need to take requests to &lt;code&gt;yourdomain.com&lt;/code&gt;, forward them to SSL, and connect them to the application running on the server.&lt;/p&gt;

&lt;p&gt;In order to enable it, we'll make another symbolic link and then restart Nginx.&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="go"&gt;sudo ln -s /etc/nginx/sites-available/yourdomain.conf /etc/nginx/sites-enabled/yourdomain.conf
sudo systemctl restart nginx
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make sure everything is working, we'll start the app on its own and try to access it through the browse.&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="go"&gt;dotnet run /var/www/yourapp/yourapp.dll
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After you've verified that this works, you can close the application. The next step is to make it working on its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up systemd
&lt;/h3&gt;

&lt;p&gt;Yes, there is some &lt;a href="https://www.youtube.com/watch?v=o_AIw9bGogo" rel="noopener noreferrer"&gt;tragedy&lt;/a&gt; around &lt;code&gt;systemd&lt;/code&gt;, but it's still handy.&lt;/p&gt;

&lt;p&gt;Create a &lt;code&gt;.service&lt;/code&gt; file for you application:&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="go"&gt;sudo nano /etc/systemd/system/yourapp.service
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the following information to the file:&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="go"&gt;[Unit]
Description=Event Registration Example

[Service]
WorkingDirectory=/var/www/yourapp
ExecStart=/usr/bin/dotnet /var/www/yourapp/yourapp.dll
Restart=always
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Restart service after 10 seconds &lt;span class="k"&gt;if &lt;/span&gt;the dotnet service crashes:
&lt;span class="go"&gt;RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=dotnet-example
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false

[Install]
WantedBy=multi-user.target
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only thing I changed here from the Microsoft Doc was removing the &lt;code&gt;User=www-data&lt;/code&gt; line. While it is best practice to have a limited scope user, I didn't have a need to worry about this service running as root. This is a non-critical system that handles next to nothing other than a redirect, so I thought it was safe enough.&lt;/p&gt;

&lt;p&gt;Now we can enable and start the service through systemd.&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="go"&gt;sudo systemctl enable yourapp.service
sudo systemctl start yourapp.service
sudo systemctl status yourapp.service
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything has gone to plan, you should receive a status of running and be able to access your page. If it has all gone pair shape, you can still access the logs.&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="go"&gt;sudo journalctl -fu yourapp.service
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;That's It!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Hope this helps. There are a lot of ways this can be optimized or better secured, but for something this simple I didn't sweat it too hard. It's been running for a couple weeks and processed a handful of payments from people we gave early access to.&lt;/p&gt;

&lt;p&gt;I did make some subtle styling changes, but that's about it. Works perfect and is relatively easy to modify going forward.&lt;/p&gt;

&lt;p&gt;Thanks for coming along on the ride!&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>nginx</category>
      <category>csharp</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
