<?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: Tony Morris</title>
    <description>The latest articles on Forem by Tony Morris (@tonytalkstech).</description>
    <link>https://forem.com/tonytalkstech</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%2F851%2F6c6a8933-8200-4d72-887c-ee56cc49ca68.jpg</url>
      <title>Forem: Tony Morris</title>
      <link>https://forem.com/tonytalkstech</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/tonytalkstech"/>
    <language>en</language>
    <item>
      <title>The Vision for CloverleafTrack.com</title>
      <dc:creator>Tony Morris</dc:creator>
      <pubDate>Thu, 26 Sep 2024 19:41:15 +0000</pubDate>
      <link>https://forem.com/tonytalkstech/the-vision-for-cloverleaftrackcom-3j0</link>
      <guid>https://forem.com/tonytalkstech/the-vision-for-cloverleaftrackcom-3j0</guid>
      <description>&lt;p&gt;So, here’s the deal—I’m both a track and field coach and a bit of a tech geek. I’ve always been into the numbers side of things when it comes to sports. Track and field, especially, is all about numbers: personal bests, meet results, season records. You name it. But keeping track of all those stats? Not as easy as you’d think. Spreadsheets are a pain to deal with, and don’t even get me started on paper records—they always seem to go missing. And the tools out there? They just don’t quite fit what a high school track program really needs.&lt;/p&gt;

&lt;p&gt;That’s why I decided to build &lt;a href="https://cloverleaftrack.com" rel="noopener noreferrer"&gt;CloverleafTrack.com&lt;/a&gt;. My goal here is simple: I want to create a custom platform that tracks every performance from every athlete on Cloverleaf High School’s track and field team. But it’s not just about storing data. I want this platform to actually help coaches, athletes, parents, and even the fans interact with all that data in ways that are actually useful. You know, things like spotting trends, seeing how someone’s improving, or comparing results between meets and over different seasons. I want CloverleafTrack.com to be the go-to place for everything track and field related at Cloverleaf.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Start from Scratch?
&lt;/h2&gt;

&lt;p&gt;Now, you might be wondering why I didn’t just use some existing software. I thought about it, but honestly, there are a few reasons I wanted to build this thing from the ground up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Customization&lt;/strong&gt;
The pre-made solutions out there? They’re fine, I guess, but they don’t let me do exactly what I want. CloverleafTrack.com is going to be tailored specifically for our team’s needs. I’m talking full control over everything—performance tracking, team management, visualizing the data. I don’t want to deal with the limitations of some off-the-shelf tool that can’t handle the unique quirks of track and field events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt;
As more athletes join the team and we gather more data, this platform needs to keep up without slowing down or getting bogged down in old data. By building it from scratch, I can make sure the system will scale up as we grow—and even bring in historical data from past seasons without a hitch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ownership and Flexibility&lt;/strong&gt;
By hosting the platform ourselves, we’re not relying on some third-party service that could change its terms, raise prices, or go offline whenever they feel like it. We’re in full control. That means we can adapt and evolve the platform as our needs change without worrying about anyone else’s decisions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Passion Project&lt;/strong&gt;
Honestly? This is kind of a labor of love for me. Track and field has always been a big part of my life, and combining that with my love of tech just feels right. I’m not just building something functional; I’m building something I can continuously tweak and improve, making it more useful for everyone involved.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where I’m Starting
&lt;/h2&gt;

&lt;p&gt;For the first version of CloverleafTrack.com, I’m focusing on these four areas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Athlete Profiles&lt;/strong&gt;
Every athlete will have their own profile that shows all their performance history, personal bests, and how they’ve improved over time. Whether they’re in individual events or relays, their profile will give a full picture of what they’ve accomplished.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meet Management&lt;/strong&gt;
This is where I’ll be organizing all the data from meets, showing results across all events and athletes. It’ll make comparing performances between different meets and seasons much easier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance Tracking and Scoring&lt;/strong&gt;
I’m also building a detailed scoring system that will give coaches more context around the numbers. It won’t just show raw results—it’ll show how those results stack up against different scoring systems, whether for local meets or even potential benchmarks for college-level competition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Leaderboard&lt;/strong&gt;
This feature is one of the more exciting parts. There will be two types of leaderboards:

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;School Record Leaderboard&lt;/strong&gt;: This will track the top performances for every event throughout the school’s history. It’ll give everyone a clear look at who holds the record for each event and when it was set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event Leaderboard&lt;/strong&gt;: This one will track performances for a specific event over time, showing how results have improved and where current athletes rank against historical data.&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;/ol&gt;

&lt;h2&gt;
  
  
  Looking Ahead
&lt;/h2&gt;

&lt;p&gt;There’s still a lot more I want to build, and this is just the beginning. Down the road, I plan to add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data Visualization&lt;/strong&gt;
I’m looking to create charts and graphs that’ll make it easier for coaches and athletes to spot trends and see where things are headed over time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile-Friendly Interfaces&lt;/strong&gt;
Everyone’s on their phone these days, so I’ll be making sure the platform works seamlessly on phones and tablets. That way, anyone can check results or track performances while on the go.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meet and Season Scoring Metrics&lt;/strong&gt;
In the future, I’m going to build out scoring metrics that can show how the entire team performed across a meet or even over a full season. This will let coaches compare results from one meet to the next, and across seasons, making it easier to see team strengths and where improvements need to happen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CloverleafTrack.com is more than just a place to track numbers. I’m hoping it becomes an interactive, useful tool that makes the data more meaningful for everyone involved—from coaches and athletes to parents and fans. By building it from scratch, I can make sure it’s built exactly for the unique needs of our track and field program, and I’ll keep improving it as we go.&lt;/p&gt;

</description>
      <category>dotnet</category>
    </item>
    <item>
      <title>Self-Hosted Audiobooks</title>
      <dc:creator>Tony Morris</dc:creator>
      <pubDate>Fri, 10 Mar 2023 02:11:00 +0000</pubDate>
      <link>https://forem.com/tonytalkstech/self-hosted-audiobooks-4kc6</link>
      <guid>https://forem.com/tonytalkstech/self-hosted-audiobooks-4kc6</guid>
      <description>&lt;p&gt;I typically spend around two to five hours per week in a car, driving back and forth to work. While some weeks I work from home more regularly, I still end up in the car a few hours per week.&lt;/p&gt;

&lt;p&gt;During these longer commutes, I love listening to audiobooks. However, being the self-hosted advocate that I am, relying on a SaaS provider, like Audible, for my audiobooks runs counter to my beliefs. This post describes my self-hosted setup that lets me access my library anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audiobook Source
&lt;/h2&gt;

&lt;p&gt;I exclusively use &lt;a href="https://audible.com" rel="noopener noreferrer"&gt;Audible&lt;/a&gt; to get my audiobook files. While Amazon is generally the devil, Audible has amazing quality and range of audiobooks. Also, they let you download the &lt;code&gt;.m4b&lt;/code&gt; without any issues.&lt;/p&gt;

&lt;p&gt;I’ve got an &lt;strong&gt;Audible Premium Plus&lt;/strong&gt; subscription, which gives me a free audiobook every month for $14.95/month. This price is typically cheaper than purchasing audiobooks at retail, so it works out for me. I also don’t typically go through more than one audiobook per month, so the cadence works great.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audiobook Downloader
&lt;/h2&gt;

&lt;p&gt;To automate the downloading of the audiobooks from Audible, I use a tool called &lt;a href="https://openaudible.org" rel="noopener noreferrer"&gt;OpenAudible&lt;/a&gt;. I pay $18.95/year for the license, and it’s tied to the version that you purchase. In other words, if, after a year, a new version is out, I have to pay another $18.95 in order to upgrade. However, I can choose to &lt;strong&gt;not&lt;/strong&gt; upgrade and stay on the version I purchased permanently. It’s a pretty standard perpetual license model.&lt;/p&gt;

&lt;p&gt;OpenAudible is configured to connect to my Audible account and download all my audiobooks to a well-known network share. I run it on my personal MacBook Air.&lt;/p&gt;

&lt;h2&gt;
  
  
  Download Location
&lt;/h2&gt;

&lt;p&gt;Without going into too much detail, I have a &lt;a href="https://www.qnap.com/en/product/ts-431" rel="noopener noreferrer"&gt;QNAP TS-431&lt;/a&gt; on the network that houses all my network shares. In this case, I have a network share called &lt;code&gt;Audiobooks&lt;/code&gt; that I mount on my Air. I then configure OpenAudible to export all downloaded files to that network share.&lt;/p&gt;

&lt;h2&gt;
  
  
  Media Server
&lt;/h2&gt;

&lt;p&gt;I run &lt;a href="http://plex.tv" rel="noopener noreferrer"&gt;Plex&lt;/a&gt; on a server that is connected to my network as my media server. Adding the library and configuring it is a bit of an adventure in metadata, but generally it’s straightforward.&lt;/p&gt;

&lt;p&gt;Before adding any libraries, I added the &lt;a href="https://github.com/djdembeck/Audnexus.bundle" rel="noopener noreferrer"&gt;Audnexus&lt;/a&gt; library agent to Plex following the instructions on its GitHub README.&lt;/p&gt;

&lt;p&gt;I added an &lt;code&gt;Audiobooks&lt;/code&gt; library that uses the &lt;strong&gt;Music&lt;/strong&gt; type. From there, I add the auto-generated &lt;code&gt;books&lt;/code&gt; directory that OpenAudible creates in the &lt;code&gt;Audiobooks&lt;/code&gt; mount point.&lt;/p&gt;

&lt;p&gt;Within the configuration of the library, I chose the &lt;code&gt;Plex Music Scanner&lt;/code&gt; for the scanner and the newly-added &lt;code&gt;Audnexus Agent&lt;/code&gt; for the agent. The Audnexus GitHub README includes the recommended configuration for the library, as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Listening to the Audiobooks
&lt;/h2&gt;

&lt;p&gt;Now that these audiobooks exist on a self-hosted media server, it’s time to listen to them! While it’s true that I could use the native Plex app on my devices, I’ve found that &lt;a href="https://prologue.audio" rel="noopener noreferrer"&gt;Prologue&lt;/a&gt; is an upgrade in every way. In order to get offline playback, you have to spend $5 one time, which is well worth the price.&lt;/p&gt;

&lt;p&gt;From here, I just plug my phone into my car and listen to the audiobooks via CarPlay.&lt;/p&gt;

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

&lt;p&gt;All-in-all, my self-hosted audiobook configuration is pretty straightforward, and it requires almost no maintenance. Every month or so, I open my MacBook Air, fire up OpenAudible, sync my library, and everything &lt;em&gt;just works&lt;/em&gt;. I typically listen to books after downloading them locally in Prologue. It all works really well!&lt;/p&gt;

</description>
      <category>security</category>
      <category>algorithms</category>
      <category>discuss</category>
    </item>
    <item>
      <title>README</title>
      <dc:creator>Tony Morris</dc:creator>
      <pubDate>Tue, 23 Aug 2022 23:32:23 +0000</pubDate>
      <link>https://forem.com/tonytalkstech/readme-4fj2</link>
      <guid>https://forem.com/tonytalkstech/readme-4fj2</guid>
      <description>&lt;p&gt;&lt;em&gt;Inspired by &lt;a href="https://managerreadme.com/" rel="noopener noreferrer"&gt;Manager Readme&lt;/a&gt;, I recently decided to write up my own version. It's important to explicitly call out that this exercise was not to replace any formal onboarding that a new team member goes through, but instead to supplement it. There are quite a few detractors to Manager Readmes out there, but I'm working really hard to make it as introspective as possible. Anyway, enjoy!&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Hi! Welcome to the team! I’m Tony. My official title is Director of Cloud Enablement, but to you, I’m just your manager (or peer, or manager’s manager, or whatever our working relationship is).&lt;/p&gt;

&lt;p&gt;This document is intended to enhance, not replace, our working relationship. Indeed, the intention of this document is to understand how I think, act, react, and work. This document is intended as a user guide for me and how I work. It captures what you can expect out of the average weekly working with me.&lt;/p&gt;

&lt;p&gt;While I may have some opinions and nuance about how &lt;strong&gt;&lt;em&gt;I&lt;/em&gt;&lt;/strong&gt; work, this document should not, in any way, influence the way &lt;strong&gt;&lt;em&gt;you&lt;/em&gt;&lt;/strong&gt; work.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Leadership Style
&lt;/h2&gt;

&lt;p&gt;I’ve been working professional on technology since 2006. My career has taken me from software engineer, team/tech lead, software architect, software manager, enterprise architect, and now Director of Cloud Enablement. Throughout that journey, I’ve refined my leadership approach, but it’s still built on the same foundational elements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You are an adult&lt;/strong&gt;. This is, by far, the most important value you need to hear. I trust adults implicitly. Adults are self-sufficient. Adults know when to escalate bad situations. Adults know how to praise one another. Adults tell one another when they are overloaded and/or stressed out. I will always treat you as an adult first, until you are no longer being an adult. At that point, I will treat you like a child. Children need to earn trust. Children need their hands held crossing the street. Children throw tantrums. Children are unable to prepare for the future. Don’t be a child.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do less.&lt;/strong&gt; Delivery of value will always take precedent over being busy. The easiest way to deliver value is to have less balls in the air. In fact, I will regularly challenge you to take items off your plate, either by delegation or by backlogging the work for later. If you aren’t sure of the priority of items in the backlog, just ask!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bias towards action&lt;/strong&gt;. This is an Amazon leadership principle, and I am shamelessly stealing it. I do not want to be in hours of contemplative, theoretical meetings, and I do not want you to be either. Actions are very rarely irreversible. Try something small, detail your experiment, and learn from it. Keep experimenting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Be honest.&lt;/strong&gt; We don’t lie to each other. This includes both positive and constructive feedback. This is part of the social contract between you and me. I expect this to be difficult for you sometimes, and that’s okay! We will get better at it together.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Schedule
&lt;/h2&gt;

&lt;p&gt;I’m technically an in-office employee. This means that I’ll be in Westlake around 3 days per week. On those days, expect me to be in the office from 9:30am US Eastern until about 5:00pm US Eastern. For the days that I’m remote, the hours typically shift an hour earlier.&lt;/p&gt;

&lt;p&gt;I’m very particular about my calendar in Outlook--it’s always up-to-date, especially if I am unavailable. Additionally, except in very specific cases, my entire calendar is available to everyone in the organization.&lt;/p&gt;

&lt;h3&gt;
  
  
  Interruptions
&lt;/h3&gt;

&lt;p&gt;My job is to be interrupted, especially by you. I am, first and foremost, a servant leader to you. Do not hesitate to ask for time with me! I’ll be very honest about my availability, and I will typically make the time for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Focus Time
&lt;/h3&gt;

&lt;p&gt;Upon inspection of my calendar, you may notice that I aggressively block many hours of the week as Focus Time (&lt;a href="https://docs.microsoft.com/en-us/viva/insights/personal/teams/viva-insights-protect-time#schedule-focus-time" rel="noopener noreferrer"&gt;thanks Microsoft Viva!&lt;/a&gt;). This Focus Time is to block meeting scheduling from people &lt;strong&gt;outside&lt;/strong&gt; of my organization. It does not apply to you! Book right over the top of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  About The Spring
&lt;/h3&gt;

&lt;p&gt;In the Spring (from February to the first weekend in June), I coach Track and Field at our local High School. Practice runs from about 3:00pm US Eastern until 5:30pm US Eastern. During those times, I have my cell phone on me, but I’m mostly unavailable. To accommodate this schedule interruption, I am typically picking up extra time in the evening, outside of most people’s normal working hours. &lt;strong&gt;If something is urgent, I’m available&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You may notice that I have explicit Commute Time on my calendar during these months. I can be considered Available, But Working Remotely during those specific times.&lt;/p&gt;

&lt;p&gt;If you feel like you can’t reach me during these times, do not hesitate to reach out! I can’t fix a problem if I don’t know about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Communications
&lt;/h2&gt;

&lt;p&gt;Nowadays, there are about three hundred different ways to communicate with me. In an effort to set some expectations, here are the different ways that I regularly receive communications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Face-to-Face (Physical or Virtual).&lt;/strong&gt; This is for both scheduled meetings and impromptu conversations. Need to discuss something that doesn’t fit a textual model? Message me on Slack to confirm availability, then set it up. If it’s not urgent, schedule a meeting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phone Call.&lt;/strong&gt; This is a very urgent mode of communication. If you get a unprompted phone call from me, expect that the contents of the phone call need &lt;u&gt;immediate&lt;/u&gt; attention. I will not call just to talk about my day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Text Message.&lt;/strong&gt; I rarely will text you. If you get a text from me, it’s typically because I’m unable to call at the moment. You can safely assume it is &lt;u&gt;significantly less urgent&lt;/u&gt; than a phone call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instant Message (Slack / Teams).&lt;/strong&gt; This is our normal mode of conversation. I consider IMs to be &lt;u&gt;asynchronous communication&lt;/u&gt;. You should have no expectation of immediate responses from me. I try to respond quickly, and I will be upfront if I’m unable to respond with substance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Email.&lt;/strong&gt; The purest form of asynchronous communication. There’s almost no expectation of me responding to an email in less than 24 hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  1:1 Meetings
&lt;/h2&gt;

&lt;p&gt;1:1 meetings &lt;strong&gt;are for you&lt;/strong&gt;. When you start, I will send you a survey about 1:1’s to determine your ideal cadence, day, time, and topics that you would like to discuss. From there, I will schedule our regular 50-minute 1:1 at your defined cadence, day, and time.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Agenda
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;You own the agenda for our 1:1.&lt;/strong&gt; When I schedule our 1:1, I will be explicitly clear on when you should expect to receive the collaborative agenda, and I try to make it at least one full business day prior to the 1:1. This agenda will live in my personal space in Confluence in an area that only you and I can see and contribute.&lt;/p&gt;

&lt;p&gt;I will very likely have items in every agenda, but this is, first and foremost, your opportunity to tell me how you’re doing, what you need, what you wish could be different, how you feel about our team and your teammates, what your career goals are, etc. These are for the conversations you might not necessarily have with me when we’re sitting at our desks amongst coworkers. If you’d like to give me a brief status update on things you’re working on or that you’re stuck on, that is fine with me, but those are generally better-suited to a quick chat while I’m at my desk, a Slack message, or a separate meeting.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Slack Channel
&lt;/h3&gt;

&lt;p&gt;I create a private Slack channel for our 1:1s. These are explicitly to be used as a sort of scratchpad for items that you or I want to discuss in an upcoming 1:1 whose agenda is not yet available. These are removed from Direct Messages (DMs) on purpose, as I reserve DMs for normal work chatter, and 1:1 topics will typically get lost in the shuffle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Skip-Level 1:1 Meetings
&lt;/h3&gt;

&lt;p&gt;I am trying very hard to have 1:1’s with everyone in the department. I am bad at this, and it is something I am working to improve. If you and I haven’t met, anticipate that I will be correcting that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback
&lt;/h2&gt;

&lt;p&gt;I’m going to be asking you for your feedback on how the team and department are running. I’m going to do this &lt;strong&gt;a lot&lt;/strong&gt;. I’m going to do this in a multitude of ways. Sometimes, I will put you on the spot in a 1:1, hoping for a top-of-the-mind answer. Other times, I will send out an anonymous survey, hoping to get more detailed, challenging feedback.&lt;/p&gt;

&lt;p&gt;These are not the only times to provide feedback! Feedback should be provided and received as soon as possible. Instead of waiting for our next 1:1, or the next State of the Team survey, or the next watercooler conversation, grab me and let’s talk it out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quarterly Check-Ins
&lt;/h3&gt;

&lt;p&gt;At the conclusion of every quarter, you will be expected to complete a Quarterly Check-In. This process is for you to provide self-reflection feedback to me. From there, you and I will have a discussion about that feedback. During this, I will be supplying my own feedback. This is currently the only HR-mandated feedback cycle at Hyland, so I will be very keen for you to complete it on time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Leadership Reviews
&lt;/h3&gt;

&lt;p&gt;Once a year, I will be sending out Leadership Reviews, in which you will provide me with feedback on my leadership competencies. I ask that everyone in the department fill this out for me. Following the review cycle, I will be sharing the results of the feedback with the department.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tony Experience
&lt;/h2&gt;

&lt;p&gt;I’ve been using this term to describe what it’s like to work with me the first time. The alternative title was “My Nuances,” but I figured this would land more elegantly. Below is a list of interesting personality and professional quirks that I’m aware of.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I am an extrovert&lt;/strong&gt;. Working with people, especially groups of people, is empowering for my mind and soul. I am most effective when I am working collaboratively with people. This isn’t a quirk, but it’s something to understand about me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I’m very open&lt;/strong&gt; , and I will talk about anything. I want to know about every non-protected detail in your life, be it unique or mundane. This isn’t some management consulting exercise to remember you--I genuinely care about you and your life. However, I recognize there are lines that are not crossed, and I will endeavor to never cross them. You should feel no obligation to share anything with me, either. I understand there’s a weird power imbalance with these last two statements, and I am working on defining these boundaries more explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strong opinions, held weakly.&lt;/strong&gt; When a decision has to be made, but there is no one making it, I will make a decision, even if it’s mostly wrong. In fact, I will typically come to the table with a decision in my head, and I’ll explain it to everyone with the caveat that I am likely wrong, but it’s a good start. You should challenge me on my assertions and assumptions regularly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I work in visible bursts.&lt;/strong&gt; My job is mainly around strategic initiatives and large shifts in organizational and operational structures. This type of work can be difficult for me to crowdsource, so I tend to wander off into the desert for a few weeks at a time before coming back with a completed work product. Yes, this runs directly counter to my previous statement about collaborating with people, I know. This isn’t the best way to work, and I’m actively working on improving this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When I ask you to do something nebulous&lt;/strong&gt; , it’s likely because I haven’t thought about it all the way through. You should ask me to clarify and prioritize the work if you are uncertain. Sometimes, this will make the nebulous thing go away completely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I’m great at starting new things&lt;/strong&gt; , and poor at finishing them when I know the end result. This is especially true when the end result is weeks or months away. If I tend to give you work that feels started-but-unfinished, it’s because I know you are an exceptional operator of work that can take it across the finish line. As my career has moved further away from individual contribution, this quirk has diminished.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I’m regularly hyperbolic.&lt;/strong&gt; Things are rarely as good or as bad as my inner salesman believes. Most of this can be chalked up to me being excited about the topic, in one way or another. Call me out on something in which you don’t believe the hype--I won’t be offended.&lt;/p&gt;

</description>
      <category>career</category>
      <category>watercooler</category>
    </item>
    <item>
      <title>Static Website Playground: Terraform on AWS</title>
      <dc:creator>Tony Morris</dc:creator>
      <pubDate>Tue, 31 May 2022 17:30:28 +0000</pubDate>
      <link>https://forem.com/tonytalkstech/static-website-playground-terraform-on-aws-11md</link>
      <guid>https://forem.com/tonytalkstech/static-website-playground-terraform-on-aws-11md</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Xzpwb3SE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://images.unsplash.com/photo-1539186607619-df476afe6ff1%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DMnwxMTc3M3wwfDF8c2VhcmNofDE5fHxzdGF0aWN8ZW58MHx8fHwxNjM2MzM3OTU5%26ixlib%3Drb-1.2.1%26q%3D80%26w%3D2000" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Xzpwb3SE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://images.unsplash.com/photo-1539186607619-df476afe6ff1%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DMnwxMTc3M3wwfDF8c2VhcmNofDE5fHxzdGF0aWN8ZW58MHx8fHwxNjM2MzM3OTU5%26ixlib%3Drb-1.2.1%26q%3D80%26w%3D2000" alt="Static Website Playground: Terraform on AWS" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I need a side project, so here I am.&lt;/p&gt;

&lt;p&gt;I make a lot of strong statements about my opinions of public cloud providers and infrastructure-as-code tooling. I figure I may as well back them up with real actual projects.&lt;/p&gt;

&lt;p&gt;This is the first post in a series that I'm calling Static Website Playground. Each post will have the same premise: build a static website and deploy it to a specific public cloud, utilizing some type of infrastructure-as-code tooling.&lt;/p&gt;

&lt;p&gt;This post is about using &lt;strong&gt;Terraform&lt;/strong&gt; to deploy to &lt;strong&gt;AWS&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;The important piece first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;URL: &lt;a href="https://aws-terraform-static.morriscloud.com/" rel="noopener noreferrer"&gt;https://aws-terraform-static.morriscloud.com/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I was going to draw a diagram to represent the architectural layout here, but... it's super simple. So I'll just discuss the interest bits below.&lt;/p&gt;

&lt;h1&gt;
  
  
  GitHub
&lt;/h1&gt;

&lt;p&gt;URL: &lt;a href="https://github.com/morriscloud/static-website-playground/tree/main/terraform/aws" rel="noopener noreferrer"&gt;https://github.com/morriscloud/static-website-playground/tree/main/terraform/aws&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each of these static websites is stored in the same repository and organized by IaC provider and then by cloud provider.&lt;/p&gt;

&lt;h1&gt;
  
  
  Cloudflare
&lt;/h1&gt;

&lt;p&gt;Every post in this series will be using Cloudflare as the public DNS provider. I've got a Zone that I created previously (&lt;code&gt;morriscloud.com&lt;/code&gt;), so this part is super simple. I look up the Zone using a &lt;code&gt;data&lt;/code&gt; resource, then I create a new CNAME Record in the Zone that points to the S3 bucket's website endpoint. More on that part later.&lt;/p&gt;

&lt;p&gt;Note that I recently had to update this to use the new S3 resources in the Terraform AWS provider, so I'm now using &lt;code&gt;aws_s3_bucket_website_configuration.this.website_endpoint&lt;/code&gt; as the CNAME target, rather than the previous &lt;code&gt;aws_s3_bucket.this.website_endpoint&lt;/code&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  AWS
&lt;/h1&gt;

&lt;p&gt;This deployment is as bare-bones as possible in AWS. It creates an S3 bucket, uploads an HTML file &lt;code&gt;index.html&lt;/code&gt;, and sets some properties on the bucket to enable it to be viewed as a public website. There's a small bucket policy that's also attached to the bucket, allowing &lt;code&gt;GetObject&lt;/code&gt; access to the objects in the specified bucket from anywhere in the world.&lt;/p&gt;

&lt;h1&gt;
  
  
  Terraform Cloud
&lt;/h1&gt;

&lt;p&gt;To deploy the resources, I use the free version of Terraform Cloud. I connect a workspace to my GitHub repository, then I use the working directory to point to the correct subdirectory in the repository.&lt;/p&gt;

&lt;p&gt;Using this project as an example, I have a workspace named &lt;code&gt;static-website-playground-aws-terraform&lt;/code&gt;, pointed to the GitHub repository linked above, and then set to &lt;code&gt;terraform/aws&lt;/code&gt; as its working directory.&lt;/p&gt;

&lt;p&gt;Using Terraform Cloud gives me a nice hook directly into Pull Requests (if you enable it), plus I can see the changes that I'm making in a feature branch easily.&lt;/p&gt;

&lt;p&gt;It's important to note that my favorite feature of using Terraform Cloud is the ability to run &lt;code&gt;terraform plan&lt;/code&gt; from my local console pointed to the remote workspace. This runs the plan in a Terraform Cloud runner with all the variables defined there, rather than somewhere on my laptop that I could accidentally commit to source.&lt;/p&gt;




&lt;h1&gt;
  
  
  What's Next?
&lt;/h1&gt;

&lt;p&gt;Next up will be using Pulumi to deploy to AWS. Obviously it won't have Terraform Cloud, but I imagine most of the rest of it will be exactly the same. Stay tuned!&lt;/p&gt;

</description>
      <category>devops</category>
      <category>terraform</category>
      <category>aws</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Regular Feedback From My Team</title>
      <dc:creator>Tony Morris</dc:creator>
      <pubDate>Sun, 08 May 2022 01:13:14 +0000</pubDate>
      <link>https://forem.com/tonytalkstech/regular-feedback-from-my-team-26dn</link>
      <guid>https://forem.com/tonytalkstech/regular-feedback-from-my-team-26dn</guid>
      <description>&lt;p&gt;As a brand new-ish leader of people at Hyland, I am regularly in search of ways to solicit feedback from my team.&lt;/p&gt;

&lt;p&gt;Hyland has a quarterly review process that is completely optional, so it can be tough to get the organizational support that implementing such a process would entail. Additionally, this quarterly review process is not anonymous, so it can be tough for people to provide "true" feedback.&lt;/p&gt;

&lt;p&gt;Yearly, Hyland has a Leadership Evaluation Process which does provide that level of anonymity that I require, plus the organizational support around it. Unfortunately, this only reviews the &lt;strong&gt;people&lt;/strong&gt; in leadership, and not necessarily the health of the &lt;strong&gt;team&lt;/strong&gt;. Back to the drawing board.&lt;/p&gt;

&lt;h2&gt;
  
  
  My wife solves the problem
&lt;/h2&gt;

&lt;p&gt;My wife mentioned to me offhand that she her organization's weekly "Voice of the Team" survey is moving to a biweekly cadence. This sparked my interest, mainly because I had no idea what she was talking about.&lt;/p&gt;

&lt;p&gt;She explained it as such:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The survey is completely anonymous.&lt;/li&gt;
&lt;li&gt;The first question is a 1-10 rating: "How satisfied are you with the team?"&lt;/li&gt;
&lt;li&gt;The second question is a Yes/No, and it's a different question every week. This is typically statements about the leadership or the team in general.&lt;/li&gt;
&lt;li&gt;The third question is an open response: "Do you have any other feedback?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. Three simple questions, sent weekly, to gauge the health of the team and to provide the team a manner in which to provide feedback.&lt;/p&gt;

&lt;h2&gt;
  
  
  My solution
&lt;/h2&gt;

&lt;p&gt;Using Hyland's Microsoft 365 subscription, I created a simple &lt;a href="https://forms.office.com/" rel="noopener noreferrer"&gt;Microsoft Form&lt;/a&gt; for the given week, as seen in the screenshot below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1ezg92f65lsey6eel8kk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1ezg92f65lsey6eel8kk.png" alt="Regular Feedback From My Team" width="800" height="476"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Note that we are not recording names! Totally anonymous.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then I copied that form multiple times, one for each subsequent week. I gave each of those forms a different second question:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS Hosting Team Leadership cares about me as an individual.&lt;/li&gt;
&lt;li&gt;AWS Hosting Team Leadership holds the team accountable for its actions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For now, those are the only three leadership-focused questions I've come up with. Future questions will likely include state-of-the-team type questions, in addition to some product roadmap ones.&lt;/p&gt;

&lt;p&gt;Every Friday morning at 10am Eastern, I send out the following email:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Team,&lt;/p&gt;

&lt;p&gt;In an effort to become more a more transparent and feedback-driven organization, I will be sending out a very short survey every week on Friday.&lt;/p&gt;

&lt;p&gt;This survey is completely anonymous. While it requires you to be logged into your Hyland Office 365 account, names and emails are not recorded.&lt;/p&gt;

&lt;p&gt;I strongly encourage you to respond honestly! The responses to these surveys will drive a portion of every department meeting.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://forms.office.com/r/---" rel="noopener noreferrer"&gt;https://forms.office.com/r/---&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks,&lt;br&gt;
Tony&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How it's going
&lt;/h2&gt;

&lt;p&gt;My team spans three very distinct geographic regions: US, Poland, and India. Because I send this out Friday morning Eastern time, most of the non-US team members are not likely to respond on the same day.&lt;/p&gt;

&lt;p&gt;I am not planning on closing the form until the next one comes out, so it gives the team a chance to provide feedback when they see fit.&lt;/p&gt;

&lt;p&gt;I would love to get insight into other survey-style questions that anyone else might think fit in this type of feedback gathering! Let's hear it!&lt;/p&gt;

</description>
      <category>feedback</category>
      <category>people</category>
      <category>management</category>
    </item>
    <item>
      <title>Don't Manage Terraform Enterprise With Terraform</title>
      <dc:creator>Tony Morris</dc:creator>
      <pubDate>Sat, 07 Aug 2021 18:03:27 +0000</pubDate>
      <link>https://forem.com/tonytalkstech/dont-manage-terraform-enterprise-with-terraform-gl0</link>
      <guid>https://forem.com/tonytalkstech/dont-manage-terraform-enterprise-with-terraform-gl0</guid>
      <description>&lt;p&gt;First, some facts.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.terraform.io/docs/enterprise/index.html" rel="noopener noreferrer"&gt;Terraform Enterprise&lt;/a&gt; is a wonderful product. It's the self-hosted distribution of Terraform Cloud for organizations that want the privacy and scale of an enterprise-grade installation.&lt;/li&gt;
&lt;li&gt;There exists a &lt;a href="https://registry.terraform.io/providers/hashicorp/tfe/latest/docs" rel="noopener noreferrer"&gt;Terraform Cloud/Enterprise Provider&lt;/a&gt; that can easily template and manage how an organization creates Workspaces (and other TFC/TFE resources).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Given the generally painful user experience of entering dozens of Workspace Variables into the TFE, it makes sense that nearly everyone I've worked with has stated a desire to use the &lt;code&gt;tfe&lt;/code&gt; provider to manage workspaces. I'm here to tell you why this ends up being a rougher idea than you hoped for.&lt;/p&gt;

&lt;h1&gt;
  
  
  Pricing
&lt;/h1&gt;

&lt;p&gt;Terraform Enterprise is priced by the number of Workspaces you have.&lt;br&gt;&lt;br&gt;
They're not cheap.&lt;/p&gt;

&lt;p&gt;If you start dedicated Workspaces to creating and managing other Workspaces, you're effectively shorting yourself out of your own licenses.&lt;/p&gt;

&lt;p&gt;On the other hand, if you're in Terraform Cloud, you're not paying per-Workspace, so feel free to use this method if the following hiccups don't pertain to you.&lt;/p&gt;

&lt;p&gt;For what it's worth, if HashiCorp had a concept of "Configuration Workspaces" that didn't hit against your Workspace count, then this would obviously not be an issue.&lt;/p&gt;

&lt;h1&gt;
  
  
  Multi-Step Updates
&lt;/h1&gt;

&lt;p&gt;Let's say you can get over the pricing issue. Talking through how the Configuration Workspaces would be architected and deployed brings up some other potential issues.&lt;/p&gt;

&lt;p&gt;First, a quick overview of how we had tried this out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Workspace Repository
&lt;/h3&gt;

&lt;p&gt;The most important piece of functionality here is the Terraform module that uses the &lt;code&gt;tfe&lt;/code&gt; provider to create the Workload Workspaces.&lt;/p&gt;

&lt;p&gt;Everything about the Workload Workspace is contained within this Workspace Repository, including the Workspace Variables.&lt;/p&gt;

&lt;p&gt;Another name for this repository could be a "Configuration Repository," as it &lt;em&gt;configures&lt;/em&gt; the implementation of the Workload Repository.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workload Repository
&lt;/h3&gt;

&lt;p&gt;The Workload Repository defines the Terraform resources to create the workloads that you are configuring. For us, this is a bunch of resources from the &lt;code&gt;aws&lt;/code&gt; provider, but it could be anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration Workspace
&lt;/h3&gt;

&lt;p&gt;The Configuration Workspace in Terraform Enterprise is pointed to the Workspace Repository. When it executes a Run, it generates Terraform Enterprise resources, such as the Workload Workspaces and the requisite Workspace Variables in each one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workload Workspace
&lt;/h3&gt;

&lt;p&gt;The Workload Workspace is created by the Configuration Workspace and pointed to the Workload Repository. When it executes a Run, it generates Workload-specific resources. In our cases, this is generally AWS resources such as EC2 instances, EBS volumes, etc.&lt;/p&gt;

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

&lt;p&gt;The biggest problem with this setup is the back-and-forth you have to do in order to make changes.&lt;/p&gt;

&lt;p&gt;Let's say you want to add a resource to the Workload Repository. This is straightforward, and it doesn't cause many issues. You would just commit your changes to the Workload Repository, and the Workload Workspace would pick those up.&lt;/p&gt;

&lt;p&gt;What if, however, that introduces a new variable to the repository? The changes you would have to make in order to get it through the system look something like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add the Workspace Variable resource to the Workspace Repository.&lt;/li&gt;
&lt;li&gt;Push the Run through the Configuration Workspace to add the TFE Workspace Variable to the Workload Workspace.&lt;/li&gt;
&lt;li&gt;Add the variable to the Workload Repository.&lt;/li&gt;
&lt;li&gt;You can finally push the Run through the Workload Workspace.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Four steps for a simple variable addition seems like a tough solution to roll out to any team.&lt;/p&gt;




&lt;h1&gt;
  
  
  What Should You Do Instead?
&lt;/h1&gt;

&lt;p&gt;Let's be honest. I'm certain there are some really creative ways to work around these limitations managing Terraform at-scale. Feel free to use them and tell me what they are! &lt;a href="https://twitter.com/tonytalkstech" rel="noopener noreferrer"&gt;Hit me up on Twitter&lt;/a&gt; if you find a really neat solution!&lt;/p&gt;

&lt;p&gt;For our use cases, we are building out a Control Plane that abstracts that business-level functions from TFE itself. So, instead of thinking about "I need to create this TFE Workspaces with these Workspace Variables," we are now thinking "This team needs to create this application cluster."&lt;/p&gt;

&lt;p&gt;This abstraction is not unique. I've talked to many people in the community that do a similar thing.&lt;/p&gt;

&lt;p&gt;This solution works really well for us because we have a number of backend systems that we need to integrate together during an "application cluster spin-up." These include SaaS tools, such as PagerDuty, Splunk, New Relic, and many others. Given that Terraform Enterprise has a robust API, it makes our Control Plane much more straightforward to implement.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>terraform</category>
    </item>
    <item>
      <title>Finding S3 Batch Operations Failures in CloudTrail</title>
      <dc:creator>Tony Morris</dc:creator>
      <pubDate>Sun, 01 Aug 2021 14:29:34 +0000</pubDate>
      <link>https://forem.com/tonytalkstech/finding-s3-batch-operations-failures-in-cloudtrail-4j4d</link>
      <guid>https://forem.com/tonytalkstech/finding-s3-batch-operations-failures-in-cloudtrail-4j4d</guid>
      <description>&lt;p&gt;At work, I recently got the distinct opportunity to copy millions of objects from one S3 bucket to another.&lt;/p&gt;

&lt;p&gt;There are roughly a dozen separate ways to do this (as with everything in AWS), but the "right" way is to use an S3 Batch Operation to copy everything from an S3 Inventory Report.&lt;/p&gt;

&lt;p&gt;The only problem with an S3 Batch Operation is that it fails in surprising and hidden ways, especially if there's a misconfigured IAM permission. For example, our most recent job was failing due to a missing KMS permission. To determine what the missing permission was, we would typically head to CloudTrail and hunt down the failed requests.&lt;/p&gt;

&lt;p&gt;As S3 Batch Operations run as an assumed role, hunting these logs can be slightly more difficult, but we finally found the right way to accomplish it.&lt;/p&gt;

&lt;p&gt;The first, most important, piece is to hunt down the S3 Batch Operation's Job ID. You'll find this on the details screen clear at the top.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft66ddo3mng7w1jmmpucv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft66ddo3mng7w1jmmpucv.png" alt="Finding S3 Batch Operations Failures in CloudTrail" width="800" height="284"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;S3 Batch Operation Job Details Screen&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Next, shoot on over to CloudTrail and filter by User Name. The value you'll want to use is &lt;code&gt;s3-batch-operations_{Job ID}&lt;/code&gt;, where &lt;code&gt;{Job ID}&lt;/code&gt; is your S3 Batch Operation's Job ID retrieved in the previous step.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvuhkpvxhq752uf515uy5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvuhkpvxhq752uf515uy5.png" alt="Finding S3 Batch Operations Failures in CloudTrail" width="800" height="351"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;CloudTrail Filtering on Job ID User&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Heads Up!&lt;/strong&gt; If you've got KMS enabled for the Job, then you're going to get a whole heck of a lot of logs. I tend to download the logs to use Excel to look through it, but when you're moving more than 19 million records, you're going to have a bad time. If you need to dive into the reasons even more, I recommend using an Athena table.&lt;/p&gt;




&lt;p&gt;In case anyone is curious, the missing permission for the role was &lt;code&gt;kms:GenerateDataKey*&lt;/code&gt;. KMS is super fun.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>tutorial</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Auto-Generating Terraform State Storage in Azure</title>
      <dc:creator>Tony Morris</dc:creator>
      <pubDate>Sun, 15 Sep 2019 13:21:26 +0000</pubDate>
      <link>https://forem.com/tonytalkstech/auto-generating-terraform-state-storage-in-azure-eh5</link>
      <guid>https://forem.com/tonytalkstech/auto-generating-terraform-state-storage-in-azure-eh5</guid>
      <description>&lt;p&gt;In my latest &lt;a href="https://dev.to/tonytalkstech/azure-and-terraform-round-two-3oam#setup-stage"&gt;Azure/Terraform post&lt;/a&gt;, I touched on how I solved the “Chicken and Egg” problem with Terraform: how you need cloud resources in order to store Terraform state, but you can’t use Terraform to generate those cloud resources. This post details the solution to that problem.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Problem
&lt;/h1&gt;

&lt;p&gt;The "Chicken and Egg" problem with Terraform, for me, can be succinctly defined with four points:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I want to write Terraform to generate resources in a cloud provider.&lt;/li&gt;
&lt;li&gt;I don't want to run Terraform locally.&lt;/li&gt;
&lt;li&gt;I want to store my Terraform state in the same cloud provider.&lt;/li&gt;
&lt;li&gt;How do I generate the cloud provider's resources that will store my Terraform state?&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  The Solution
&lt;/h1&gt;

&lt;p&gt;Instead of relying on Terraform to generate the cloud provider's resources that will store your Terraform state, you instead use the cloud provider's native tools to generate and manage those resources. As I am currently working in Azure, I will use the &lt;a href="https://docs.microsoft.com/en-us/cli/azure/get-started-with-azure-cli?view=azure-cli-latest"&gt;Azure's &lt;code&gt;az&lt;/code&gt; CLI tool&lt;/a&gt;. This type of solution is also relevant to &lt;a href="https://aws.amazon.com/cli/"&gt;AWS's &lt;code&gt;aws&lt;/code&gt; CLI&lt;/a&gt; and &lt;a href="https://cloud.google.com/sdk/gcloud/"&gt;GCP's &lt;code&gt;gcloud&lt;/code&gt; CLI&lt;/a&gt;, if you want to use those instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Script
&lt;/h2&gt;

&lt;p&gt;In each of my repositories that house Terraform definitions (which is every repository at the end of the day), I have an extra script: &lt;code&gt;create-storage.sh&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is a Bash script because I develop using .NET Core, and my build/deploy machines are all Linux-based in Azure DevOps. This could just as easily be a PowerShell script if it needed to be executed in a Windows environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Heads Up!:&lt;/strong&gt; If you want the most recent version of the script, please &lt;a href="https://github.com/afmorris/TerraformAutoGenStorage/blob/master/Azure/create-storage.sh"&gt;go here&lt;/a&gt; instead of trusting this static website.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="c"&gt;# Heads up! You need to define the following environment variables:&lt;/span&gt;
&lt;span class="c"&gt;# RESOURCE_GROUP_NAME for the resource group that will contain the Azure Storage Account that will house your Terraform state files&lt;/span&gt;
&lt;span class="c"&gt;# STORAGE_ACCOUNT_NAME for the name of the Azure Storage Account&lt;/span&gt;
&lt;span class="c"&gt;# KEYVAULT_NAME to store the Storage Account's access key, so you don't have to manually keep track of it&lt;/span&gt;
&lt;span class="c"&gt;# LOCATION for the location of the Azure resources&lt;/span&gt;
&lt;span class="c"&gt;# KEYVAULT_SECRET_NAME for the name of the secret in Key VAult of the Storage Account's access key&lt;/span&gt;
&lt;span class="c"&gt;# CONTAINER_NAME for the Azure Blob Storage's container that will hold the Terraform state file(s)&lt;/span&gt;

&lt;span class="nv"&gt;RESOURCE_GROUP_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mm-terraform-rg
&lt;span class="nv"&gt;STORAGE_ACCOUNT_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mmterraform
&lt;span class="nv"&gt;KEYVAULT_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mm-terraform-kv

&lt;span class="c"&gt;# Create resource group&lt;/span&gt;
az group create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RESOURCE_GROUP_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LOCATION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Create storage account&lt;/span&gt;
az storage account create &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RESOURCE_GROUP_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;STORAGE_ACCOUNT_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--sku&lt;/span&gt; Standard_LRS &lt;span class="nt"&gt;--encryption-services&lt;/span&gt; blob

&lt;span class="c"&gt;# Get storage account key&lt;/span&gt;
&lt;span class="nv"&gt;ACCOUNT_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az storage account keys list &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RESOURCE_GROUP_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--account-name&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;STORAGE_ACCOUNT_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;0].value &lt;span class="nt"&gt;-o&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Create Key Vault&lt;/span&gt;
az keyvault create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KEYVAULT_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RESOURCE_GROUP_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LOCATION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Store account key in secret&lt;/span&gt;
az keyvault secret &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KEYVAULT_SECRET_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--vault-name&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KEYVAULT_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--value&lt;/span&gt; &lt;span class="nv"&gt;$ACCOUNT_KEY&lt;/span&gt;

&lt;span class="c"&gt;# Create blob container&lt;/span&gt;
az storage container create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CONTAINER_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--account-name&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;STORAGE_ACCOUNT_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--account-key&lt;/span&gt; &lt;span class="nv"&gt;$ACCOUNT_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h3&gt;
  
  
  Environment Variables
&lt;/h3&gt;

&lt;p&gt;There are a few environment variables that are required with the script above.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RESOURCE_GROUP_NAME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The name of the resource group that will house all of the resources generated for the Terraform state storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LOCATION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The location of the resource group (and subsequent resources located within the resource group)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;STORAGE_ACCOUNT_NAME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The name of the Azure Storage Account that we will be creating blob storage within&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CONTAINER_NAME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The name of the Azure Storage Container in the Azure Blob Storage. This will actually hold the Terraform state files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;KEYVAULT_NAME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The name of the Azure Key Vault to create to store the Azure Storage Account key. This allows us to automate the running of Terraform files without having to store the key ourselves&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;KEYVAULT_SECRET_NAME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The name of the secret that will store the Azure Storage Account key&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pay special note to the fact that we are generating an Azure Key Vault and Azure Key Vault Secret to manage the storage of the Azure Storage Account's key. While this isn't technically necessary, and we could just query the Azure Storage Account itself for the key anytime we needed it (as seen in the &lt;strong&gt;Get storage account key&lt;/strong&gt; section of the script), Azure DevOps has tight integration with Azure Key Vault, and this step simplifies our future deployment of Terraform resources.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Process
&lt;/h3&gt;

&lt;p&gt;The functionality in the file itself is pretty similar. The file itself isn't special, as it's mostly the same as what &lt;a href="https://docs.microsoft.com/en-us/azure/terraform/terraform-backend#configure-storage-account"&gt;Azure provides in their docs&lt;/a&gt;. The interesting bits that are special are the exporting of the Storage Account's key into Key Vault for later use in an Azure Pipeline during deploy-time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pipeline
&lt;/h2&gt;

&lt;p&gt;Having this file exist is useful, but it doesn't truly tie together delivery of the resources and the "auto-generating" part in a real-world scenario. To accomplish this, I use Azure Pipelines in the Azure DevOps offering.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build Pipeline
&lt;/h3&gt;

&lt;p&gt;As Azure Pipelines can use YAML to define the build pipelines, I've got an &lt;code&gt;azure-pipelines.yml&lt;/code&gt; file in the root of each repository. Within that YAML file, I've got the following snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;AZURE_SUBSCRIPTION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;xxx'&lt;/span&gt;
  &lt;span class="na"&gt;APPLICATION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;xxx'&lt;/span&gt;
  &lt;span class="na"&gt;CONTAINER_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(ENVIRONMENT_PREFIX)terraform&lt;/span&gt;
  &lt;span class="na"&gt;KEYVAULT_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(APPLICATION)-terraform-kv&lt;/span&gt;
  &lt;span class="na"&gt;KEYVAULT_SECRET_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(ENVIRONMENT_PREFIX)-storage-account-key&lt;/span&gt;
  &lt;span class="na"&gt;LOCATION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;eastus'&lt;/span&gt;
  &lt;span class="na"&gt;RESOURCE_GROUP_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(APPLICATION)-terraform-rg&lt;/span&gt;
  &lt;span class="na"&gt;STORAGE_ACCOUNT_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(APPLICATION)terraform&lt;/span&gt;
  &lt;span class="na"&gt;TF_IN_AUTOMATION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;

&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup&lt;/span&gt;
  &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SetupDevelopmentStorage&lt;/span&gt;
    &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;ENVIRONMENT_PREFIX&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;d'&lt;/span&gt;
      &lt;span class="na"&gt;ENVIRONMENT_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;development'&lt;/span&gt;
    &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Setup&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Development&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Storage'&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AzureCLI@1&lt;/span&gt;
      &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Run&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Setup&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Script'&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;azureSubscription&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(AZURE_SUBSCRIPTION)&lt;/span&gt;
        &lt;span class="na"&gt;scriptPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./create-storage.sh'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SetupStagingStorage&lt;/span&gt;
    &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;ENVIRONMENT_PREFIX&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s'&lt;/span&gt;
      &lt;span class="na"&gt;ENVIRONMENT_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;staging'&lt;/span&gt;
    &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Setup&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Staging&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Storage'&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AzureCLI@1&lt;/span&gt;
      &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Run&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Setup&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Script'&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;azureSubscription&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(AZURE_SUBSCRIPTION)&lt;/span&gt;
        &lt;span class="na"&gt;scriptPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./create-storage.sh'&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SetupProductionStorage&lt;/span&gt;
    &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;ENVIRONMENT_PREFIX&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;p'&lt;/span&gt;
      &lt;span class="na"&gt;ENVIRONMENT_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;production'&lt;/span&gt;
    &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Setup&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Production&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Storage'&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AzureCLI@1&lt;/span&gt;
      &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Run&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Setup&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Script'&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;azureSubscription&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(AZURE_SUBSCRIPTION)&lt;/span&gt;
        &lt;span class="na"&gt;scriptPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./create-storage.sh'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Nothing too special in this build pipeline, to be clear. It is just executing the &lt;code&gt;create-storage.sh&lt;/code&gt; script with different environment-specific parameters being passed in. You can see that I use the environment-specific parameters in the definition of the other environment variables (e.g., &lt;code&gt;CONTAINER_NAME&lt;/code&gt; has &lt;code&gt;$(ENVIRONMENT_PREFIX)&lt;/code&gt; at the start of its definition). This lets me have separate containers in the same Azure Storage Account that house environment-specific Terraform state files. Alternatively, I could just use differently-named Terraform state files, but I like consistency for the file names themselves.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy Pipeline
&lt;/h3&gt;

&lt;p&gt;This is where the magic happens. As of the time of this writing, I don't currently use &lt;a href="https://devblogs.microsoft.com/devops/whats-new-with-azure-pipelines/"&gt;multi-stage pipelines&lt;/a&gt;, so this is all done within the Classic Release Pipelines web UI.&lt;/p&gt;

&lt;p&gt;The first step in the Release Pipeline is to retrieve the Key Vault secret that was stored from the &lt;code&gt;create-storage.sh&lt;/code&gt; script via the &lt;code&gt;AzureKeyVault&lt;/code&gt; step. This is then stored within an Azure Pipelines variable named after the secret name itself. For example, if the secret name is &lt;code&gt;d-storage-account-key&lt;/code&gt;, the Azure Pipeline's variable will also be &lt;code&gt;d-storage-account-key&lt;/code&gt;. We will see this in use in a future step.&lt;/p&gt;

&lt;p&gt;For the Terraform-specific steps, I use the very wonderful &lt;a href="https://marketplace.visualstudio.com/items?itemName=charleszipp.azure-pipelines-tasks-terraform"&gt;Terraform Build and Release Tasks by Charles Zipp&lt;/a&gt; found on Azure Marketplace.&lt;/p&gt;

&lt;p&gt;The first Terraform step I use is merely to install Terraform to the agent, using the &lt;code&gt;TerraformInstaller&lt;/code&gt; step. As of this writing, I am using Terraform v0.12.3, but I'm sure that'll change soon.&lt;/p&gt;

&lt;p&gt;The second Terraform step is to run &lt;code&gt;terraform init&lt;/code&gt; by using the &lt;code&gt;TerraformCLI&lt;/code&gt; step. I pass in the following Command Options: &lt;code&gt;-backend-config="access_key=$(d-storage-account-key)" -backend-config="storage_account_name=$(APPLICATION)terraform" -backend-config="container_name=$(ENVIRONMENT_PREFIX)terraform" -backend-config="key=$(APPLICATION).tfstate"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;As I mentioned previously, you'll see that I'm using the &lt;code&gt;$(d-storage-account-key)&lt;/code&gt; Azure Pipeline variable to retrieve the access key. Straightforward, but something to keep in mind if you're building it out.&lt;/p&gt;

&lt;p&gt;The final Terraform step is to run &lt;code&gt;terraform apply&lt;/code&gt; by using the &lt;code&gt;TerraformCLI&lt;/code&gt; step again. The only Command Options I pass in are the environment-specific Terraform variables: &lt;code&gt;-var-file="./environments/$(ENVIRONMENT_NAME)/terraform.tfvars"&lt;/code&gt;. The usage of these variables can be seen in the &lt;a href="https://dev.to/tonytalkstech/azure-and-terraform-round-two-3oam#terraform-tfvars"&gt;previous Azure/Terraform post&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  tl;dr
&lt;/h1&gt;

&lt;p&gt;Use native cloud provider CLI tools to generate and store access keys for use later in build and deploy pipelines. Pretty straightforward, yeah?&lt;/p&gt;

</description>
      <category>azure</category>
      <category>terraform</category>
      <category>devops</category>
      <category>azuredevops</category>
    </item>
    <item>
      <title>Azure and Terraform, Round Two</title>
      <dc:creator>Tony Morris</dc:creator>
      <pubDate>Sun, 08 Sep 2019 20:37:47 +0000</pubDate>
      <link>https://forem.com/tonytalkstech/azure-and-terraform-round-two-3oam</link>
      <guid>https://forem.com/tonytalkstech/azure-and-terraform-round-two-3oam</guid>
      <description>&lt;p&gt;I &lt;a href="https://dev.to/tonytalkstech/azure-and-terraform-1n0l"&gt;recently blogged&lt;/a&gt; about using Terraform to manage resources in Azure. To be honest, my implementation was &lt;em&gt;okay&lt;/em&gt;, but it could definitely improve. This post is an update on how I’ve updated the structure and usage of Terraform within projects.&lt;/p&gt;

&lt;h1&gt;
  
  
  Project Structure
&lt;/h1&gt;

&lt;p&gt;On any given project that has Terraform resources, my folder structure looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project
│   .gitignore
│   azure-pipelines.yml
│   create-storage.sh  
│
└───terraform
    │   data.tf
    │   locals.tf
    │   main.tf
    │   provider.tf
    │   variables.tf
    │   versions.tf
    │
    └───environments
        └───development
            │   terraform.tfvars
        └───staging
            │   terraform.tfvars
        └───production
            │   terraform.tfvars
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  .gitignore
&lt;/h2&gt;

&lt;p&gt;Pretty standard &lt;code&gt;.gitignore&lt;/code&gt; file here. I use JetBrains IDEs, so I pull in the IntelliJ-standard entries, plus a few more. &lt;a href="https://www.gitignore.io/api/terraform,intellij+all,visualstudiocode" rel="noopener noreferrer"&gt;Go here&lt;/a&gt; for the exact &lt;code&gt;.gitignore&lt;/code&gt; I use.&lt;/p&gt;

&lt;h2&gt;
  
  
  azure-pipelines.yml
&lt;/h2&gt;

&lt;p&gt;As my resources are in Azure, it makes sense to use Azure DevOps for build and deploy pipelines. The build pipeline is explicitly defined with &lt;a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=azure-devops" rel="noopener noreferrer"&gt;Azure Pipeline's YAML schema&lt;/a&gt;. The release pipeline, unfortunately, is currently only defined within the web UI of Azure Pipelines (it's really just a &lt;code&gt;terraform apply&lt;/code&gt; at the end of the day, anyway).&lt;/p&gt;

&lt;p&gt;Generally speaking, the Terraform bits in my &lt;code&gt;azure-pipelines.yml&lt;/code&gt; is the same from project to project. Note that I truncated the file to only include the &lt;code&gt;development&lt;/code&gt; environment, but the other environments are basically the same but with updated variables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;AZURE_SUBSCRIPTION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;xxx'&lt;/span&gt;
  &lt;span class="na"&gt;BASE_ENVIRONMENT_PATH&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/environments/$(ENVIRONMENT_NAME)'&lt;/span&gt;
  &lt;span class="na"&gt;CONTAINER_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(ENVIRONMENT_PREFIX)terraform&lt;/span&gt;
  &lt;span class="na"&gt;KEYVAULT_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;product-terraform-kv'&lt;/span&gt;
  &lt;span class="na"&gt;KEYVAULT_SECRET_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(ENVIRONMENT_PREFIX)-storage-account-key&lt;/span&gt;
  &lt;span class="na"&gt;LOCATION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;eastus'&lt;/span&gt;
  &lt;span class="na"&gt;STORAGE_ACCOUNT_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;productterraform'&lt;/span&gt;
  &lt;span class="na"&gt;TERRAFORM_PATH&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="na"&gt;TERRAFORM_STATE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;product_infrastructure.tfstate'&lt;/span&gt;
  &lt;span class="na"&gt;TERRAFORM_VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.12.3'&lt;/span&gt;
  &lt;span class="na"&gt;TF_IN_AUTOMATION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;

&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup&lt;/span&gt;
  &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SetupDevelopmentStorage&lt;/span&gt;
    &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;ENVIRONMENT_PREFIX&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;d'&lt;/span&gt;
      &lt;span class="na"&gt;ENVIRONMENT_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;development'&lt;/span&gt;
    &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Setup&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Development&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Storage'&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AzureCLI@1&lt;/span&gt;
      &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Run&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Setup&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Script'&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;azureSubscription&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(AZURE_SUBSCRIPTION)&lt;/span&gt;
        &lt;span class="na"&gt;scriptPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./create-storage.sh'&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test&lt;/span&gt;
  &lt;span class="na"&gt;dependsOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup&lt;/span&gt;
  &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TestDevelopmentTerraform&lt;/span&gt;
    &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;ENVIRONMENT_PREFIX&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;d'&lt;/span&gt;
      &lt;span class="na"&gt;ENVIRONMENT_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;development'&lt;/span&gt;
    &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Test&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Development&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Terraform'&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AzureKeyVault@1&lt;/span&gt;
      &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Azure&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Key&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Vault:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$(KEYVAULT_NAME)'&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;azureSubscription&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(AZURE_SUBSCRIPTION)'&lt;/span&gt;
        &lt;span class="na"&gt;KeyVaultName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(KEYVAULT_NAME)'&lt;/span&gt;
        &lt;span class="na"&gt;SecretsFilter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(KEYVAULT_SECRET_NAME)'&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;charleszipp.azure-pipelines-tasks-terraform.azure-pipelines-tasks-terraform-installer.TerraformInstaller@0&lt;/span&gt;
      &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Use&lt;/span&gt;&lt;span class="nv"&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;$(TERRAFORM_VERSION)'&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;terraformVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(TERRAFORM_VERSION)&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;charleszipp.azure-pipelines-tasks-terraform.azure-pipelines-tasks-terraform-cli.TerraformCLI@0&lt;/span&gt;
      &lt;span class="na"&gt;displayName&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;init'&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;init&lt;/span&gt;
        &lt;span class="na"&gt;workingDirectory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(BASE_ENVIRONMENT_PATH)'&lt;/span&gt;
        &lt;span class="na"&gt;commandOptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-backend-config="access_key=$(d-storage-account-key)"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-backend-config="storage_account_name=$(STORAGE_ACCOUNT_NAME)"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-backend-config="container_name=$(ENVIRONMENT_PREFIX)terraform"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-backend-config="key=$(TERRAFORM_STATE)"'&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;charleszipp.azure-pipelines-tasks-terraform.azure-pipelines-tasks-terraform-cli.TerraformCLI@0&lt;/span&gt;
      &lt;span class="na"&gt;displayName&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;validate'&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;validate&lt;/span&gt;
        &lt;span class="na"&gt;workingDirectory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(BASE_ENVIRONMENT_PATH)'&lt;/span&gt;
        &lt;span class="na"&gt;commandOptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-var-file=".\environments\$(ENVIRONMENT_NAME)\terraform.tfvars"'&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Package&lt;/span&gt;
  &lt;span class="na"&gt;dependsOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test&lt;/span&gt;
  &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PackageTerraform&lt;/span&gt;
    &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Package&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Terraform'&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PublishBuildArtifacts@1&lt;/span&gt;
      &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Publish&lt;/span&gt;&lt;span class="nv"&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;Artifacts'&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;pathToPublish&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_PATH)'&lt;/span&gt;
        &lt;span class="na"&gt;artifactName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tf&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a lot of configuration, but I'll attempt to condense it down. The pipeline is broken up into three separate Stages: Setup, Test, and Package.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup Stage
&lt;/h3&gt;

&lt;p&gt;The Setup stage solves what I call "The Chicken and Egg Problem." It boils down to requiring Azure resources to store Terraform state, but we cannot create those Azure resources via Terraform because it doesn't know where store it yet. Instead of relying on Terraform to create those resources, I call a separate script. It sets some environment variables, and then it calls out to a shell script located in source: &lt;code&gt;create-storage.sh&lt;/code&gt;. The contents of this script are below.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="nv"&gt;RESOURCE_GROUP_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;product-terraform-rg
&lt;span class="nv"&gt;STORAGE_ACCOUNT_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;productterraform
&lt;span class="nv"&gt;KEYVAULT_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;product-terraform-kv

&lt;span class="c"&gt;# Create resource group&lt;/span&gt;
az group create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LOCATION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Create storage account&lt;/span&gt;
az storage account create &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$STORAGE_ACCOUNT_NAME&lt;/span&gt; &lt;span class="nt"&gt;--sku&lt;/span&gt; Standard_LRS &lt;span class="nt"&gt;--encryption-services&lt;/span&gt; blob

&lt;span class="c"&gt;# Get storage account key&lt;/span&gt;
&lt;span class="nv"&gt;ACCOUNT_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az storage account keys list &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt; &lt;span class="nt"&gt;--account-name&lt;/span&gt; &lt;span class="nv"&gt;$STORAGE_ACCOUNT_NAME&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;0].value &lt;span class="nt"&gt;-o&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Create Key Vault&lt;/span&gt;
az keyvault create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$KEYVAULT_NAME&lt;/span&gt; &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LOCATION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Store account key in secret&lt;/span&gt;
az keyvault secret &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KEYVAULT_SECRET_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--vault-name&lt;/span&gt; &lt;span class="nv"&gt;$KEYVAULT_NAME&lt;/span&gt; &lt;span class="nt"&gt;--value&lt;/span&gt; &lt;span class="nv"&gt;$ACCOUNT_KEY&lt;/span&gt;

&lt;span class="c"&gt;# Create blob container&lt;/span&gt;
az storage container create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CONTAINER_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--account-name&lt;/span&gt; &lt;span class="nv"&gt;$STORAGE_ACCOUNT_NAME&lt;/span&gt; &lt;span class="nt"&gt;--account-key&lt;/span&gt; &lt;span class="nv"&gt;$ACCOUNT_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script itself is pretty straightforward. It ensures a standard resource group for each given product exists. Within that resource group, it creates a storage account, key vault, key vault secret, and a blob container. The script pulls the storage account's key from the Azure CLI and stores it within the key vault secret. This key will be used to in future &lt;code&gt;terraform init&lt;/code&gt; calls. The blob container will hold the Terraform state files created later in the process.&lt;/p&gt;

&lt;p&gt;In case the application being deployed to Azure requires a database, I have a slightly altered version of the script that will generate a random database password and store it within the same key vault, but in a separate secret. That version can be seen below.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="nv"&gt;RESOURCE_GROUP_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;product-terraform-rg
&lt;span class="nv"&gt;STORAGE_ACCOUNT_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;productterraform
&lt;span class="nv"&gt;KEYVAULT_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;product-terraform-kv

&lt;span class="c"&gt;# Create resource group&lt;/span&gt;
az group create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="nv"&gt;$LOCATION&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null

&lt;span class="c"&gt;# Create storage account&lt;/span&gt;
az storage account create &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$STORAGE_ACCOUNT_NAME&lt;/span&gt; &lt;span class="nt"&gt;--sku&lt;/span&gt; Standard_LRS &lt;span class="nt"&gt;--encryption-services&lt;/span&gt; blob &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null

&lt;span class="c"&gt;# Get storage account key&lt;/span&gt;
&lt;span class="nv"&gt;ACCOUNT_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az storage account keys list &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt; &lt;span class="nt"&gt;--account-name&lt;/span&gt; &lt;span class="nv"&gt;$STORAGE_ACCOUNT_NAME&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;0].value &lt;span class="nt"&gt;-o&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Create Key Vault&lt;/span&gt;
az keyvault create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$KEYVAULT_NAME&lt;/span&gt; &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="nv"&gt;$LOCATION&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null

&lt;span class="c"&gt;# Store account key in secret&lt;/span&gt;
az keyvault secret &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$KEYVAULT_SECRET_NAME&lt;/span&gt; &lt;span class="nt"&gt;--vault-name&lt;/span&gt; &lt;span class="nv"&gt;$KEYVAULT_NAME&lt;/span&gt; &lt;span class="nt"&gt;--value&lt;/span&gt; &lt;span class="nv"&gt;$ACCOUNT_KEY&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null

&lt;span class="c"&gt;# Check if database password exists&lt;/span&gt;
&lt;span class="nv"&gt;DB_SECRET_INFO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;az keyvault secret show &lt;span class="o"&gt;--&lt;/span&gt;name &lt;span class="nv"&gt;$DB_PASSWORD_SECRET_NAME&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;vault-name &lt;span class="nv"&gt;$KEYVAULT_NAME&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Create the database password if it doesn't exist&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$DB_SECRET_INFO&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ &lt;span class="s2"&gt;"(SecretNotFound)"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;NEW_UUID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-base64&lt;/span&gt; 24&lt;span class="si"&gt;)&lt;/span&gt;
  az keyvault secret &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;name &lt;span class="nv"&gt;$DB_PASSWORD_SECRET_NAME&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;vault-name &lt;span class="nv"&gt;$KEYVAULT_NAME&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;value &lt;span class="nv"&gt;$NEW_UUID&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;dev/null
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Create blob container&lt;/span&gt;
az storage container create &lt;span class="o"&gt;--&lt;/span&gt;name &lt;span class="nv"&gt;$CONTAINER_NAME&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;account-name &lt;span class="nv"&gt;$STORAGE_ACCOUNT_NAME&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;account-key &lt;span class="nv"&gt;$ACCOUNT_KEY&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, this is mostly the same script, but with a small UUID generator if the database password has not already been generated. There are a number of ways to generate a random string, but the &lt;code&gt;openssl rand -base64 24&lt;/code&gt; was the most straightforward (and it worked on the Azure Linux worker machines).&lt;/p&gt;

&lt;h3&gt;
  
  
  Test Stage
&lt;/h3&gt;

&lt;p&gt;The Test Stage installs a specific version of Terraform, runs a &lt;code&gt;terraform init&lt;/code&gt; with assistance from the values retrieved from the previously-created key vault, and then runs a &lt;code&gt;terraform validate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You'll notice that the &lt;code&gt;terraform-init&lt;/code&gt; uses the &lt;code&gt;$(d-storage-account-key)&lt;/code&gt; variable. The Azure Key Vault step prior to that will pull out the value from the key vault secret into that variable. Unfortunately, I haven't discovered a way to double-reference a variable, so I have to keep it as a hard-coded reference. For reference, I would much rather have something like &lt;code&gt;$($(KEYVAULT_SECRET_NAME))&lt;/code&gt;, but that doesn't seem to be possible currently.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;terraform validate&lt;/code&gt; step's details are important: it points directly to the environment-specific &lt;code&gt;terraform.tfvars&lt;/code&gt;. This is how I accomplish multi-environment releases with a single codebase.&lt;/p&gt;

&lt;h3&gt;
  
  
  Package Stage
&lt;/h3&gt;

&lt;p&gt;The Package Stage is the simplest of the pipeline: it just runs an out-of-the-box &lt;code&gt;PublishBuildArtifacts&lt;/code&gt; task, pointed to the &lt;code&gt;terraform&lt;/code&gt; directory and dropping it into the &lt;code&gt;tf&lt;/code&gt; artifact. This will be used later in the release pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terraform Artifacts
&lt;/h2&gt;

&lt;p&gt;I've broken down the Terraform artifacts into a number of files for ease of use.&lt;/p&gt;

&lt;h3&gt;
  
  
  data.tf
&lt;/h3&gt;

&lt;p&gt;For infrastructure-only repositories, this file is very straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;data "azurerm_subscription" "this" {&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, if the given repository is building off another repository (e.g., an application-specific repository building on top of an infrastructure-specific repository), there will obviously be other &lt;code&gt;data&lt;/code&gt; blocks here. A sample one can be seen below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;data "azurerm_resource_group" "this" {&lt;/span&gt;
  &lt;span class="s"&gt;name = local.resource_group_name&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;data "azurerm_app_service_plan" "this" {&lt;/span&gt;
  &lt;span class="s"&gt;name                = local.app_service_plan_name&lt;/span&gt;
  &lt;span class="s"&gt;resource_group_name = data.azurerm_resource_group.this.name&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;data "azurerm_storage_account" "this" {&lt;/span&gt;
  &lt;span class="s"&gt;name                = local.storage_account_name&lt;/span&gt;
  &lt;span class="s"&gt;resource_group_name = data.azurerm_resource_group.this.name&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;data "azurerm_sql_server" "this" {&lt;/span&gt;
  &lt;span class="s"&gt;name                = local.sql_server_name&lt;/span&gt;
  &lt;span class="s"&gt;resource_group_name = data.azurerm_resource_group.this.name&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  locals.tf
&lt;/h3&gt;

&lt;p&gt;I typically use the &lt;code&gt;locals.tf&lt;/code&gt; file to define aggregated resource names that I'm going to be using in a number of places.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;locals {&lt;/span&gt;
  &lt;span class="s"&gt;resource_group_name   = "${var.environment_prefix}-${var.application_name}-rg"&lt;/span&gt;
  &lt;span class="s"&gt;app_service_plan_name = "${var.environment_prefix}-${var.application_name}-plan"&lt;/span&gt;
  &lt;span class="s"&gt;scope                 = "/subscriptions/${var.subscription_id}/resourceGroups/${azurerm_resource_group.this.name}"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;My &lt;code&gt;main.tf&lt;/code&gt; is where I create the Azure resources themselves. There's very little interesting or unique about this file, except that I'm generally not creating my own modules to group items. I simply haven't had a good reason to at this point.&lt;/p&gt;

&lt;p&gt;It is likely useful to point out that each repository &lt;strong&gt;only has one main.tf defined&lt;/strong&gt;. This is important, as it alludes to the fact that each environment has the same types of Azure resources. While everything is variable-driven, so the resources themselves can be configured differently, each different environment will have the same resources in total.&lt;/p&gt;

&lt;h3&gt;
  
  
  provider.tf
&lt;/h3&gt;

&lt;p&gt;Nothing crazy here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;terraform {&lt;/span&gt;
  &lt;span class="s"&gt;backend "azurerm" {&lt;/span&gt;
  &lt;span class="s"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;provider "azurerm" {&lt;/span&gt;
  &lt;span class="s"&gt;version = "~&amp;gt;1.30.1"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;I &lt;em&gt;try&lt;/em&gt; to make it a point to upgrade my provider and Terraform versions as much as possible, but I'm typically working across 10-15 repositories at a time, so once I get all the repositories on a single version, I'll stick to that version for awhile.&lt;/p&gt;

&lt;h3&gt;
  
  
  variables.tf
&lt;/h3&gt;

&lt;p&gt;Again, nothing special here. Fancy new Terraform v0.12 usage in the &lt;code&gt;role_assignments&lt;/code&gt; variable below!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;variable "environment_prefix" {&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "application_name" {&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "location" {&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "subscription_id" {&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "app_service_plan_sku_tier" {&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "app_service_plan_sku_size" {&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "tags" {&lt;/span&gt;
  &lt;span class="s"&gt;type = map(string)&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "role_assignments" {&lt;/span&gt;
  &lt;span class="s"&gt;type = list(object({ username = string, object_id = string, role_definition = string }))&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  versions.tf
&lt;/h3&gt;

&lt;p&gt;I like to explicitly define what version of Terraform to support for a given repository. This is where that's done.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;terraform {&lt;/span&gt;
  &lt;span class="s"&gt;required_version = "&amp;gt;= 0.12"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  terraform.tfvars
&lt;/h3&gt;

&lt;p&gt;Each environment has its own &lt;code&gt;terraform.tfvars&lt;/code&gt; file. This is where the values for the given variables (defined in &lt;code&gt;variables.tf&lt;/code&gt; above) are passed in if they are free to be exposed publicly. If there are secret values that need to be passed in, they are stored within a key vault and pulled in during the release pipeline, similar to the storage account key above.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;environment_prefix        = "d"&lt;/span&gt;
&lt;span class="s"&gt;application_name          = "product"&lt;/span&gt;
&lt;span class="s"&gt;location                  = "eastus"&lt;/span&gt;
&lt;span class="s"&gt;subscription_id           = "xxx"&lt;/span&gt;
&lt;span class="s"&gt;app_service_plan_sku_tier = "Shared"&lt;/span&gt;
&lt;span class="s"&gt;app_service_plan_sku_size = "D1"&lt;/span&gt;

&lt;span class="s"&gt;tags = {&lt;/span&gt;
  &lt;span class="s"&gt;"terraform"   = "true",&lt;/span&gt;
  &lt;span class="s"&gt;"environment" = "Development",&lt;/span&gt;
  &lt;span class="s"&gt;"application" = "Product"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;role_assignments = [&lt;/span&gt;
  &lt;span class="s"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;username        = "xxx"&lt;/span&gt;
    &lt;span class="s"&gt;object_id       = "xxx",&lt;/span&gt;
    &lt;span class="s"&gt;role_definition = "Owner"&lt;/span&gt;
  &lt;span class="s"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Release Pipeline
&lt;/h1&gt;

&lt;p&gt;As stated previously, Azure DevOps has a limitation in that it only allows Release Pipelines to be edited with the in-browser UI. This &lt;strong&gt;sucks&lt;/strong&gt;, but I've come to live with it.&lt;/p&gt;

&lt;p&gt;The Release Pipeline for any given project generally looks the same:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pull secrets from Azure Key Vault&lt;/li&gt;
&lt;li&gt;Install Terraform&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;terraform init&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;terraform apply&lt;/code&gt; &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then, if the pipeline requires it, and there's an application to deploy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Set Terraform outputs to Azure Pipeline variables&lt;/li&gt;
&lt;li&gt;Deploy application to Azure App Services&lt;/li&gt;
&lt;li&gt;Set values from pipeline variables as necessary&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This section is intentionally light on details, as there's not really much to talk about it.&lt;/p&gt;

&lt;h1&gt;
  
  
  tl;dr
&lt;/h1&gt;

&lt;p&gt;All-in-all, my approach to Terraform on Azure has changed pretty heavily in the past 7ish months. Instead of defining resources for each environment, I've now consolidated resource creation into a single file, and I'm setting the variables in each environment directory instead. Again, this is explicitly because I don't have a use case which requires &lt;strong&gt;different resources&lt;/strong&gt; per environment.&lt;/p&gt;

&lt;p&gt;In addition to the project structure changes, the "Chicken and Egg Problem" has been solved within the Azure Pipeline itself. Instead of having to manually create resources before running Terraform the first time, I can now rely on the pipeline itself to manage the backing data storage. This has been my biggest improvement to how I run pipelines in Azure DevOps.&lt;/p&gt;

&lt;p&gt;As always, if there's something you want to chat about more directly, hit me up on &lt;a href="https://twitter.com/tonytalkstech" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;, as that's where I'm most active. &lt;/p&gt;

</description>
      <category>azure</category>
      <category>terraform</category>
      <category>devops</category>
    </item>
    <item>
      <title>Don't You Dare Update That Copyright Manually</title>
      <dc:creator>Tony Morris</dc:creator>
      <pubDate>Fri, 18 Jan 2019 13:44:34 +0000</pubDate>
      <link>https://forem.com/tonytalkstech/dont-you-dare-update-that-copyright-manually-28a3</link>
      <guid>https://forem.com/tonytalkstech/dont-you-dare-update-that-copyright-manually-28a3</guid>
      <description>&lt;p&gt;A quick reminder for developers: now that we're in a new year, if you update your copyright to manually be "2019" there's almost certainly a spot for you in hell. Don't kick this down the road--solve the problem forever with something like &lt;code&gt;DateTime.UtcNow.Year&lt;/code&gt; (or whatever your language of choice does for current year).&lt;/p&gt;

&lt;p&gt;I just approved a ticket that changes &lt;code&gt;2016-2018&lt;/code&gt; to &lt;code&gt;2016-{DateTime.UtcNow.Year}&lt;/code&gt; because there were three separate pull requests in the past that didn't address this. Now, barring any systemic changes to how .NET calculates the current UTC DateTime, we will &lt;strong&gt;never have to update this again&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  July 12, 2016
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs16dx1j2028i7zzy5llk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs16dx1j2028i7zzy5llk.png" alt="2016 Change" width="800" height="32"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  February 16, 2017
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsnrmd1kb2er70jjrrve5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsnrmd1kb2er70jjrrve5.png" alt="2017 Change" width="800" height="43"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  July 13, 2018
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnkgl7s5bku1k8p9ikxjf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnkgl7s5bku1k8p9ikxjf.png" alt="2018 Change" width="800" height="40"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  January 17, 2019
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flvo6w8vtlmztwp0ah32s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flvo6w8vtlmztwp0ah32s.png" alt="2019 Change" width="800" height="32"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rant</category>
      <category>csharp</category>
      <category>advice</category>
    </item>
    <item>
      <title>Azure and Terraform</title>
      <dc:creator>Tony Morris</dc:creator>
      <pubDate>Tue, 15 Jan 2019 00:00:00 +0000</pubDate>
      <link>https://forem.com/tonytalkstech/azure-and-terraform-1n0l</link>
      <guid>https://forem.com/tonytalkstech/azure-and-terraform-1n0l</guid>
      <description>&lt;p&gt;I have always believed that Delivery is one of the most important aspects of software development. I blogged about it previously (&lt;a href="https://dev.to/tonytalkstech/my-core-values-13go"&gt;My Core Values&lt;/a&gt;). Software delivery isn’t just putting the bits into the final resting location; it must also include the infrastructure provisioning to explicitly define &lt;strong&gt;where&lt;/strong&gt; the bits will actually land. Terraform helps bridge that gap, especially given a public cloud offering like Azure.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Project
&lt;/h1&gt;

&lt;p&gt;I was recently contracted to implement a deployment pipeline for a financial services startup. The client had a special need to have the application environments built out in a reliable, scalable manner. There were two applications within the client's solution: a single-page application and a web service tier.&lt;/p&gt;

&lt;p&gt;From a non-functional perspective, there were a number of requirements from the technology team:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The development team must be able to deploy updated code assets to the environment in a reliable, well-known manner, without manual intervention&lt;/li&gt;
&lt;li&gt;The QA team must be able to deploy a feature branch to a specified environment for feature-specific testing&lt;/li&gt;
&lt;li&gt;System administrators must be able to quickly commission and decommission all aspects of environments, including the entire environment itself&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  The Proposal
&lt;/h1&gt;

&lt;p&gt;The non-functional requirements detailed above pointed me in an obvious location: host the application in &lt;a href="https://azure.microsoft.com/en-us/" rel="noopener noreferrer"&gt;Microsoft Azure&lt;/a&gt; and to utilize as many platform services as possible. Additionally, utilize &lt;a href="https://azure.microsoft.com/en-us/services/devops/" rel="noopener noreferrer"&gt;Azure DevOps&lt;/a&gt; products to enable tight integration between the application code and the application environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Microsoft Azure
&lt;/h2&gt;

&lt;p&gt;Within Azure, &lt;a href="https://azure.microsoft.com/en-us/services/app-service/" rel="noopener noreferrer"&gt;App Services&lt;/a&gt; would be used as application hosts and &lt;a href="https://azure.microsoft.com/en-us/services/postgresql/" rel="noopener noreferrer"&gt;Azure Database for PostgreSQL&lt;/a&gt; would be used for the database management. In addition to the core platform services, there will also be a few other products involved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://azure.microsoft.com/en-us/services/key-vault/" rel="noopener noreferrer"&gt;Key Vault&lt;/a&gt;: Used to manage application secrets (e.g., connection strings, encryption keys, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview" rel="noopener noreferrer"&gt;Application Insights&lt;/a&gt; Used to provide Application Performance Monitoring for both the single-page application and the web service&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://azure.microsoft.com/en-us/services/storage/" rel="noopener noreferrer"&gt;Storage&lt;/a&gt;: Used as a blob storage location for the applications&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Azure DevOps
&lt;/h2&gt;

&lt;p&gt;Azure DevOps (previously known as Visual Studio Team Services, previously known as Team Foundation Server) was chosen as the set of tools to manage source control and the build and release pipelines. &lt;a href="https://azure.microsoft.com/en-us/services/devops/repos/" rel="noopener noreferrer"&gt;Azure Repos&lt;/a&gt; is the remote source control repository and &lt;a href="https://azure.microsoft.com/en-us/services/devops/pipelines/" rel="noopener noreferrer"&gt;Azure Pipelines&lt;/a&gt; is the build and release pipeline tool.&lt;/p&gt;

&lt;p&gt;In a future iteration, it is possible that the work item tracking will migrate to &lt;a href="https://azure.microsoft.com/en-us/services/devops/boards/" rel="noopener noreferrer"&gt;Azure Boards&lt;/a&gt; from Jira and to &lt;a href="https://azure.microsoft.com/en-us/services/devops/test-plans/" rel="noopener noreferrer"&gt;Azure Test Plans&lt;/a&gt; from Zephyr, but this was not on the initial set of work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terraform
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.terraform.io/" rel="noopener noreferrer"&gt;Terraform&lt;/a&gt; is a product from &lt;a href="https://www.hashicorp.com/" rel="noopener noreferrer"&gt;HashiCorp&lt;/a&gt; to implement &lt;a href="https://en.wikipedia.org/wiki/Infrastructure_as_code" rel="noopener noreferrer"&gt;Infrastructure-as-Code&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That sounds like a lot of jargon, so let's boil it down: Infrastructure-as-Code (IaC) allows you to prescriptively define your infrastructure implementation in source control. Terraform is then a tool that executes these definitions to ensure the implemented infrastructure is equivalent to the specification. It moves the best-of-breed software development practices (e.g., source control, code reviews, deployment pipelines, etc.) into the infrastructure management realm.&lt;/p&gt;

&lt;p&gt;Additionally, Terraform was chosen as the IaC tool rather than &lt;a href="https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-authoring-templates" rel="noopener noreferrer"&gt;Azure Resource Manager Templates (ARM Templates)&lt;/a&gt; due to the extensive Terraform community and my personal expertise. I've worked with ARM Templates previously, but Terraform offered the same output with less initial startup work. While I hate YAML with an undying passion, it is much less verbose than the JSON that is used with ARM Templates.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Solution
&lt;/h1&gt;

&lt;p&gt;Implementing the solutions for each of the applications was generally the same. The only main difference was the Terraform implementation and the steps within the Azure Build Pipeline itself, but the concepts are similar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Azure Pipelines
&lt;/h2&gt;

&lt;p&gt;Azure Pipelines has two concepts: Build and Release Pipelines. While I generally believe this should be one pipeline in a perfect world, I can understand why they're separated. The Build Pipeline is used to generate artifacts, and the Release Pipeline is used to move those artifacts to separate stages (i.e., environments).&lt;/p&gt;

&lt;p&gt;Recently, Azure Pipelines added source control-defined Build Pipelines to its offering. As a firm believer in source controlling everything, an &lt;code&gt;azure-pipeline.yml&lt;/code&gt; file now exists in both the single-page application's repository and the web service's repository. These are detailed in the sections below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Single-Page Application Build Pipeline
&lt;/h3&gt;

&lt;p&gt;The single-page application's pipeline can be seen in its entirety below. At a high level, it's a straightforward build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run &lt;code&gt;npm install&lt;/code&gt; to get the application's dependencies&lt;/li&gt;
&lt;li&gt;Lint the application to ensure it conforms to the Angular's specifications&lt;/li&gt;
&lt;li&gt;Build the application&lt;/li&gt;
&lt;li&gt;Publish the built application&lt;/li&gt;
&lt;li&gt;Publish the Terraform artifacts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both the built application and the Terraform artifacts are used in the related Release pipeline.&lt;/p&gt;

&lt;p&gt;It is important to note that this build pipeline is used for &lt;strong&gt;all branches&lt;/strong&gt;. This is to ensure the entire repository is deployable at any given commit, assuming it passes the build pipeline successfully.&lt;/p&gt;

&lt;p&gt;In the future, the application team plans to implement automated tests in this pipeline to ensure the application is stable enough to push through to a release pipeline.&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;# Single Page Application&lt;/span&gt;

&lt;span class="na"&gt;pool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;vmImage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Ubuntu-16.04'&lt;/span&gt;

&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;publishPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dist/single-page-app'&lt;/span&gt;
  &lt;span class="na"&gt;terraformPath&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="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NodeTool@0&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;versionSpec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;8.x'&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Install&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Node.js'&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;npm install -g @angular/cli&lt;/span&gt;
    &lt;span class="s"&gt;npm install&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;npm&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;install'&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;ng lint&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ng&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lint'&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;ng build&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ng&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;build'&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PublishBuildArtifacts@1&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Publish&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Website&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Artifacts'&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;pathtoPublish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(publishPath)'&lt;/span&gt;
    &lt;span class="na"&gt;artifactName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;drop&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PublishBuildArtifacts@1&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Publish&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Terraform'&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;pathtoPublish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(terraformPath)'&lt;/span&gt;
    &lt;span class="na"&gt;artifactName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Web Services Build Pipeline
&lt;/h3&gt;

&lt;p&gt;While the web services pipeline has many more steps, it accomplishes roughly the same concepts as the single-page application:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Restore the solution's dependencies with &lt;code&gt;nuget restore&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Build the background data processor&lt;/li&gt;
&lt;li&gt;Build the web service&lt;/li&gt;
&lt;li&gt;Build the database migrator application (used to run SQL scripts needed for the environment (e.g., schema changes, data changes, etc.))&lt;/li&gt;
&lt;li&gt;Build the database seeder application (used to setup first-time data for the SQL database (i.e., default database users and their roles))&lt;/li&gt;
&lt;li&gt;Publish each of the built applications and Terraform&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As before, the built applications and Terraform are used in the future release pipeline. Additionally, this build is run for &lt;strong&gt;all branches&lt;/strong&gt; for the same reason as before: having a deployable codebase is vitally important.&lt;/p&gt;

&lt;p&gt;You'll notice that &lt;code&gt;dotnet publish&lt;/code&gt; is used for both the database migrator and database seeder projects; this is on purpose, as the application is planned to move toward .NET Core in the future.&lt;/p&gt;

&lt;p&gt;The absence of any automated testing (e.g., unit tests) is especially glaring in this build pipeline. The application development team is planning on adding those in the future, as with the single-page application.&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;# Web Service&lt;/span&gt;

&lt;span class="na"&gt;pool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;vmImage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;VS2017-Win2016'&lt;/span&gt;

&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;solution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/*.sln'&lt;/span&gt;
  &lt;span class="na"&gt;buildConfiguration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Release'&lt;/span&gt;
  &lt;span class="na"&gt;processorProject&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;WebService.Processor.csproj'&lt;/span&gt;
  &lt;span class="na"&gt;webServiceProject&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;WebService.csproj'&lt;/span&gt;
  &lt;span class="na"&gt;migratorProject&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;WebService.DbMigrator.csproj'&lt;/span&gt;
  &lt;span class="na"&gt;seederProject&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;WebService.DbSeeder.csproj'&lt;/span&gt;
  &lt;span class="na"&gt;webServicePublishPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;WebService/obj/Release/Package'&lt;/span&gt;
  &lt;span class="na"&gt;migratorPublishPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;WebService.DbMigrator/bin/Debug/netcoreapp2.1/publish'&lt;/span&gt;
  &lt;span class="na"&gt;seederPublishPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;WebService.DbSeeder/bin/Debug/netcoreapp2.1/publish'&lt;/span&gt;

&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NuGetToolInstaller@0&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Install&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;NuGet'&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NuGetCommand@2&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Solution&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;NuGet&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Restore'&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;restoreSolution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(solution)'&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;VSBuild@1&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Processor'&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;solution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(processorProject)'&lt;/span&gt;
    &lt;span class="na"&gt;configuration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(buildConfiguration)'&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;VSBuild@1&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Web&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Service'&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;solution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(webServiceProject)'&lt;/span&gt;
    &lt;span class="na"&gt;msbuildArgs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/p:DeployOnBuild=true&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/p:WebPublishMethod=Package&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/p:PackageAsSingleFile=true&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/p:SkipInvalidConfigurations=true'&lt;/span&gt;
    &lt;span class="na"&gt;configuration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(buildConfiguration)'&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dotnet publish $(migratorProject)&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Package&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Database&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Migrator'&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dotnet publish $(seederProject)&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Package&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Database&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Seeder'&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PublishBuildArtifacts@1&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Publish&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Web&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Service&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Artifacts'&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;pathtoPublish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(webServicePublishPath)'&lt;/span&gt;
    &lt;span class="na"&gt;artifactName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;drop&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PublishBuildArtifacts@1&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Publish&lt;/span&gt;&lt;span class="nv"&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;Artifacts'&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;pathtoPublish&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="na"&gt;artifactName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PublishBuildArtifacts@1&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Publish&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Database&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Migrator&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Artifacts'&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;pathtoPublish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(migratorPublishPath)'&lt;/span&gt;
    &lt;span class="na"&gt;artifactName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;migrator&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PublishBuildArtifacts@1&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Publish&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Database&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Seeder&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Artifacts'&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;pathtoPublish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(seederPublishPath)'&lt;/span&gt;
    &lt;span class="na"&gt;artifactName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;seeder&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Terraform Configuration
&lt;/h2&gt;

&lt;p&gt;For each of the applications, I define the infrastructure and platform services alongside the application's source code, generally within a &lt;code&gt;terraform&lt;/code&gt; folder. Each &lt;code&gt;terraform&lt;/code&gt; folder is organized as such:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform
  env-dev
    destroy.bat
    main.tf
    setup.bat
    variables.tf
  env-production
    destroy.bat
    main.tf
    setup.bat
    variables.tf
  env-staging
    destroy.bat
    main.tf
    setup.bat
    variables.tf
  env-test-1
    destroy.bat
    main.tf
    setup.bat
    variables.tf
  env-test-2
    destroy.bat
    main.tf
    setup.bat
    variables.tf
  env-test-3
    destroy.bat
    main.tf
    setup.bat
    variables.tf
  modules
    ... some number of modules ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  Modules
&lt;/h3&gt;

&lt;p&gt;I use Terraform modules to encapsulate specific sections of the infrastructure. The modules that I'm using across the two applications are detailed below.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;tr&gt;
    &lt;th&gt;Name&lt;/th&gt;
    &lt;th&gt;Short Name&lt;/th&gt;
    &lt;th&gt;Description&lt;/th&gt;
    &lt;th&gt;Azure Resources&lt;/th&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;API&lt;/td&gt;
    &lt;td&gt;api&lt;/td&gt;
    &lt;td&gt;Defines the Web Services hosting services.&lt;/td&gt;
    &lt;td&gt;
        &lt;ul&gt;
            &lt;li&gt;App Service Plan&lt;/li&gt;
            &lt;li&gt;Application Insights&lt;/li&gt;
            &lt;li&gt;App Service&lt;/li&gt;
        &lt;/ul&gt;
    &lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;Database&lt;/td&gt;
    &lt;td&gt;db&lt;/td&gt;
    &lt;td&gt;Defines the PostgreSQL server and database.&lt;/td&gt;
    &lt;td&gt;
        &lt;ul&gt;
            &lt;li&gt;PostgreSQL Server&lt;/li&gt;
            &lt;li&gt;PostgreSQL Firewall Rules&lt;/li&gt;
            &lt;li&gt;PostgreSQL Database&lt;/li&gt;
        &lt;/ul&gt;
    &lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;Key Vault&lt;/td&gt;
    &lt;td&gt;kv&lt;/td&gt;
    &lt;td&gt;Defines the secret store and the default secrets.&lt;/td&gt;
    &lt;td&gt;
        &lt;ul&gt;
            &lt;li&gt;Key Vault&lt;/li&gt;
            &lt;li&gt;Key Vault Access Policies&lt;/li&gt;
            &lt;li&gt;Key Vault Secrets&lt;/li&gt;
        &lt;/ul&gt;
    &lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;Resource Group&lt;/td&gt;
    &lt;td&gt;rg&lt;/td&gt;
    &lt;td&gt;Defines the resource group for the given environment that contains all the Azure resources.&lt;/td&gt;
    &lt;td&gt;
        &lt;ul&gt;
            &lt;li&gt;Resource Group&lt;/li&gt;
        &lt;/ul&gt;
    &lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;Storage&lt;/td&gt;
    &lt;td&gt;storage&lt;/td&gt;
    &lt;td&gt;Defines the storage account used by the application.&lt;/td&gt;
    &lt;td&gt;
        &lt;ul&gt;
            &lt;li&gt;Storage Account&lt;/li&gt;
        &lt;/ul&gt;
    &lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;Web&lt;/td&gt;
    &lt;td&gt;web&lt;/td&gt;
    &lt;td&gt;Defines the Single-Page Application hosting services.&lt;/td&gt;
    &lt;td&gt;
        &lt;ul&gt;
            &lt;li&gt;App Service Plan&lt;/li&gt;
            &lt;li&gt;Application Insights&lt;/li&gt;
            &lt;li&gt;App Service&lt;/li&gt;
        &lt;/ul&gt;
    &lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To be a bit more explicit, below are the exact files that I am using for each of these modules.&lt;/p&gt;
&lt;h4&gt;
  
  
  API
&lt;/h4&gt;
&lt;h5&gt;
  
  
  Folder Structure
&lt;/h5&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;modules
  api
    main.tf
    variables.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h5&gt;
  
  
  Files
&lt;/h5&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# main.tf&lt;/span&gt;

&lt;span class="s"&gt;resource "azurerm_app_service_plan" "default" {&lt;/span&gt;
  &lt;span class="s"&gt;name                = "${var.environment_prefix}-ws-plan"&lt;/span&gt;
  &lt;span class="s"&gt;location            = "${var.location}"&lt;/span&gt;
  &lt;span class="s"&gt;resource_group_name = "${var.resource_group_name}"&lt;/span&gt;
  &lt;span class="s"&gt;tags                = "${var.tags}"&lt;/span&gt;

  &lt;span class="s"&gt;sku {&lt;/span&gt;
    &lt;span class="s"&gt;tier = "${var.app_service_plan_sku_tier}"&lt;/span&gt;
    &lt;span class="s"&gt;size = "${var.app_service_plan_sku_size}"&lt;/span&gt;
  &lt;span class="s"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;resource "azurerm_application_insights" "default" {&lt;/span&gt;
  &lt;span class="s"&gt;name                = "${var.environment_prefix}-ws-ai"&lt;/span&gt;
  &lt;span class="s"&gt;location            = "${var.location}"&lt;/span&gt;
  &lt;span class="s"&gt;resource_group_name = "${var.resource_group_name}"&lt;/span&gt;
  &lt;span class="s"&gt;application_type    = "Web"&lt;/span&gt;
  &lt;span class="s"&gt;tags                = "${var.tags}"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;resource "azurerm_app_service" "api" {&lt;/span&gt;
  &lt;span class="s"&gt;name                = "${var.environment_prefix}-ws"&lt;/span&gt;
  &lt;span class="s"&gt;location            = "${var.location}"&lt;/span&gt;
  &lt;span class="s"&gt;resource_group_name = "${var.resource_group_name}"&lt;/span&gt;
  &lt;span class="s"&gt;app_service_plan_id = "${azurerm_app_service_plan.default.id}"&lt;/span&gt;
  &lt;span class="s"&gt;tags                = "${var.tags}"&lt;/span&gt;

  &lt;span class="s"&gt;app_settings {&lt;/span&gt;
    &lt;span class="s"&gt;"APPINSIGHTS_INSTRUMENTATIONKEY"                  = "${azurerm_application_insights.default.instrumentation_key}"&lt;/span&gt;
    &lt;span class="s"&gt;"APPINSIGHTS_PROFILERFEATURE_VERSION"             = "1.0.0"&lt;/span&gt;
    &lt;span class="s"&gt;"APPINSIGHTS_SNAPSHOTFEATURE_VERSION"             = "1.0.0"&lt;/span&gt;
    &lt;span class="s"&gt;"ApplicationInsightsAgent_EXTENSION_VERSION"      = "~2"&lt;/span&gt;
    &lt;span class="s"&gt;"DiagnosticServices_EXTENSION_VERSION"            = "~3"&lt;/span&gt;
    &lt;span class="s"&gt;"InstrumentationEngine_EXTENSION_VERSION"         = "~1"&lt;/span&gt;
    &lt;span class="s"&gt;"SnapshotDebugger_EXTENSION_VERSION"              = "~1"&lt;/span&gt;
    &lt;span class="s"&gt;"XDT_MicrosoftApplicationInsights_BaseExtensions" = "~1"&lt;/span&gt;
    &lt;span class="s"&gt;"XDT_MicrosoftApplicationInsights_Mode"           = "recommended"&lt;/span&gt;
  &lt;span class="s"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# variables.tf&lt;/span&gt;

&lt;span class="s"&gt;variable "environment_prefix" {}&lt;/span&gt;
&lt;span class="s"&gt;variable "location" {}&lt;/span&gt;

&lt;span class="s"&gt;variable "tags" {&lt;/span&gt;
  &lt;span class="s"&gt;type = "map"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "resource_group_name" {}&lt;/span&gt;
&lt;span class="s"&gt;variable "app_service_plan_sku_tier" {}&lt;/span&gt;
&lt;span class="s"&gt;variable "app_service_plan_sku_size" {}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Database
&lt;/h4&gt;

&lt;h5&gt;
  
  
  Folder Structure
&lt;/h5&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;modules
  db
    main.tf
    variables.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h5&gt;
  
  
  Files
&lt;/h5&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# main.tf&lt;/span&gt;

&lt;span class="s"&gt;resource "azurerm_postgresql_server" "default" {&lt;/span&gt;
  &lt;span class="s"&gt;name                = "${var.environment_prefix}-app-db"&lt;/span&gt;
  &lt;span class="s"&gt;location            = "${var.location}"&lt;/span&gt;
  &lt;span class="s"&gt;resource_group_name = "${var.resource_group_name}"&lt;/span&gt;
  &lt;span class="s"&gt;tags                = "${var.tags}"&lt;/span&gt;

  &lt;span class="s"&gt;sku {&lt;/span&gt;
    &lt;span class="s"&gt;name     = "${var.db_sku}"&lt;/span&gt;
    &lt;span class="s"&gt;capacity = "${var.db_capacity}"&lt;/span&gt;
    &lt;span class="s"&gt;tier     = "${var.db_tier}"&lt;/span&gt;
    &lt;span class="s"&gt;family   = "${var.db_family}"&lt;/span&gt;
  &lt;span class="s"&gt;}&lt;/span&gt;

  &lt;span class="s"&gt;storage_profile {&lt;/span&gt;
    &lt;span class="s"&gt;storage_mb            = "${var.db_storage_mb}"&lt;/span&gt;
    &lt;span class="s"&gt;backup_retention_days = "${var.db_backup_retention_days}"&lt;/span&gt;
    &lt;span class="s"&gt;geo_redundant_backup  = "${var.db_geo_redundant_backup}"&lt;/span&gt;
  &lt;span class="s"&gt;}&lt;/span&gt;

  &lt;span class="s"&gt;administrator_login          = "${var.db_administrator_login}"&lt;/span&gt;
  &lt;span class="s"&gt;administrator_login_password = "${var.db_administrator_login_password}"&lt;/span&gt;
  &lt;span class="s"&gt;version                      = "${var.db_version}"&lt;/span&gt;
  &lt;span class="s"&gt;ssl_enforcement              = "${var.db_ssl_enforcement}"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;resource "azurerm_postgresql_firewall_rule" "azure" {&lt;/span&gt;
  &lt;span class="s"&gt;name                = "azure"&lt;/span&gt;
  &lt;span class="s"&gt;resource_group_name = "${var.resource_group_name}"&lt;/span&gt;
  &lt;span class="s"&gt;server_name         = "${azurerm_postgresql_server.default.name}"&lt;/span&gt;
  &lt;span class="s"&gt;start_ip_address    = "0.0.0.0"&lt;/span&gt;
  &lt;span class="s"&gt;end_ip_address      = "0.0.0.0"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;resource "azurerm_postgresql_firewall_rule" "userhome" {&lt;/span&gt;
  &lt;span class="s"&gt;name                = "user-home"&lt;/span&gt;
  &lt;span class="s"&gt;resource_group_name = "${var.resource_group_name}"&lt;/span&gt;
  &lt;span class="s"&gt;server_name         = "${azurerm_postgresql_server.default.name}"&lt;/span&gt;
  &lt;span class="s"&gt;start_ip_address    = "xx.xx.xxx.xxx"&lt;/span&gt;
  &lt;span class="s"&gt;end_ip_address      = "xx.xx.xxx.xxx"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;resource "azurerm_postgresql_database" "app" {&lt;/span&gt;
  &lt;span class="s"&gt;name                = "app"&lt;/span&gt;
  &lt;span class="s"&gt;resource_group_name = "${var.resource_group_name}"&lt;/span&gt;
  &lt;span class="s"&gt;server_name         = "${azurerm_postgresql_server.default.name}"&lt;/span&gt;
  &lt;span class="s"&gt;charset             = "UTF8"&lt;/span&gt;
  &lt;span class="s"&gt;collation           = "English_United States.1252"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# variables.tf&lt;/span&gt;

&lt;span class="s"&gt;variable "environment_prefix" {}&lt;/span&gt;
&lt;span class="s"&gt;variable "location" {}&lt;/span&gt;

&lt;span class="s"&gt;variable "tags" {&lt;/span&gt;
  &lt;span class="s"&gt;type = "map"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "resource_group_name" {}&lt;/span&gt;
&lt;span class="s"&gt;variable "db_sku" {}&lt;/span&gt;
&lt;span class="s"&gt;variable "db_capacity" {}&lt;/span&gt;
&lt;span class="s"&gt;variable "db_tier" {}&lt;/span&gt;
&lt;span class="s"&gt;variable "db_family" {}&lt;/span&gt;
&lt;span class="s"&gt;variable "db_storage_mb" {}&lt;/span&gt;
&lt;span class="s"&gt;variable "db_backup_retention_days" {}&lt;/span&gt;
&lt;span class="s"&gt;variable "db_geo_redundant_backup" {}&lt;/span&gt;
&lt;span class="s"&gt;variable "db_administrator_login" {}&lt;/span&gt;
&lt;span class="s"&gt;variable "db_administrator_login_password" {}&lt;/span&gt;
&lt;span class="s"&gt;variable "db_version" {}&lt;/span&gt;
&lt;span class="s"&gt;variable "db_ssl_enforcement" {}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Key Vault
&lt;/h4&gt;

&lt;h5&gt;
  
  
  Folder Structure
&lt;/h5&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;modules
  kv
    main.tf
    variables.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h5&gt;
  
  
  Files
&lt;/h5&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# main.tf&lt;/span&gt;

&lt;span class="s"&gt;data "azurerm_client_config" "current" {}&lt;/span&gt;

&lt;span class="s"&gt;resource "azurerm_key_vault" "default" {&lt;/span&gt;
  &lt;span class="s"&gt;name                            = "${var.environment_prefix}-app-kv"&lt;/span&gt;
  &lt;span class="s"&gt;location                        = "${var.location}"&lt;/span&gt;
  &lt;span class="s"&gt;resource_group_name             = "${var.resource_group_name}"&lt;/span&gt;
  &lt;span class="s"&gt;tags                            = "${var.tags}"&lt;/span&gt;
  &lt;span class="s"&gt;enabled_for_template_deployment = &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="s"&gt;tenant_id                       = "${var.tenant_id}"&lt;/span&gt;

  &lt;span class="s"&gt;sku {&lt;/span&gt;
    &lt;span class="s"&gt;name = "standard"&lt;/span&gt;
  &lt;span class="s"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;resource "azurerm_key_vault_access_policy" "azuredevops" {&lt;/span&gt;
  &lt;span class="s"&gt;vault_name          = "${azurerm_key_vault.default.name}"&lt;/span&gt;
  &lt;span class="s"&gt;resource_group_name = "${var.resource_group_name}"&lt;/span&gt;
  &lt;span class="s"&gt;tenant_id           = "${var.tenant_id}"&lt;/span&gt;
  &lt;span class="s"&gt;object_id           = "${var.azuredevops_object_id}"&lt;/span&gt;

  &lt;span class="s"&gt;secret_permissions = [&lt;/span&gt;
    &lt;span class="s"&gt;"get",&lt;/span&gt;
    &lt;span class="s"&gt;"list",&lt;/span&gt;
    &lt;span class="s"&gt;"set",&lt;/span&gt;
    &lt;span class="s"&gt;"delete",&lt;/span&gt;
  &lt;span class="s"&gt;]&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;resource "azurerm_key_vault_access_policy" "current" {&lt;/span&gt;
  &lt;span class="s"&gt;vault_name          = "${azurerm_key_vault.default.name}"&lt;/span&gt;
  &lt;span class="s"&gt;resource_group_name = "${var.resource_group_name}"&lt;/span&gt;
  &lt;span class="s"&gt;tenant_id           = "${data.azurerm_client_config.current.tenant_id}"&lt;/span&gt;
  &lt;span class="s"&gt;object_id           = "${data.azurerm_client_config.current.service_principal_object_id}"&lt;/span&gt;

  &lt;span class="s"&gt;secret_permissions = [&lt;/span&gt;
    &lt;span class="s"&gt;"get",&lt;/span&gt;
    &lt;span class="s"&gt;"list",&lt;/span&gt;
    &lt;span class="s"&gt;"set",&lt;/span&gt;
    &lt;span class="s"&gt;"delete",&lt;/span&gt;
  &lt;span class="s"&gt;]&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;resource "azurerm_key_vault_access_policy" "adminuser" {&lt;/span&gt;
  &lt;span class="s"&gt;vault_name          = "${azurerm_key_vault.default.name}"&lt;/span&gt;
  &lt;span class="s"&gt;resource_group_name = "${var.resource_group_name}"&lt;/span&gt;
  &lt;span class="s"&gt;tenant_id           = "${var.tenant_id}"&lt;/span&gt;
  &lt;span class="s"&gt;object_id           = "${var.adminuser_object_id}"&lt;/span&gt;

  &lt;span class="s"&gt;secret_permissions = [&lt;/span&gt;
    &lt;span class="s"&gt;"get",&lt;/span&gt;
    &lt;span class="s"&gt;"list",&lt;/span&gt;
    &lt;span class="s"&gt;"set",&lt;/span&gt;
    &lt;span class="s"&gt;"delete",&lt;/span&gt;
    &lt;span class="s"&gt;"recover",&lt;/span&gt;
    &lt;span class="s"&gt;"backup",&lt;/span&gt;
    &lt;span class="s"&gt;"restore",&lt;/span&gt;
  &lt;span class="s"&gt;]&lt;/span&gt;

  &lt;span class="s"&gt;certificate_permissions = [&lt;/span&gt;
    &lt;span class="s"&gt;"create",&lt;/span&gt;
    &lt;span class="s"&gt;"delete",&lt;/span&gt;
    &lt;span class="s"&gt;"deleteissuers",&lt;/span&gt;
    &lt;span class="s"&gt;"get",&lt;/span&gt;
    &lt;span class="s"&gt;"getissuers",&lt;/span&gt;
    &lt;span class="s"&gt;"import",&lt;/span&gt;
    &lt;span class="s"&gt;"list",&lt;/span&gt;
    &lt;span class="s"&gt;"listissuers",&lt;/span&gt;
    &lt;span class="s"&gt;"managecontacts",&lt;/span&gt;
    &lt;span class="s"&gt;"manageissuers",&lt;/span&gt;
    &lt;span class="s"&gt;"purge",&lt;/span&gt;
    &lt;span class="s"&gt;"recover",&lt;/span&gt;
    &lt;span class="s"&gt;"setissuers",&lt;/span&gt;
    &lt;span class="s"&gt;"update",&lt;/span&gt;
    &lt;span class="s"&gt;"backup",&lt;/span&gt;
    &lt;span class="s"&gt;"restore",&lt;/span&gt;
  &lt;span class="s"&gt;]&lt;/span&gt;

  &lt;span class="s"&gt;key_permissions = [&lt;/span&gt;
    &lt;span class="s"&gt;"get",&lt;/span&gt;
    &lt;span class="s"&gt;"list",&lt;/span&gt;
    &lt;span class="s"&gt;"update",&lt;/span&gt;
    &lt;span class="s"&gt;"create",&lt;/span&gt;
    &lt;span class="s"&gt;"import",&lt;/span&gt;
    &lt;span class="s"&gt;"delete",&lt;/span&gt;
    &lt;span class="s"&gt;"recover",&lt;/span&gt;
    &lt;span class="s"&gt;"backup",&lt;/span&gt;
    &lt;span class="s"&gt;"restore",&lt;/span&gt;
  &lt;span class="s"&gt;]&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;resource "azurerm_key_vault_secret" "secret_1" {&lt;/span&gt;
  &lt;span class="s"&gt;name      = "secret_1"&lt;/span&gt;
  &lt;span class="s"&gt;value     = "${var.key_vault_secret_1}"&lt;/span&gt;
  &lt;span class="s"&gt;vault_uri = "${azurerm_key_vault.default.vault_uri}"&lt;/span&gt;
  &lt;span class="s"&gt;tags      = "${var.tags}"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;resource "azurerm_key_vault_secret" "secret_2" {&lt;/span&gt;
  &lt;span class="s"&gt;name      = "secret_2"&lt;/span&gt;
  &lt;span class="s"&gt;value     = "${var.key_vault_secret_2}"&lt;/span&gt;
  &lt;span class="s"&gt;vault_uri = "${azurerm_key_vault.default.vault_uri}"&lt;/span&gt;
  &lt;span class="s"&gt;tags      = "${var.tags}"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# variables.tf&lt;/span&gt;

&lt;span class="s"&gt;variable "environment_prefix" {}&lt;/span&gt;
&lt;span class="s"&gt;variable "location" {}&lt;/span&gt;

&lt;span class="s"&gt;variable "tags" {&lt;/span&gt;
  &lt;span class="s"&gt;type = "map"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "resource_group_name" {}&lt;/span&gt;

&lt;span class="s"&gt;variable "tenant_id" {&lt;/span&gt;
  &lt;span class="s"&gt;type        = "string"&lt;/span&gt;
  &lt;span class="s"&gt;description = "The Azure Active Directory tenant ID that should be used for authenticating requests to the key vault."&lt;/span&gt;
  &lt;span class="s"&gt;default     = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "azuredevops_object_id" {&lt;/span&gt;
  &lt;span class="s"&gt;default     = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "adminuser_object_id" {&lt;/span&gt;
  &lt;span class="s"&gt;default     = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "key_vault_secret_1" {}&lt;/span&gt;
&lt;span class="s"&gt;variable "key_vault_secret_2" {}&lt;/span&gt;

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

&lt;/div&gt;



&lt;h4&gt;
  
  
  Resource Group
&lt;/h4&gt;

&lt;h5&gt;
  
  
  Folder Structure
&lt;/h5&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;modules
  rg
    main.tf
    output.tf
    variables.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h5&gt;
  
  
  Files
&lt;/h5&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# main.tf&lt;/span&gt;

&lt;span class="s"&gt;resource "azurerm_resource_group" "default" {&lt;/span&gt;
  &lt;span class="s"&gt;name     = "${var.environment_prefix}-app-rg"&lt;/span&gt;
  &lt;span class="s"&gt;location = "${var.location}"&lt;/span&gt;
  &lt;span class="s"&gt;tags     = "${var.tags}"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# output.tf&lt;/span&gt;

&lt;span class="s"&gt;output "name" {&lt;/span&gt;
  &lt;span class="s"&gt;value = "${azurerm_resource_group.default.name}"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;output "location" {&lt;/span&gt;
  &lt;span class="s"&gt;value = "${azurerm_resource_group.default.location}"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# variables.tf&lt;/span&gt;

&lt;span class="s"&gt;variable "environment_prefix" {}&lt;/span&gt;
&lt;span class="s"&gt;variable "location" {}&lt;/span&gt;

&lt;span class="s"&gt;variable "tags" {&lt;/span&gt;
  &lt;span class="s"&gt;type = "map"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Storage
&lt;/h4&gt;

&lt;h5&gt;
  
  
  Folder Structure
&lt;/h5&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;modules
  storage
    main.tf
    variables.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h5&gt;
  
  
  Files
&lt;/h5&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# main.tf&lt;/span&gt;

&lt;span class="s"&gt;resource "azurerm_storage_account" "default" {&lt;/span&gt;
  &lt;span class="s"&gt;name                     = "${var.environment_prefix}appstorage"&lt;/span&gt;
  &lt;span class="s"&gt;location                 = "${var.location}"&lt;/span&gt;
  &lt;span class="s"&gt;resource_group_name      = "${var.resource_group_name}"&lt;/span&gt;
  &lt;span class="s"&gt;tags                     = "${var.tags}"&lt;/span&gt;
  &lt;span class="s"&gt;account_tier             = "Standard"&lt;/span&gt;
  &lt;span class="s"&gt;account_replication_type = "LRS"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# variables.tf&lt;/span&gt;

&lt;span class="s"&gt;variable "environment_prefix" {}&lt;/span&gt;
&lt;span class="s"&gt;variable "location" {}&lt;/span&gt;

&lt;span class="s"&gt;variable "tags" {&lt;/span&gt;
  &lt;span class="s"&gt;type = "map"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "resource_group_name" {}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Web
&lt;/h4&gt;

&lt;h5&gt;
  
  
  Folder Structure
&lt;/h5&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;modules
  web
    main.tf
    variables.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h5&gt;
  
  
  Files
&lt;/h5&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# main.tf&lt;/span&gt;

&lt;span class="s"&gt;resource "azurerm_app_service_plan" "default" {&lt;/span&gt;
  &lt;span class="s"&gt;name                = "${var.environment_prefix}-web-plan"&lt;/span&gt;
  &lt;span class="s"&gt;location            = "${var.location}"&lt;/span&gt;
  &lt;span class="s"&gt;resource_group_name = "${var.resource_group_name}"&lt;/span&gt;

  &lt;span class="s"&gt;sku {&lt;/span&gt;
    &lt;span class="s"&gt;tier = "${var.app_service_plan_sku_tier}"&lt;/span&gt;
    &lt;span class="s"&gt;size = "${var.app_service_plan_sku_size}"&lt;/span&gt;
  &lt;span class="s"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;resource "azurerm_application_insights" "default" {&lt;/span&gt;
  &lt;span class="s"&gt;name                = "${var.environment_prefix}-web-ai"&lt;/span&gt;
  &lt;span class="s"&gt;location            = "${var.location}"&lt;/span&gt;
  &lt;span class="s"&gt;resource_group_name = "${var.resource_group_name}"&lt;/span&gt;
  &lt;span class="s"&gt;application_type    = "Web"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;resource "azurerm_app_service" "web" {&lt;/span&gt;
  &lt;span class="s"&gt;name                = "${var.environment_prefix}-web"&lt;/span&gt;
  &lt;span class="s"&gt;location            = "${var.location}"&lt;/span&gt;
  &lt;span class="s"&gt;resource_group_name = "${var.resource_group_name}"&lt;/span&gt;
  &lt;span class="s"&gt;app_service_plan_id = "${azurerm_app_service_plan.default.id}"&lt;/span&gt;

  &lt;span class="s"&gt;app_settings {&lt;/span&gt;
    &lt;span class="s"&gt;"APPINSIGHTS_INSTRUMENTATIONKEY"                  = "${azurerm_application_insights.default.instrumentation_key}"&lt;/span&gt;
    &lt;span class="s"&gt;"APPINSIGHTS_PROFILERFEATURE_VERSION"             = "1.0.0"&lt;/span&gt;
    &lt;span class="s"&gt;"APPINSIGHTS_SNAPSHOTFEATURE_VERSION"             = "1.0.0"&lt;/span&gt;
    &lt;span class="s"&gt;"ApplicationInsightsAgent_EXTENSION_VERSION"      = "~2"&lt;/span&gt;
    &lt;span class="s"&gt;"DiagnosticServices_EXTENSION_VERSION"            = "~3"&lt;/span&gt;
    &lt;span class="s"&gt;"InstrumentationEngine_EXTENSION_VERSION"         = "~1"&lt;/span&gt;
    &lt;span class="s"&gt;"SnapshotDebugger_EXTENSION_VERSION"              = "~1"&lt;/span&gt;
    &lt;span class="s"&gt;"XDT_MicrosoftApplicationInsights_BaseExtensions" = "~1"&lt;/span&gt;
    &lt;span class="s"&gt;"XDT_MicrosoftApplicationInsights_Mode"           = "recommended"&lt;/span&gt;
  &lt;span class="s"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

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

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# variables.tf&lt;/span&gt;

&lt;span class="s"&gt;variable "environment_prefix" {}&lt;/span&gt;
&lt;span class="s"&gt;variable "location" {}&lt;/span&gt;

&lt;span class="s"&gt;variable "tags" {&lt;/span&gt;
  &lt;span class="s"&gt;type = "map"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "resource_group_name" {}&lt;/span&gt;
&lt;span class="s"&gt;variable "app_service_plan_sku_tier" {}&lt;/span&gt;
&lt;span class="s"&gt;variable "app_service_plan_sku_size" {}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Environments
&lt;/h3&gt;

&lt;p&gt;Generally, each of the environments is the same look and feel. The point of having each of these separate environment folders (e.g., &lt;code&gt;env-dev&lt;/code&gt;, &lt;code&gt;env-production&lt;/code&gt;, etc.) is to allow Terraform to easily run its normal scripts without any more configuration in the release pipelines. For example, if we are deploying the application to the development environment, we change the current working directory to &lt;code&gt;env-dev&lt;/code&gt; and execute the same scripts that we would elsewhere.&lt;/p&gt;

&lt;p&gt;A more detailed look at the files can be seen below.&lt;/p&gt;

&lt;h4&gt;
  
  
  Single-Page Application Environments
&lt;/h4&gt;

&lt;p&gt;The single-page application contains only a few Terraform modules, as its implementation is much simpler.&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;# main.tf&lt;/span&gt;

&lt;span class="s"&gt;terraform {&lt;/span&gt;
  &lt;span class="s"&gt;backend "azurerm" {&lt;/span&gt;
    &lt;span class="s"&gt;container_name = "terraform"&lt;/span&gt;
    &lt;span class="s"&gt;key            = "tfbackend-web.tfstate"&lt;/span&gt;
  &lt;span class="s"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;provider "azurerm" {&lt;/span&gt;
  &lt;span class="s"&gt;version = "=1.20.0"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;module "rg" {&lt;/span&gt;
  &lt;span class="s"&gt;source = "../modules/rg"&lt;/span&gt;

  &lt;span class="s"&gt;environment_prefix = "${var.environment_prefix}"&lt;/span&gt;
  &lt;span class="s"&gt;location           = "${var.location}"&lt;/span&gt;
  &lt;span class="s"&gt;tags               = "${var.tags}"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;module "web" {&lt;/span&gt;
  &lt;span class="s"&gt;source = "../modules/web"&lt;/span&gt;

  &lt;span class="s"&gt;environment_prefix        = "${var.environment_prefix}"&lt;/span&gt;
  &lt;span class="s"&gt;resource_group_name       = "${module.rg.name}"&lt;/span&gt;
  &lt;span class="s"&gt;location                  = "${module.rg.location}"&lt;/span&gt;
  &lt;span class="s"&gt;tags                      = "${var.tags}"&lt;/span&gt;
  &lt;span class="s"&gt;app_service_plan_sku_tier = "${var.app_service_plan_sku_tier}"&lt;/span&gt;
  &lt;span class="s"&gt;app_service_plan_sku_size = "${var.app_service_plan_sku_size}"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# variables.tf&lt;/span&gt;

&lt;span class="s"&gt;variable "environment_prefix" {&lt;/span&gt;
  &lt;span class="s"&gt;description = "The prefix for the environment."&lt;/span&gt;
  &lt;span class="s"&gt;type        = "string"&lt;/span&gt;
  &lt;span class="s"&gt;default     = "d"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "location" {&lt;/span&gt;
  &lt;span class="s"&gt;description = "Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created."&lt;/span&gt;
  &lt;span class="s"&gt;type        = "string"&lt;/span&gt;
  &lt;span class="s"&gt;default     = "eastus"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "tags" {&lt;/span&gt;
  &lt;span class="s"&gt;description = "The tags to associate with the resources."&lt;/span&gt;
  &lt;span class="s"&gt;type        = "map"&lt;/span&gt;

  &lt;span class="s"&gt;default = {&lt;/span&gt;
    &lt;span class="s"&gt;"terraform" = "true"&lt;/span&gt;
  &lt;span class="s"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "app_service_plan_sku_tier" {&lt;/span&gt;
  &lt;span class="s"&gt;type        = "string"&lt;/span&gt;
  &lt;span class="s"&gt;description = "Specifies the plan's pricing tier."&lt;/span&gt;
  &lt;span class="s"&gt;default     = "Shared"&lt;/span&gt;                             &lt;span class="c1"&gt;# Shared | Basic | Standard | ...&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "app_service_plan_sku_size" {&lt;/span&gt;
  &lt;span class="s"&gt;type        = "string"&lt;/span&gt;
  &lt;span class="s"&gt;description = "Specifies the plan's instance size."&lt;/span&gt;
  &lt;span class="s"&gt;default     = "D1"&lt;/span&gt;                                  &lt;span class="c1"&gt;# D1 | B1 | S1 | ...&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Web Services Environments
&lt;/h4&gt;

&lt;p&gt;The Web Services contain a significant amount more infrastructure than the single-page application does. This is mainly to accommodate the required database for the application and some configuration secrets.&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;# main.tf&lt;/span&gt;

&lt;span class="s"&gt;terraform {&lt;/span&gt;
  &lt;span class="s"&gt;backend "azurerm" {&lt;/span&gt;
    &lt;span class="s"&gt;container_name = "terraform"&lt;/span&gt;
    &lt;span class="s"&gt;key            = "tfbackend"&lt;/span&gt;
  &lt;span class="s"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;provider "azurerm" {&lt;/span&gt;
  &lt;span class="s"&gt;version = "=1.20.0"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;module "rg" {&lt;/span&gt;
  &lt;span class="s"&gt;source = "../modules/rg"&lt;/span&gt;

  &lt;span class="s"&gt;environment_prefix = "${var.environment_prefix}"&lt;/span&gt;
  &lt;span class="s"&gt;location           = "${var.location}"&lt;/span&gt;
  &lt;span class="s"&gt;tags               = "${var.tags}"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;module "db" {&lt;/span&gt;
  &lt;span class="s"&gt;source = "../modules/db"&lt;/span&gt;

  &lt;span class="s"&gt;environment_prefix              = "${var.environment_prefix}"&lt;/span&gt;
  &lt;span class="s"&gt;location                        = "${module.rg.location}"&lt;/span&gt;
  &lt;span class="s"&gt;tags                            = "${var.tags}"&lt;/span&gt;
  &lt;span class="s"&gt;resource_group_name             = "${module.rg.name}"&lt;/span&gt;
  &lt;span class="s"&gt;db_sku                          = "${var.db_sku}"&lt;/span&gt;
  &lt;span class="s"&gt;db_capacity                     = "${var.db_capacity}"&lt;/span&gt;
  &lt;span class="s"&gt;db_tier                         = "${var.db_tier}"&lt;/span&gt;
  &lt;span class="s"&gt;db_family                       = "${var.db_family}"&lt;/span&gt;
  &lt;span class="s"&gt;db_storage_mb                   = "${var.db_storage_mb}"&lt;/span&gt;
  &lt;span class="s"&gt;db_backup_retention_days        = "${var.db_backup_retention_days}"&lt;/span&gt;
  &lt;span class="s"&gt;db_geo_redundant_backup         = "${var.db_geo_redundant_backup}"&lt;/span&gt;
  &lt;span class="s"&gt;db_administrator_login          = "${var.db_administrator_login}"&lt;/span&gt;
  &lt;span class="s"&gt;db_administrator_login_password = "${var.db_administrator_login_password}"&lt;/span&gt;
  &lt;span class="s"&gt;db_version                      = "${var.db_version}"&lt;/span&gt;
  &lt;span class="s"&gt;db_ssl_enforcement              = "${var.db_ssl_enforcement}"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;module "api" {&lt;/span&gt;
  &lt;span class="s"&gt;source = "../modules/api"&lt;/span&gt;

  &lt;span class="s"&gt;environment_prefix        = "${var.environment_prefix}"&lt;/span&gt;
  &lt;span class="s"&gt;resource_group_name       = "${module.rg.name}"&lt;/span&gt;
  &lt;span class="s"&gt;location                  = "${module.rg.location}"&lt;/span&gt;
  &lt;span class="s"&gt;tags                      = "${var.tags}"&lt;/span&gt;
  &lt;span class="s"&gt;app_service_plan_sku_tier = "${var.app_service_plan_sku_tier}"&lt;/span&gt;
  &lt;span class="s"&gt;app_service_plan_sku_size = "${var.app_service_plan_sku_size}"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;module "kv" {&lt;/span&gt;
  &lt;span class="s"&gt;source = "../modules/kv"&lt;/span&gt;

  &lt;span class="s"&gt;environment_prefix               = "${var.environment_prefix}"&lt;/span&gt;
  &lt;span class="s"&gt;location                         = "${var.location}"&lt;/span&gt;
  &lt;span class="s"&gt;tags                             = "${var.tags}"&lt;/span&gt;
  &lt;span class="s"&gt;resource_group_name              = "${module.rg.name}"&lt;/span&gt;
  &lt;span class="s"&gt;tenant_id                        = "${var.tenant_id}"&lt;/span&gt;
  &lt;span class="s"&gt;azuredevops_object_id            = "${var.azuredevops_object_id}"&lt;/span&gt;
  &lt;span class="s"&gt;key_vault_secret_1               = "${var.key_vault_secret_1}"&lt;/span&gt;
  &lt;span class="s"&gt;key_vault_secret_2               = "${var.key_vault_secret_2}"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;module "storage" {&lt;/span&gt;
  &lt;span class="s"&gt;source = "../modules/storage"&lt;/span&gt;

  &lt;span class="s"&gt;environment_prefix  = "${var.environment_prefix}"&lt;/span&gt;
  &lt;span class="s"&gt;location            = "${var.location}"&lt;/span&gt;
  &lt;span class="s"&gt;tags                = "${var.tags}"&lt;/span&gt;
  &lt;span class="s"&gt;resource_group_name = "${module.rg.name}"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# variables.tf&lt;/span&gt;

&lt;span class="s"&gt;variable "environment_prefix" {&lt;/span&gt;
  &lt;span class="s"&gt;description = "The prefix for the environment."&lt;/span&gt;
  &lt;span class="s"&gt;type        = "string"&lt;/span&gt;
  &lt;span class="s"&gt;default     = "d"&lt;/span&gt;          &lt;span class="c1"&gt;# this is different for each environment&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "location" {&lt;/span&gt;
  &lt;span class="s"&gt;description = "Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created."&lt;/span&gt;
  &lt;span class="s"&gt;type        = "string"&lt;/span&gt;
  &lt;span class="s"&gt;default     = "eastus"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "tags" {&lt;/span&gt;
  &lt;span class="s"&gt;description = "The tags to associate with the resources."&lt;/span&gt;
  &lt;span class="s"&gt;type        = "map"&lt;/span&gt;

  &lt;span class="s"&gt;default = {&lt;/span&gt;
    &lt;span class="s"&gt;"terraform" = "true"&lt;/span&gt;
  &lt;span class="s"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "tenant_id" {&lt;/span&gt;
  &lt;span class="s"&gt;type        = "string"&lt;/span&gt;
  &lt;span class="s"&gt;description = "The Azure Active Directory tenant ID that should be used for authenticating requests to the key vault."&lt;/span&gt;
  &lt;span class="s"&gt;default     = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "azuredevops_object_id" {&lt;/span&gt;
  &lt;span class="s"&gt;default     = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "adminuser_object_id" {&lt;/span&gt;
  &lt;span class="s"&gt;default     = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "app_service_plan_sku_tier" {&lt;/span&gt;
  &lt;span class="s"&gt;type        = "string"&lt;/span&gt;
  &lt;span class="s"&gt;description = "Specifies the plan's pricing tier."&lt;/span&gt;
  &lt;span class="s"&gt;default     = "Shared"&lt;/span&gt;                             &lt;span class="c1"&gt;# Shared | Basic | Standard | ...&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "app_service_plan_sku_size" {&lt;/span&gt;
  &lt;span class="s"&gt;type        = "string"&lt;/span&gt;
  &lt;span class="s"&gt;description = "Specifies the plan's instance size."&lt;/span&gt;
  &lt;span class="s"&gt;default     = "D1"&lt;/span&gt;                                  &lt;span class="c1"&gt;# D1 | B1 | S1 | ...&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "db_sku" {&lt;/span&gt;
  &lt;span class="s"&gt;type        = "string"&lt;/span&gt;
  &lt;span class="s"&gt;description = "Specifies the SKU Name for this PostgreSQL Server. The name of the SKU, follows the tier + family + cores pattern (e.g. B_Gen4_1, GP_Gen5_8)."&lt;/span&gt;
  &lt;span class="s"&gt;default     = "B_Gen5_1"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "db_capacity" {&lt;/span&gt;
  &lt;span class="s"&gt;type        = "string"&lt;/span&gt;
  &lt;span class="s"&gt;description = "The scale up/out capacity, representing server's compute units."&lt;/span&gt;
  &lt;span class="s"&gt;default     = &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "db_tier" {&lt;/span&gt;
  &lt;span class="s"&gt;type        = "string"&lt;/span&gt;
  &lt;span class="s"&gt;description = "The tier of the particular SKU. Possible values are Basic, GeneralPurpose, and MemoryOptimized. "&lt;/span&gt;
  &lt;span class="s"&gt;default     = "Basic"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "db_family" {&lt;/span&gt;
  &lt;span class="s"&gt;type        = "string"&lt;/span&gt;
  &lt;span class="s"&gt;description = "The family of hardware Gen4 or Gen5, before selecting your family check the product documentation for availability in your region."&lt;/span&gt;
  &lt;span class="s"&gt;default     = "Gen5"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "db_storage_mb" {&lt;/span&gt;
  &lt;span class="s"&gt;type        = "string"&lt;/span&gt;
  &lt;span class="s"&gt;description = "Max storage allowed for a server. Possible values are between 5120 MB(5GB) and 1048576 MB(1TB) for the Basic SKU and between 5120 MB(5GB) and 4194304 MB(4TB) for General Purpose/Memory Optimized SKUs."&lt;/span&gt;
  &lt;span class="s"&gt;default     = &lt;/span&gt;&lt;span class="m"&gt;51200&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "db_backup_retention_days" {&lt;/span&gt;
  &lt;span class="s"&gt;type        = "string"&lt;/span&gt;
  &lt;span class="s"&gt;description = "Backup retention days for the server, supported values are between 7 and 35 days."&lt;/span&gt;
  &lt;span class="s"&gt;default     = &lt;/span&gt;&lt;span class="m"&gt;7&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "db_geo_redundant_backup" {&lt;/span&gt;
  &lt;span class="s"&gt;type        = "string"&lt;/span&gt;
  &lt;span class="s"&gt;description = "Enable Geo-redundant or not for server backup. Valid values for this property are Enabled or Disabled, not supported for the basic tier."&lt;/span&gt;
  &lt;span class="s"&gt;default     = "Disabled"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "db_administrator_login" {&lt;/span&gt;
  &lt;span class="s"&gt;type        = "string"&lt;/span&gt;
  &lt;span class="s"&gt;description = "The Administrator Login for the PostgreSQL Server. Changing this forces a new resource to be created."&lt;/span&gt;
  &lt;span class="s"&gt;default     = "adminuser"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "db_administrator_login_password" {&lt;/span&gt;
  &lt;span class="s"&gt;type        = "string"&lt;/span&gt;
  &lt;span class="s"&gt;description = "The Administrator Login's password for the PostgreSQL Server. Required to be passed in."&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "db_version" {&lt;/span&gt;
  &lt;span class="s"&gt;type        = "string"&lt;/span&gt;
  &lt;span class="s"&gt;description = "Specifies the version of PostgreSQL to use. Valid values are 9.5, 9.6, and 10.0. Changing this forces a new resource to be created."&lt;/span&gt;
  &lt;span class="s"&gt;default     = "10.0"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "db_ssl_enforcement" {&lt;/span&gt;
  &lt;span class="s"&gt;type        = "string"&lt;/span&gt;
  &lt;span class="s"&gt;description = "Specifies if SSL should be enforced on connections. Possible values are Enabled and Disabled."&lt;/span&gt;
  &lt;span class="s"&gt;default     = "Enabled"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "key_vault_secret_1" {&lt;/span&gt;
  &lt;span class="s"&gt;type        = "string"&lt;/span&gt;
  &lt;span class="s"&gt;description = "The first secret"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;variable "key_vault_secret_2" {&lt;/span&gt;
  &lt;span class="s"&gt;type        = "string"&lt;/span&gt;
  &lt;span class="s"&gt;description = "The second secret"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Setup and Destroy Scripts
&lt;/h4&gt;

&lt;p&gt;For those of you who were paying attention, you'll notice that I have two scripts in each of the environment folders: &lt;code&gt;setup.bat&lt;/code&gt; and &lt;code&gt;destroy.bat&lt;/code&gt;. Each of these files are &lt;strong&gt;ignored&lt;/strong&gt; in the &lt;code&gt;.gitignore&lt;/code&gt; file, as they contain Azure secrets, but they're useful for while I was developing the solutions. The contents can be seen below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="c"&gt;rem setup.bat&lt;/span&gt;

@ECHO &lt;span class="kd"&gt;OFF&lt;/span&gt;

&lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="kd"&gt;ARM_ACCESS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kd"&gt;xxxxx&lt;/span&gt;

&lt;span class="kd"&gt;terraform&lt;/span&gt; &lt;span class="kd"&gt;init&lt;/span&gt; &lt;span class="na"&gt;-backend-config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"storage_account_name=dterraformstorage"&lt;/span&gt;
&lt;span class="kd"&gt;terraform&lt;/span&gt; &lt;span class="kd"&gt;fmt&lt;/span&gt;
&lt;span class="kd"&gt;terraform&lt;/span&gt; &lt;span class="kd"&gt;validate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="c"&gt;rem destroy.bat&lt;/span&gt;

@ECHO &lt;span class="kd"&gt;OFF&lt;/span&gt;

&lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="kd"&gt;ARM_ACCESS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kd"&gt;xxxxx&lt;/span&gt;

&lt;span class="kd"&gt;terraform&lt;/span&gt; &lt;span class="kd"&gt;init&lt;/span&gt; &lt;span class="na"&gt;-backend-config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"storage_account_name=dterraformstorage"&lt;/span&gt;
&lt;span class="kd"&gt;terraform&lt;/span&gt; &lt;span class="kd"&gt;destroy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Azure Release Pipeline
&lt;/h2&gt;

&lt;p&gt;I detailed the source-controlled Build Pipelines above, but I explicitly left out the Release Pipelines in the conversation. Azure Pipelines currently has no support for configuration-as-code for Release Pipelines yet. While this is a detriment to the product offering as a whole, the web UI of the Release Pipeline is generally a good one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Single-Page Application Release Pipeline
&lt;/h3&gt;

&lt;p&gt;There exists a single Release Pipeline for the entire single-page application, each with a number of stages defined. Within each of these stages, a number of tasks are run.&lt;/p&gt;

&lt;h4&gt;
  
  
  SPA Release Pipeline Stages
&lt;/h4&gt;

&lt;p&gt;A given stage can be seen as the functional equivalent to an environment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fimtogismlphyqiy93mlx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fimtogismlphyqiy93mlx.png" alt="Single-Page Application Release Pipeline Stages" width="800" height="870"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The image defines a few paths that a given set of artifacts from the Build Pipeline can take:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatically deployed to the Development stage if it originates from the &lt;code&gt;develop&lt;/code&gt; branch&lt;/li&gt;
&lt;li&gt;Manually deployed to any of the three Testing environments&lt;/li&gt;
&lt;li&gt;Automatically deployed to the Staging stage if it originates from the &lt;code&gt;master&lt;/code&gt; branch. It can then get manually elevated to the Production stage if team members approve the release.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  SPA Release Pipeline Stage Tasks
&lt;/h4&gt;

&lt;p&gt;You'll notice that each of the stages have four tasks: each of these tasks is generally the same, minus a few environment variables.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6qn3ok2va260m6wwp1lv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6qn3ok2va260m6wwp1lv.png" alt="Single-Page Application Release Pipeline Tasks" width="650" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These tasks are relatively straightforward. Note that the Terraform tasks are added from the &lt;a href="https://marketplace.visualstudio.com/items?itemName=charleszipp.azure-pipelines-tasks-terraform" rel="noopener noreferrer"&gt;Marketplace&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Download the Terraform binaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhbwnzoh7yh74ticc3mjm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhbwnzoh7yh74ticc3mjm.png" alt="Single-Path Application Release Pipeline: Install Terraform" width="800" height="349"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Initialize the Terraform backend (using &lt;code&gt;azurerm&lt;/code&gt; in this case)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnhs3reyjnnkgkiu3ssbf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnhs3reyjnnkgkiu3ssbf.png" alt="Single-Path Application Release Pipeline: Initialize Terraform" width="800" height="491"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Apply the Terraform configuration to the specified environment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5g6ay448hhgieax12rzo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5g6ay448hhgieax12rzo.png" alt="Single-Path Application Release Pipeline: Apply Terraform" width="800" height="506"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deploy the &lt;code&gt;dist&lt;/code&gt; directory (the built application) into the previously-created App Service&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffxwgaa2tdeohbmv5c7ao.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffxwgaa2tdeohbmv5c7ao.png" alt="Single-Path Application Release Pipeline: Deploy" width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Web Services Release Pipeline
&lt;/h3&gt;

&lt;p&gt;The web services release pipeline is very similar to the single-page application's release pipeline in its concepts. There just happen to be a few more steps, specifically related to the database automation.&lt;/p&gt;

&lt;h4&gt;
  
  
  WS Release Pipeline Stages
&lt;/h4&gt;

&lt;p&gt;Each of the stages below, just like the previous release pipeline, is analogous to a given environment for the application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkcmzn4nswyc74kru3hkt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkcmzn4nswyc74kru3hkt.png" alt="Web Services Release Pipeline Stages" width="800" height="865"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The paths an artifact can take are the same as the single-page application:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatically deployed to the Development stage if it originates from the &lt;code&gt;develop&lt;/code&gt; branch&lt;/li&gt;
&lt;li&gt;Manually deployed to any of the three Testing environments&lt;/li&gt;
&lt;li&gt;Automatically deployed to the Staging stage if it originates from the &lt;code&gt;master&lt;/code&gt; branch. It can then get manually elevated to the Production stage if team members approve the release.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  WS Release Pipeline Stage Tasks
&lt;/h4&gt;

&lt;p&gt;Each of the stages for this Release Pipeline has six tasks. Each of the stages are the same six steps, just with some differences in the environment variables.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F10vqq57iokzgwqmkdbl2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F10vqq57iokzgwqmkdbl2.png" alt="Web Services Release Pipeline Tasks" width="649" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The tasks for this application's release pipeline can be seen below.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Download the Terraform binaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9sgdilthz6ens1y8gc13.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9sgdilthz6ens1y8gc13.png" alt="Web Services Release Pipeline: Install Terraform" width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Initialize the Terraform backend (using &lt;code&gt;azurerm&lt;/code&gt; in this case)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5r46lqquths9ng3er0op.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5r46lqquths9ng3er0op.png" alt="Web Services Release Pipeline: Initialize Terraform" width="800" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Apply the Terraform configuration to the specified environment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frv48vf9oitzkr2zhqssk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frv48vf9oitzkr2zhqssk.png" alt="Web Services Release Pipeline: Apply Terraform" width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run the database migrator application&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuui3fgs7esrt47uci6qe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuui3fgs7esrt47uci6qe.png" alt="Web Services Release Pipeline: Run Migrator" width="800" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run the database seeder application&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fli06i1v6ulcpi8fwerda.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fli06i1v6ulcpi8fwerda.png" alt="Web Services Release Pipeline: Run Seeder" width="800" height="339"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deploy the packaged web application into the previously-created App Service&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ne8wdrxbk9xtefugxfo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ne8wdrxbk9xtefugxfo.png" alt="Web Services Release Pipeline: Deploy" width="800" height="548"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Next Steps
&lt;/h1&gt;

&lt;p&gt;Sorry, this got really long.&lt;/p&gt;

&lt;p&gt;As stated in the previous sections, there's still a bit of work to do from the application development teams with respect to automated testing. The pipelines detailed above allow for easy adaptation to utilize any of the testing frameworks needed.&lt;/p&gt;

&lt;p&gt;There are a number of small improvements or refactorings that can be done across the application tiers, but those will be pushed to later in the project's lifecycle.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>terraform</category>
    </item>
    <item>
      <title>ASP.NET Static Site Generation</title>
      <dc:creator>Tony Morris</dc:creator>
      <pubDate>Tue, 21 Aug 2018 00:00:00 +0000</pubDate>
      <link>https://forem.com/tonytalkstech/aspnet-static-site-generation-23lk</link>
      <guid>https://forem.com/tonytalkstech/aspnet-static-site-generation-23lk</guid>
      <description>&lt;p&gt;As hinted at in a &lt;a href="https://dev.to/tonytalkstech/getting-my-aws-cost-to-nearly-nothing-c78"&gt;previous post&lt;/a&gt;, I have transitioned from hosting dynamic web applications in a cloud hosting service (like &lt;a href="https://aws.amazon.com/elasticbeanstalk/" rel="noopener noreferrer"&gt;AWS Elastic Beanstalk&lt;/a&gt; or &lt;a href="https://azure.microsoft.com/en-us/services/app-service/" rel="noopener noreferrer"&gt;Azure App Service&lt;/a&gt;) to hosting static websites in my document storage location of choice (&lt;a href="https://aws.amazon.com/s3/" rel="noopener noreferrer"&gt;Amazon S3&lt;/a&gt; in this case). This post discusses my development process for a project like this.&lt;/p&gt;

&lt;h1&gt;
  
  
  Existing Projects
&lt;/h1&gt;

&lt;p&gt;I have a number of projects that currently utilize this deployment paradigm. They are all some flavor of ASP.NET developed locally with a local database, which are then exported and uploaded to S3.&lt;/p&gt;

&lt;h2&gt;
  
  
  MorrisPhotos.com
&lt;/h2&gt;

&lt;p&gt;My photography website, &lt;a href="https://morrisphotos.com" rel="noopener noreferrer"&gt;MorrisPhotos.com&lt;/a&gt;, is a static website hosted in S3 and pushed to the edge via &lt;a href="https://aws.amazon.com/cloudfront/" rel="noopener noreferrer"&gt;Amazon CloudFront&lt;/a&gt;. I've got &lt;a href="https://cloudflare.com" rel="noopener noreferrer"&gt;CloudFlare&lt;/a&gt; handling DNS and threat detection, and it all just works out really nicely.&lt;/p&gt;

&lt;h2&gt;
  
  
  LodiCornFest5k.com
&lt;/h2&gt;

&lt;p&gt;A local race website, &lt;a href="https://lodicornfest5k.com" rel="noopener noreferrer"&gt;LodiCornFest5k.com&lt;/a&gt;, is also a static website hosted in S3. I don't have CloudFront in front of this guy, as it's less important to aggressively cache the data on the website. &lt;/p&gt;

&lt;h2&gt;
  
  
  OhioTrackStats.com
&lt;/h2&gt;

&lt;p&gt;My pride and joy, &lt;a href="https://ohiotrackstats.com" rel="noopener noreferrer"&gt;OhioTrackStats.com&lt;/a&gt;, is a static website hosted in S3. There will be later blog posts about this one at another date.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Process
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Local Development
&lt;/h2&gt;

&lt;p&gt;I'm a .NET developer. This means I am most comfortable slinging some ASP.NET code, especially for local development. As .NET Core 2.1 is the de facto standard, all my new projects are moving toward that.&lt;/p&gt;

&lt;p&gt;A simple example is my &lt;a href="https://github.com/afmorris/MorrisPhotos" rel="noopener noreferrer"&gt;MorrisPhotos.com source&lt;/a&gt;. Maybe one day I'll go into further detail on how I code, but this is not the purpose of the post today.&lt;/p&gt;

&lt;p&gt;My local setup is pretty straightforward: I've got &lt;a href="https://visualstudio.microsoft.com/vs/" rel="noopener noreferrer"&gt;Visual Studio 2017 Community&lt;/a&gt; running on my home machine with &lt;a href="http://www.jetbrains.com/resharper/" rel="noopener noreferrer"&gt;ReSharper&lt;/a&gt; plugged in. Nothing special here with the setup. You could do all of this with &lt;a href="https://code.visualstudio.com/" rel="noopener noreferrer"&gt;VS Code&lt;/a&gt; instead, but I like the fully-featured IDE.&lt;/p&gt;

&lt;p&gt;With respect to local development, I'm adding new features, fixing any issues, or just generally running the application locally on my home machine to see how it's working. Nothing too special here, but this is the last place that the application is truly dynamic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local Database
&lt;/h2&gt;

&lt;p&gt;I've got SQL Server 2017 Express running on my home machine. It's nothing fancy, but it's got the databases that drive each of the projects listed above. To access the databases in the code, I typically use either &lt;a href="https://docs.microsoft.com/en-us/ef/core/" rel="noopener noreferrer"&gt;Entity Framework Core&lt;/a&gt; or &lt;a href="https://servicestack.net/ormlite" rel="noopener noreferrer"&gt;ServiceStack ORMLite&lt;/a&gt;. I don't really have any speed or performance requirements with the database access layer, so I'm basically writing bad code to access the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  S3 Setup
&lt;/h2&gt;

&lt;p&gt;In order to publish a website using S3, I follow &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/user-guide/static-website-hosting.html" rel="noopener noreferrer"&gt;this guide&lt;/a&gt;. It's very self-explanatory, and I recommend you go through each step in order to get it correct.&lt;/p&gt;

&lt;p&gt;For example's sake, I've got both a &lt;code&gt;www.ohiotrackstats.com&lt;/code&gt; bucket and a &lt;code&gt;ohiotrackstats.com&lt;/code&gt; bucket in S3 currently for the OhioTrackStats.com website. The &lt;code&gt;www&lt;/code&gt; bucket redirects directly to the non-&lt;code&gt;www&lt;/code&gt; bucket. This is the pattern that I've got for each of my projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Domain Setup
&lt;/h2&gt;

&lt;p&gt;As mentioned previously, I've got DNS in CloudFlare's infrastructure. For MorrisPhotos.com, this looks like the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Domain registered through Google Domains&lt;/li&gt;
&lt;li&gt;Name Servers set to CloudFlare's name servers&lt;/li&gt;
&lt;li&gt;A couple CNAME records in CloudFlare

&lt;ul&gt;
&lt;li&gt;morrisphotos.com -&amp;gt; non-&lt;code&gt;www&lt;/code&gt; S3 bucket hostname&lt;/li&gt;
&lt;li&gt;
&lt;a href="http://www.morrisphotos.com" rel="noopener noreferrer"&gt;www.morrisphotos.com&lt;/a&gt; -&amp;gt; &lt;code&gt;www&lt;/code&gt; S3 bucket hostname&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Static Site Generation
&lt;/h2&gt;

&lt;p&gt;This is where the real magic happens. Given a dynamic web application, I want to create a static HTML website that can be hosted directly through S3.&lt;/p&gt;

&lt;p&gt;I personally use &lt;a href="https://www.httrack.com/" rel="noopener noreferrer"&gt;HTTrack Website Copier&lt;/a&gt; to crawl the locally-running ASP.NET web application and generate the HTML, CSS, and JavaScript files to a folder structure. The beauty of this tool is that it will update all the links on a website to be relative paths, allowing for the website to be run from anywhere that a web server exists.&lt;/p&gt;

&lt;p&gt;I make sure to exclude all external CSS and JS files (files hosted on external CDNs, like the Bootstrap files, for example), as I don't want to be responsible for hosting their content in my website. Beyond that, the setup is very straightforward, and once I've got a website setup once, I never have to update it.&lt;/p&gt;

&lt;p&gt;In order to properly execute this step, I need to make sure I'm actively running the web application locally, and then I point to the tool the port that IIS Express is running on. I try to make sure to run the application in Release mode in order to get the built-in benefits of bundling and minification, as well.&lt;/p&gt;

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

&lt;p&gt;Once I've got the static site generated, all I've got to do is copy the output to the non-&lt;code&gt;www&lt;/code&gt; S3 bucket. I can do this via the &lt;a href="https://console.aws.amazon.com" rel="noopener noreferrer"&gt;AWS Console&lt;/a&gt;, but I'd rather not upload the entire web site every time I make a small update.&lt;/p&gt;

&lt;p&gt;To combat full uploads, I am utilizing the &lt;a href="https://aws.amazon.com/cli/" rel="noopener noreferrer"&gt;AWS CLI&lt;/a&gt;. Specifically, I am using a batch file that runs the static site generator, then runs the S3 sync CLI command to push only the changed items. A sample batch script can be seen below. I'll describe after.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="s2"&gt;"C:\Program Files\WinHTTrack\httrack.exe"&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:59141"&lt;/span&gt; &lt;span class="na"&gt;-O &lt;/span&gt;&lt;span class="s2"&gt;"C:\My Web Sites\LodiCornFest5k"&lt;/span&gt; &lt;span class="na"&gt;--update
&lt;/span&gt;&lt;span class="kd"&gt;aws&lt;/span&gt; &lt;span class="kd"&gt;s3&lt;/span&gt; &lt;span class="kd"&gt;sync&lt;/span&gt; &lt;span class="s2"&gt;"C:\My Web Sites\LodiCornFest5k\localhost_59141"&lt;/span&gt; &lt;span class="kd"&gt;s3&lt;/span&gt;://lodicornfest5k.com &lt;span class="na"&gt;--acl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"public-read"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first line in the batch script is to run the HTTrack software from the command line, pointing to &lt;code&gt;http://localhost:59141&lt;/code&gt;, which is the IIS Express website that I set up for my LodiCornFest5k project in Visual Studio. The &lt;code&gt;-O&lt;/code&gt; switch points the application to the mirror location, which is something I already set up in the GUI with the options I wanted. Finally, we use the &lt;code&gt;-update&lt;/code&gt; switch to ensure that it only writes files that have changed since the last time we ran the tool.&lt;/p&gt;

&lt;p&gt;The second line in the batch script is the upload line. It runs the &lt;code&gt;aws s3 sync&lt;/code&gt; command, which has already been configured on my machine to use an IAM user that has write access to all my S3 buckets. This isn't the &lt;strong&gt;best&lt;/strong&gt; security, as I should probably limit that IAM user to just the buckets that I need, but it'll do for now. I then point it to the location of the files on the host machine (&lt;code&gt;C:\My Web Sites\LodiCornFest5k\localhost_59141&lt;/code&gt;), and then the bucket location to push the files to (&lt;code&gt;s3://lodicornfest5k.com&lt;/code&gt; in this case). In addition, I add the &lt;code&gt;--acl="public-read"&lt;/code&gt; flag to the uploaded files to ensure that they can be viewed over internet without authenticating, as my website is publicly available.&lt;/p&gt;

&lt;p&gt;Again, since I'm generating the static site within this batch file, I need to make sure that I'm running the website locally. I've kicked this batch file off a number of times without doing that, and the whole thing borks pretty hard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future Enhancements
&lt;/h2&gt;

&lt;p&gt;There is one major enhancement that I want to get to, but I haven't tried it yet. Instead of manually running the batch file locally when I make changes, I would love for a CI server to be able to do it instead. It would require me to package up the &lt;code&gt;httrack.exe&lt;/code&gt; binary with my application. Then, upon commit to the &lt;code&gt;master&lt;/code&gt; branch, I would have something like a Continuous Integration tool like &lt;a href="https://www.appveyor.com/" rel="noopener noreferrer"&gt;AppVeyor&lt;/a&gt; run a post-build command that is similar to the batch file itself. This would ensure that the website in S3 directly matches what's in source control, which is the holy grail.&lt;/p&gt;

&lt;p&gt;I'll probably get around to that one in the next year or so, so stay tuned!&lt;/p&gt;




&lt;h1&gt;
  
  
  Review
&lt;/h1&gt;

&lt;p&gt;In all, the "do work" stage of building my custom web applications hasn't really drastically changed with this new hosting model. I'm still running and testing locally whenever I make changes. Instead of pushing the built bits out to a web application server, though, I am just pushing out the generated files instead. It's a much more elegant and speedy solution for both me and the end user!&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost Saving
&lt;/h2&gt;

&lt;p&gt;Previously, I was using a hosted application platform for something like MorrisPhotos.com (Elastic Beanstalk, specifically). This meant that I was incurring charges on an EC2 instance, an RDS instance, plus some networking and hardware costs. This cost upwards of $100/month just to host and run the web application.&lt;/p&gt;

&lt;p&gt;Now, with just the S3 buckets hosting the static website (and the thousands of photos in a separate S3 bucket), plus CloudFront as the CDN layer in front of the S3 bucket, I am paying less than $10/month. I don't have the exact numbers at the moment, but we're looking at roughly a 90% savings per month. I'd say this was absolutely worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Time Saving
&lt;/h2&gt;

&lt;p&gt;I don't have hard numbers on the actual time savings for the end users, but I do have a general understanding that, instead of a web application server (IIS in this case) receiving a request, handing off to the web application to process, handing off to the database to retrieve data, then pushing it back up the pipeline to the end user, I am simply giving the user a typically-cached HTML/JS/CSS/image file instead. The speed that the end user is seeing is significantly faster, on both a time to first byte aspect (due to the edge locations of CloudFront in MorrisPhotos.com's case, generally) and a total download time aspect.&lt;/p&gt;

&lt;p&gt;All in all, this new deployment and hosting paradigm has greatly improved everyone's experiences with my "dynamic" web applications, and it's certainly something I'll be doing from now on for web applications that need to be database-driven but only updated somewhat regularly.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>aspnet</category>
    </item>
  </channel>
</rss>
